Tutorial de AngularJS #5

Una de las características más importantes de AngularJS son las directivas. En este quinto episodio repasaremos cómo crearlas y utilizarlas.

En el episodio anterior vimos todo lo relacionado a la comunicación entre controladores y la forma en que usando los métodos $emit, $on y $broadcast podemos actualizar vistas sin tener que actualizar el navegador. El resultado del ejemplo anterior se veía así:

Si bien esta interfaz está casi completa, nos falta la funcionalidad para el botón “favorito” la cual quiero implementar a través de una directiva. Pero no de esas directivas listas y programadas que vimos en el capítulo anterior

Pero, ¿Qué es una directiva? diran ustedes. Pues bien, una directiva (o más conocida como directive) es una funcion que permite modificar el comportamiento de los elementos del DOM (o sea, del HTML). Se componen de una configuración que define su comportamiento, que posteriormente utiliza el compilador de AngularJS ($compile) para ejecutarse y funcionar.

Las Directivas

Una directiva o directive, funciona como un módulo dentro de nuestra aplicación y puede ser declarada del siguiente modo:

app.directive('miDirectiva', function(){ ... });

En donde el nombre de la directiva se escribe usando “camel case” y luego es usada como tag. Por ejemplo:

gifwalletApp.directive('favorite', function(){ ... });

Acá, estamos definiendo la directiva favorite, y a medida que se necesite, se puede configurar según el comportamiento esperado.

Para configurar las directivas necesitamos conocer sus componentes y las forma de configurarlos. Las opciones de configuración posibles son:

  • restrict: Esta opción define el cómo será utilizado nuestra directiva. Puede ser definido como ‘A‘ para que sea un atributo, ‘E‘ para que sea utilizado como elemento o un ‘C‘ para que se utilice como clase.
  • scope: Esta opción permite aislar el scope de la directiva para que sea utilizado internamente, y no se mezcle con el scope de la aplicación. A esta opción se le deben dar los atributos que necesitamos que la directiva controle.
  • compile: Esta opción permite realizar funciones cuando la directiva es compilada por $compile y que es cuando el HTML ya es procesado
  • link: Esta opción permite hacer modificaciones de comportamiento antes de que el elemento sea compilado por $compile
  • templateUrl: Con esta opción podemos definir cuál será el template HTML de nuestra directiva, dando el nombre del template (archivo o en código).
  • template: Acá se debe configurar la vista de la directiva, es la forma general de definir el formato de salida de nuestra directiva
  • transclude: Esta opción permite que en lugar de reemplazar la directiva con el HTML del template, ésta incluya dentro de la directiva el contenido que contiene nuestra directiva.
  • replace: Esta opción se define como true o false y determina si el elemento será reemplazado por completo por el template.
  • require: Cuando hay dependencias de directivas, es decir, una que contenga a otra, se utiliza esta opción. Se debe definir el nombre de la directiva a la cual depende.

Estas opciones en general son las que permiten configurar una directiva, y sé que es difícil explicar su utilización tanto como utilizarlas. Por eso les recomiendo que complementen con los siguientes links:

Implementando nuestra directiva

De acuerdo a lo anterior intentaré explicar el objetivo de usar una directiva usando algunas de las opciones de arriba. Acá quiero hacer una pausa ya que estoy super claro que lo anterior no fue siquiera detallado pero la verdad es que es difícil explicarlo en palabras simples, así que por eso haré el ejemplo con una directiva que hice para nuestra aplicación.

La directiva que hice se llama favorite y tiene por objetivo mostrar un ícono de un corazón lleno si es que es favorito y un ícono de un corazón vacío si no lo es.

¿Se entiende? Para hacer esa magia usaremos una directiva que cambie el valor del atributo “favorite” del elemento seleccionado a true.

Para esto agregaremos a nuestro archivo app.js el siguiente código:

gifwalletApp.directive('favorite', ['Storage', '$compile',
    function (Storage, $compile) {
        return {

        }
    }   
]);

Este código está definiendo nuestra directiva llamada “favorite”, y le está inyectando dos servicios: Storage y $compile, que utilizaremos posteriormente.

Dentro de nuestro return es donde debemos definir nuestras opciones. Para este caso vamos a necesitar:

  • restrict : Que nos permitirá definir que nuestra directiva funcionará solo como Elemento.
  • replace: Utilizaremos esta opción en true para determinar que la directiva reemplace nuestro código.
  • link: Nos permitirá definir la interfaz de nuestro elemento, además de agregarle las funciones que hagan la actualización del valor del elemento.
  • scope: Con esta opción en true podremos acceder al scope interno del elemento. Será necesario para enviarle la llave del elemento que estamos haciendo click y que nos permitirá identificarlo en la lista de gifs.

Ya con esto definido, el código evoluciona a lo siguiente:

gifwalletApp.directive('favorite', ['Storage', '$compile',
    function (Storage, $compile) {
        return {
            restrict: 'E',
            replace: true,
            link: function(scope, elem, attrs) {
            },
            scope: true
        }
    }   
]);

Si se dan cuenta, definimos nuestra directiva como elemento utilizando la letra E. Luego, dejamos el atributo replace en true, para que elemento sea reemplazado por lo que escribiremos en link. Finalmente, en scope dejamos true para que todos los atributos pasados a través del tag <favorite> sean utilizables a través del scope.

Hasta el momento nuestra directiva no muestra nada, pero en nuestro HTML ya podemos utilizarla escribiendo <favorite> en nuestro template de la lista.

<div id="images" class="row" ng-controller="GifListController">
    <div class="col-lg-12 media" ng-repeat="(key, gif) in giflist">
        <a href="" class="pull-left thumbnail">
            <img class="media-object" src="{{ gif.url }}" width="200">
        </a>
        <div class="media-object">
            <h4 class="media-heading">{{ gif.name }}</h4>
            <p>
                <a><span class="glyphicon glyphicon-minus"></span> Eliminar</a>
            </p>    
            <p>
                <favorite favorite="{{ gif.favorite }}" key="{{ key }}"></favorite>
            </p>
            <p>
                Link: <a href="{{ gif.url }}">{{ gif.url }}</a>
            </p>
        </div>
    </div>
</div>

Acá, en <favorite favorite="{{ gif.favorite }}" key="{{ key }}"></favorite> estamos agregando dos atributos que son claves:

  1. favorite, que le entregará a nuestra directiva el valor del elemento, que puede ser true o false.
  2. El atributo key, que es la llave para identificar el elemento que vamos a modificar cuando hagamos click en el corazón.

Además, si miran la segunda línea del código, verán que el ng-repeat contiene (key, gif) que es de dónde obtenemos nuestro key. Con estos dos datos podemos hacer la magia dentro de nuestra directiva.

La magia de la directiva

El último paso ahora es actualizar la información de un elemento al hacer click, y como necesitamos que la interacción sea mediante click, usaremos el tag <a> como vista resultante de la directiva.

Por lo tanto, en nuestra opcion link haremos lo siguiente:

gifwalletApp.directive('favorite', ['Storage', '$compile',
    function (Storage, $compile) {
        return {
            restrict: 'E',
            replace: true,
            link: function(scope, elem, attrs) {
                html = '<a><span class="glyphicon"></span> Favorito</a>';

                elem.replaceWith($compile(html)(scope));
            },
            scope: true
        }
    }   
]);

Acá definimos una variable llamada html que será la que reemplazará el atributo favorite, y utilizaremos $compile para que sea procesado por AngularJS. La opción link recibe un callback que entrega como parámetros el scope, el elemento y los atributos. Y usando la función replaceWith reemplazaremos el contenido. Usando esto el resultado es el siguiente:

Ahora, el último paso es agregar la función de guardar el favorito al hacer click sobre el link y que la clase del span cambie entre glyphicon-heart y glyphicon-heart-empty. Para esto debemos hacer dos cosas: 1) Agregar el método favorite a nuestro servicio Storage y 2) crear una función que haga la diferencia entre las clases según el valor.

Para esto modificaremos el servicio Storage agregando dos funciones, get y favorite. El código queda del siguiente modo:

gifwalletApp.service('Storage', ['$window', '$rootScope',
    function($window, $rootScope) {
        ...

        this.get = function(key) {
            return images[key];
        }

        this.favorite = function(key) {
            images = images.reverse();
            images[key].favorite = (images[key].favorite) ? false : true;
            images = images.reverse();
            imagesString = JSON.stringify(images);
            $window.localStorage.setItem('gifWallet', imagesString);
            $rootScope.$broadcast('reloadList');
        }
    }
]);

Si se dan cuenta, estas dos funciones permiten obtener un elemento según su llave y cambia el valor del elemento de true a false, segun corresponda, almacenando el valor dentro de la lista de gifs. Analicen bien el funcionamiento porque es bien simple si es que leyeron los tutoriales anteriores.

Con estas dos funciones listas implementaremos lo siguiente en nuestra directiva:

gifwalletApp.directive('favorite', ['Storage', '$compile',
    function (Storage, $compile) {
        return {
            restrict: 'E',
            replace: true,
            link: function(scope, elem, attrs) {
                html = '<a ng-click="favItem('+attrs.key+')"><span class="glyphicon" ng-class="getClass('+attrs.key+')"></span> Favorito</a>';

                scope.favItem = function(key) {
                    Storage.favorite( key );
                };

                scope.getClass = function(key) {
                    return (scope.giflist[key].favorite) ? 'glyphicon-heart' : 'glyphicon-heart-empty';
                };

                elem.replaceWith($compile(html)(scope));
            },
            scope: true
        }
    }   
]);

Acá vemos que agregamos la función favItem al scope interno de la directiva y se lo definimos al atributo a usando ng-click. Esta función utiliza el método recién agregado y que cambia el valor favorite del elemento, usando el método favorite.

Por otra parte, la función getClass entrega la clase correcta en base al valor. Si es true devuelve glyphicon-heart, si es false entrega glyphicon-heart-empty.

Con todo esto definido, el resultado es el siguiente:

Finalmente, les recomiendo revisar bien el código y sigan leyendo sobre directivas, ya que hacen a AngularJS muy potente a la hora de hacer aplicaciones interactivas.

Agradezco a todos los que han seguido este tutorial, la próxima entrega será la última.

Blog, Tutorial, Tutorial Javascript
8 minutos