Directivas en AngularJS


@gruizdevilla

@adesis

Meetup AngularJS Madrid
Puedes encontrar los ejemplos de la charla en:
 https://github.com/gonzaloruizdevilla/directivas

¿Qué es una directiva?


  • Es un marcador sobre un elemento de DOM,
    como un atributo, un elemento o una clase CSS.
  • Le indica al compilador de HTML de AngularJS
    ($compile) que enganche un comportamiento
    específico y/o transforme al elemento
    o a sus descendientes.

jQuery y AngularJS
(sobre manipulación de DOM en general)


Solo hay UN SITIO* donde es legítimo manipular directamente el DOM (con jQuery, zepto, directamente, etc.):

LAS DIRECTIVAS

Si lo haces en otro sitio, lo estás haciendo MAL.


* Y tal vez un servicio, pero solo para los que REALMENTE saben lo que están haciendo. Por ej: https://github.com/angular-ui/bootstrap/blob/master/src/modal/modal.js#L109

Un ejemplo sencillo: chispas


Mi primera directiva
 
    .directive('chispas', function () {
      return {
        restrict: 'E',
        link: function postLink(scope, element) {
           element.text('Mi primera directiva: chispas');
        }
      };
    });

Estilos de declaración: EACM


  • E: Elemento
  • A: Atributo
  • C: Clase CSS
  • M: Comentario

Las recomendadas son EA.
El valor por defecto es A.

La función "link"



Donde registramos eventos y manipulamos el DOM.


Se ejecuta DESPUÉS de haber clonado el DOM
(una vez por instancia de la directiva)

La plantilla se compila con el scope que recibe la función link



 

  .directive('reloj', function () {
    return {
      template: '<div>Hora: {{hora | date: \'h:mm:ss a\'}}</div>',
      restrict: 'E',
      link: function postLink(scope) {
        scope.hora = new Date();
      }
    };
  });
    

Hay que mantener AngularJS al corriente de los cambios de scope

Mediante $apply, $digest o en el contexto de una ejecución de AngularJS

  .directive('reloj2', function () {
    return {
      template: '<div>Hora: {{hora | date: \'h:mm:ss a\'}} <button>Actualizar</button>
</div>',
      restrict: 'E',
      link: function postLink(scope, element) {
        scope.hora = new Date();
        element.find('button').click(function (){
          scope.$apply(function() {
            scope.hora = new Date();
          });
        });
      }
    };
  });
    

Mediante la propiedad templateUrl separamos html y JavaScript


 
  .directive('reloj3', function () {
    return {
      templateUrl: 'templates/reloj3.html',
      restrict: 'E',
      link: function postLink(scope, element) {
        scope.hora = new Date();
        element.find('button').click(function (){
          scope.$apply(function() {
            scope.hora = new Date();
          });
        });
      }
    };
  });
  

Controladores: para no reinventar la rueda

Podemos extraer parte de la lógica a un controlador tradicional de AngularJS


  .controller('RuedaCtrl', function ($scope) {
    $scope.rotating = false;
    $scope.toggle = function () {
      $scope.rotating = !$scope.rotating;
    };
  })
  .directive('rueda', function () {
    return {
      templateUrl: 'templates/rueda.html',
      controller: 'RuedaCtrl',
      restrict: 'E',
      link: function postLink() {}
    };
  });
  

Comunicándose con el exterior



La directiva puede comunicar con el exterior:

  1. A través del scope
  2. Mediante atributos

La segunda opción suele ser mejor, aunque siempre hay excepciones.

Usando el Scope


Como ocurre en los controladores,
el scope se copia por defecto: 

  • los tipos básicos por valor
  • los objetos por referencia

Puede ser legítimo leer del scope del padre, pero manipular los objetos que hay en el es otro tema, pues podríamos violar el Principio de la Mínima Sorpresa.


Usando atributos.


Es la mejor manera.
Conviene empezar aislándose del scope padre


    .directive('miDirectiva', function () {
	  return {
	    //...
	    scope: {},
	    //...
	   }
	})
      
  

Al definir un scope:{}, la directiva tendrá un scope limpio, que no heredará las propiedades del scope del contenedor.

Tres formas de comunicación:



  .directive('miDirectiva', function () {
    return {
      //...
      scope: {
        "a": "@",
        "b1": "@b2",
        
        "c": "=",
        "d1": "=d2",
        
        "e": "&",
        "f1": "&f2"
      },
      //...
    },
  })

La primera forma:
@ o @attr

Pista: arroba es "at", como de atributo.
Se recibe el valor del atributo, es decir, si es de la forma:

   <midirectiva miatributo='algoPasaCon{{nombre}}'/>
    
y en el contenedor

  $scope.nombre='Mary'
      
entonces en la directiva, el scope local tendrá

  scope.miatributo='algoPasaConMary'
      

La segunda forma:
= o =attr

Pista: igual. El scope local con el padre.

 Crea un bind bidireccional entre la propiedad del scope local y la que se haya referenciado en el padre.

   <midirectiva miatributo='propiedadP'/>
    
y en el contenedor

  $scope.propiedadP = 'Yo soy tu padre'
      
entonces en la directiva, el scope local tendrá

  scope.miatributo == 'Yo soy tu padre' 
  //el valor de propiedadP en el scope del padre, siempre actualizado
      

La tercera forma:
& o &attr

Pista: & es como un puntero en C. Devuelve una función.

Devuelve una función  que ejecutará la expresión
del atributo contra el scope del padre. El scope se puede
extender en el momento de la ejecución
con valores adicionales.


Un ejemplo habitual es ng-click.

"Transclusion", el palabro más raro

(Lo que viene a ser <content> en webcomponents).
Hay que activar la transclusión:

  .directive('midirectiva', function () {
    return {
      //...
      transclude: true,
      //..
    };
  });

Y luego usar:

ng-transclude


Esta directiva marca el punto de inserción del DOM de la directiva padre más cercana que tenga activada la transclusión.

Interacción de formularios:
ngModel.ngModelController


Las ventajas de ngModel:

  • $setViewValue: para informar de un nuevo valor, reaccionando
    a un evento DOM, por ejemplo
  • $formatters y $parsers: adaptan un valor que viaja
    del scope a la vista y viceversa, validándolo si es necesario.
  • $setValidity: permite enganchar con el sistema
    común de validaciones del formulario
  • $validators y $asyncValidators permiten enganchar validaciones
    síncronas y asíncronas

Compile vs link


  • Compile: trabaja sobre el DOM original, que se clonará
    para usar en link, antes de procesarlo para enganchar
    los comportamientos
  • (Post)Link: combina las directivas con un scope y produce una vista
    viva. Los cambios en el scope se reflejan en la vista
     y las interacciones en la vista se reflejan en el modelo.




El uso de compile es raramente necesario.
(Mira la presentación de Angular+Famous para ver un ejemplo de compile para poder realizar tareas en los momentos prelink y postlink)

Directivas y Webcomponents (PolymerJS)



  • Abramos el debate :)
  • Angular 2 funciona mejor, que duda cabe

Es difícil predecir hacia donde evolucionarán,
pero será apasionante.



¡Gracias!



¿Preguntas?


¿Empezamos a picar código? :)