Directivas con AngularJS

De ChuWiki

AngularJS permite cambiar una parte del documento html por otra de una forma sencilla. Podemos inventar nuestros propios tags html o nuestros propios atributos para tags existentes y AngularJS, indicándoselo por medio de directivas, modificará estos tags por el contenido que queramos. Veamos algunos ejemplos sencillos.


Puedes ver el código completo en AngularJS Examples, en los ficheros directive.html y lib/directive.js

Crear nuestro propio tag[editar]

Vamos a hacer un ejemplo sencillote, en la realidad se puede complicar bastante más. Imagina que en nuestro html ponemos

<head>
<script
   src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.12/angular.min.js"></script>
<script src="lib/directive.js"></script>
</head>

<body data-ng-app="myApp">
   <owntag></owntag>
   <p>This is a text</p>
</body>

En el head añadimos la librería javascript de AngularJS y también nuestra librería lib/directive.js que explicaremos a continuación. En el body hemos puesto data-ng-app="myApp" para indicarle a AngularJS que debe manejar el body de nuestro html. Puedes ver detalles de todo esto en Ejemplos Basicos con AngularJS.

Lo que nos ocupa aquí es que hemos puesto un tag <owntag></owntag> de nuestra invención y queremos que AngularJS lo reemplace por otro contenido de más interés, por ejemplo, un párrafo. Es importante abrir y cerrar el tag de esta manera, si usamos la forma abreviada <owntag/> es posible que no funcione correctamente.

En el fichero lib/directive.js ponemos algo como esto para definir la directiva que cambiará owntag por un párrafo

var myApp = angular.module('myApp',[]);

myApp.directive ('owntag', function(){
   return {
      template: '<p>Own Tag</p>',
      restrict : 'E'
   };
});

Creamos el módulo de AngularJS de nombre myApp, mismo nombre que pusimos en el body por medio de data-ng-app="myApp". Ese módulo lo guardamos en la variable javascript myApp.

Llamando a la función directive() de ese módulo creamos la directiva. A la función directive() se le pasan dos parámetros:

  • Nombre de la directiva, que debe coincidir con el tag owntag que nos hemos inventado.
  • La función que le dirá a AngularJS por qué tiene que reemplazar el tag owntag.

La parte ineresante es la función. Debe devolver un objeto javascript que puede tener varios datos dentro. En este caso necesitamos dos

  • template debe ser un string con el contenido html que queremos poner en lugar de owntag. En este caso un párrafo.
  • restrict puede ser una combinación de las letras A, E y C. A indica atributo de un tag html, E indica el tag html en sí mismo y C indica un class de un elemento html. La directiva reemplazará :
    • tags owntag si devolvemos 'E', es decir, reemplazará los <owntag></owntag> que encuentre
    • tags con un atributo owntag si devolvemos 'A', es decir, reemplazará los <div owntag=""></div> que encuentre. Por supuesto, no tienen que ser div, vale cualquier tag html al que pongamos ese atributo
    • tags con class owntag si devolvemos 'C'.
    • Valen combinaciones, como 'AE', 'AEC', etc.

Por defecto, si no ponemos restrict, el valor es 'A', es decir, para atributos. En nuestro caso qeremos reemplazar un tag, así que debemos poner el 'E'

Y esto es todo lo que necesitamos. Si visualizamos la página que contiene ese html, en vez de el tag owntag, tendremos el párrafo que muestra Own Tag.

Mirar el valor del atributo[editar]

En la directiva podemos acceder al elemento que queremos reemplazar, sus atributos y los valores de los mismos. De esta forma, podríamos hacer cambios teniendo en cuenta estos valores. Como ejemplo, supongamos un div al que ponemos un atributo de nuestra invención, así

<div data-ownattribute="hello you"></div>

queremos reemplazar este div por un párrafo y cuyo contenido sea el valor del atributo data-ownattribute. La directiva que debemos hacer sería de esta manera

myApp.directive ('ownattribute', function(){
   return {
      template : '<p>{{text}}</p>',
      link : function (scope,elem,attrs) {
         scope.text=attrs.ownattribute;
      }
   };
});

El nombre de la directiva es el mismo del atributo quitándole el data-. Lo de poner delante data- es para hacer que sea un atributo válido en html, no es obligatorio ponerlo, el atributo podría ser directamente ownattribute. AngularJS es lo suficientemente inteligente como para ignorar el data- y coger el resto. Si el atributo fueran varias palabras separadas con guiones, como-este-atributo, AngularJS quita los guiones y pone mayúscula la primera detrás de cada guión, el nombre a poner en la directiva sería comoEsteAtributo.

En la función de la directiva devolvemos como antes un objeto javascript con una serie de datos dentro.

No nos hace falta el restrict que pusimos antes puesto que por defecto su valor es 'A' (atributos), que es nuestro caso.

En el template hemos puesto el párrafo, pero en vez de poner el texto, hemos puesto una variable entre dobles llaves {{text}}, que AngularJS reemplazará por el contenido de dicha variable.

En link debemos poner una función que recibe tres paraémtros:

  • scope es donde debemos definir las variables que queramos utilizar en la directiva, por ejemplo, definiendo scope.text="algun valor", ese text es el que luego se usará en el template en {{text}}.
  • elem es el elemento html que AngularJS va a reemplazar. Podríamos interrogar a este elemento para hacer cosas distintas en función del tipo de elemento que sea.
  • attrs son los atributos del elemento, en este caso, attrs.onwattribute es el texto "hello you" que pusimos en el html ownattribute="hello you"

Así que esta función sólo tiene que poner scope.text=attrs.ownattribute para guardar en la variable text el texto "hello you".

No es necesario hacer más, al visualizar la página, en vez de el div con el atributo, veremos el párrafo con el texto "hello you"


Usar una plantilla externa[editar]

Si el html que queremos poner en lugar de la directiva es grande, puede ser incómodo ponerlo en el template de nuestra directiva. En su lugar, podemos usar templateUrl para poner un fichero html que contenga dicha plantilla. Por ejemplo

myApp.directive ('templateExample', function(){
   return {
      templateUrl : 'template.html'
   };
});

donde template.html tiene el contenido html que queremos poner en lugar de nuestra directiva.


Directiva dentro de controlador[editar]

Si nuestra directiva está dentro de un trozo html manejado por un controlador de AngularJS, la directiva tendrá acceso al $scope del controlador. Podemos usar las variables en ese $scope para nuestra directiva. Como ejemplo, vamos a hacer un pequeño contador de clicks en un botón. El código html puede ser este

   <div data-ng-controller="myController">
      <button data-ng-click="add()">Add</button>
      <counter></counter>
   </div>

El controlador, que veremos más abajo, se llamará myController. Al pulsar el botón, se llamará a una función javascript add() que se encargará de incrementar un contador. Y counter será un tag de nuestra invención que una directiva se encargará de reemplazar por el valor del contador.

Obviamente se puede hacer más sencillo sin directiva, poniendo en vez de counter algo como <p>{{counter}}</p> siendo counter el contador que el controlador se encargará de definir en $scope.counter. Sin embargo, lo haremos con directivas para ver sus posibilidades.

El código javascript del controlador puede ser este

myApp.controller ("myController", function($scope){
   $scope.counter=0;
   $scope.add = function(){
      $scope.counter++;
   };
});

Se define $scope.counter con un valor inicial de cero y una función add() que incremente el contador y que será a la que llama el botón al hacer click en él.

La directiva quedaría así

myApp.directive ('counter', function(){
   return {
      restrict: 'E',
      template : '<p>{{counter}}</p>'
   };
});

El nombre counter es el del tag counter que nos inventamos y que queremos reemplazar. El párrafo directamente tiene {{counter}} que es la variable definida en $scope por el controlador.

Listo, con esto vermos como el párrafo incremente el valor mostrado cada vez que pulsamos el botón.


transclude[editar]

transclude true[editar]

Cuando reemplazamos un elemento html por otro usando una directiva, por defecto se reemplaza también el contenido interno de esa elemento html. Por ejemplo, si tenemos

<div transclude-true-example>This is the content of the div</div>

y hacemos una directiva para reemplazar este tag, también perderemos el texto interno (u otros tags html que hubiera dentro de este div). Para conservar el contenido, debemos poner transclude:true en nuestra directiva, así

myApp.directive ('transcludeTrueExample', function(){
   return {
      restrict : 'A',
      transclude : true,
      template : '<div><p ng-transclude></p></div>'
   };
});

y en el template con el contenido html nuevo, debemos poner el atributo ng-transclude en el elemento que queremos que contenga el contenido original de nuestro tag, es decir, el texto "This is the content of the div". Tras ejecutar esta directiva, el html resultante será

<div><p ng-transclude>his is the content of the div</p></div>

es decir, se ha añadido el contenido original al tag p que es el que contiene el atributo ng-transclude.

Es interesante saber que con transclude el contenido interno tiene acceso al $scope externo de la directiva. De esta forma, ese contenido interno puede ser una plantilla de AngularJS que usa variables del $scope externo a la directiva. Por ejemplo

<div ng-controller="myController">
   <div transclude-true-example>
       <p>{{controllerScopeVariable}}</p>
   </div>
</div>

funcionará como se espera, {{controllerScopeVariable}} será reemplazado por el valor que le de el controlador a la variable $scope.controllerScopeVariable y no es necesario que la directiva de valor a esta variable.

transclude element[editar]

Con tranclude:true se conserva el contenido interno del tag html que reemplazamos, pero no el tag en sí mismo. Si queremos que ese tag también se conserve, en vez de transclude:true debemos poner transclude:"element".

Sin embargo, con tranclude:"element" no podemos usar template ni templateUrl ni ng-transclude, debemos generar el nuevo contenido por medio de la función link que vimos anteriormente, pero con dos parámetros más. El código sería así

myApp.directive ('transcludeElementExample', function(){
   return {
      restrict : 'A',
      transclude : 'element',
      link : function (scope, element, attrs, controller, transcludeFn){
         element.after(transcludeFn());
         element.after("<p>Added Element</p>");
      }
   };
});

y supongamos que el html donde vamos a aplicarlo es este

<div transclude-element-example="" class="ng-scope">I'm Original Transclude element content</div>

En el caso de transclude:"element", el parámetro element es un comentario html que genera automáticamente AngularJS, como este

<!-- transcludeElementExample:  -->

y nos lo presenta como un objeto jQLite que tiene disponibles un subconjunto de funciones jQuery. Podemos usar esa funciones (por ejemplo, after()) para anñadir tags html a medida detrás del comentario. Ese será el contenido definitivo del html una vez que la directiva termine de ejecutarse.

En transcludeFn recibimos una función que nos devuelve el tag html original y su contenido interno. El código que hemos puesto, con dos llamadas a after(), generan el siguiente código html que el el que se verá finalmente en nuestro navegador

<!-- transcludeElementExample:  -->
<p>Added Element</p>
<div transclude-element-example="" class="ng-scope">I'm Original Transclude element content</div>

es decir, el comentario generado por AngularJS, el párrafo que hemos añadido y finalmente, la última línea con el contenido original del html. El orden del párrafo y del contenido original está al revés del orden de llamada a los after() puesto que after() añade inmediatamente detrás del comentario lo que le digamos. La última llamada a after() "empuja" a lo que ya hayamos añadido para poner el nuevo contenido justo detrás del comentario y antes de lo que ya se había añadido, dando como resultado este orden inverso.