Directivas en AngularJS

{ codemotion} 

Madrid 2014

Lorenzo González  

lorenzo.profesor arroba gmail.com

@logongas 
http://cursoangularjs.es

¿Quien soy yo?

Un profesor del centro público

CIPFP Mislata en Valencia

¿Quieres que mis alumnos hagan las prácticas en tu empresa?

¡Ponte en contacto conmigo!

Contenidos

Estructura tus directivas

y

Las funciones de las directivas

Estructura tus directivas

Separa la funcionalidad de tu directiva en partes mas pequeñas

Ejemplo

ui-date tiene 2 funciones

  • Transformar el String del <input> a Date en el modelo y viceversa
  • Motrar un datepicker

Separarla en 2 directivas

date-model

date-picker

Tener una directiva por cada tipo de presentación

pero

En módulos distintos e intercambiables

Ejemplo

date-picker puede ser con

jQuery UI o Bootstrap

Crear una directiva "date-picker" en el módulo "jquery-ui"

Crear otra directiva "date-picker" en el módulo "bootstrap"

angular.
  module("jquery-ui").
    directive("date-picker",function() {
    });
angular.
  module("bootstrap").
    directive("date-picker",function() {
    });

Ahora puedes elegir una u otra con solo cambiar la dependencia del módulo

<input date-picker >
angular.module("app",["jquery-ui"]);

Cargamos el módulo de "jquery-ui"

Usamos la directiva "date-picker"

angular.module("app",["bootstrap"]);

Cargamos el módulo de "bootstrap"

Usamos la directiva "date-picker"

<input date-picker >

Oculta los detalles de tu implementación

Ejemplo

No uses el formato de fecha del datepicker de jQuery UI

<input date-picker="yy-mm-dd" >

yy : Año con 4 dígitos

mm : Mes con 2 dígitos

dd : Día con 2 dígitos

Usa el formato del filtro "date"

<input date-picker="yyyy-MM-dd" >

yyyy : Año con 4 dígitos

MM : Mes con 2 dígitos

dd : Día con 2 dígitos

Configura tu directiva globalmente con

un servicio

Ejemplo

Supongamos que date-picker permite especificar el formato de fecha

<input ng-model="persona.fechaNacimiento"    date-picker="yyyy-MM-dd" >


<input ng-model="movimiento.fechaCreacion"   date-picker="yyyy-MM-dd" >


<input ng-model="titulado.fechaInscripcion"  date-picker="yyyy-MM-dd" >


<input ng-model="cuenta.fechaActualizacion"  date-picker="yyyy-MM-dd" >


<input ng-model="cambio.fechaAplicacion"     date-picker="yyyy-MM-dd" >

Si siempre va a ser el mismo formato de fecha a lo largo de todo el proyecto, una solución es:

app.constant("datePickerConfig",{
    format:"MM-dd-yyyy"
});

Configúralo globalmente 

mediante un servicio

Se define el formato por defecto con una constante

app.config(function(datePickerConfig) {
    datePickerConfig.format("yyyy-MM-dd");
});

Ahora establecemos un formato

específico para toda

para

nuestra aplicación

Ya no es necesario poner

el formato cada vez

<input ng-model="persona.fechaNacimiento"   date-picker >


<input ng-model="movimiento.fechaCreacion"  date-picker >


<input ng-model="titulado.fechaInscripcion" date-picker >


<input ng-model="cuenta.fechaActualizacion" date-picker >


<input ng-model="cambio.fechaAplicacion"    date-picker >

Pero seguimos permitiendo

personalizaciones específicas

<input ng-model="persona.fechaNacimiento"   date-picker >


<input ng-model="movimiento.fechaCreacion"  date-picker >


<input ng-model="titulado.fechaInscripcion" date-picker="dd/MM/yyyy" >


<input ng-model="cuenta.fechaActualizacion" date-picker >


<input ng-model="cambio.fechaAplicacion"    date-picker >

Modifica el funcionamiento de una directiva desde otras directivas

<input  date-picker spanish-format >

Ejemplo

Al añadir la directiva "spanish-format" modificaremos el funcionamiento de "date-picker"

Pero "date-picker" no sabe nada de

la nueva directiva

app.directive("spanishFormat", [function () {
    return {
        compile: function (tElement, tAttrs) {
            tAttrs.datePicker="dd/mm/yyyy";
        }
    };

}]);

Creamos la nueva directiva y en "compile" modificamos el valor del atributo

¿Y si queremos usar en nuestra nueva directiva algún valor del "scope"?

app.directive("spanishFormat", [function () {
    return {
        compile: function (tElement, tAttrs) {
            return {
                pre: function (scope, iElement, iAttrs, controller, transcludeFn) {
                    iAttrs.datePicker="dd/mm/yyyy";
                },
                post: function (scope, iElement, iAttrs, controller, transcludeFn) {
                }
            };
        }
    };

}]);

Usamos "pre-link" para modificar el valor del atributo

Ahora podríamos hacer la asignación de "iAttrs.datePicker" usando algún valor del Scope o un atributo interpolado

Usa la función "template" para mantener 2 versiones de la directiva

<div class="row">
	<div class="span12">
		<ng-transclude></ng-transclude>
	</div>
</div>
<div class="row">
	<div class="col-md-12">
		<ng-transclude></ng-transclude>
	</div>
</div>

Ejemplo

Versión para bootstrap 2

Versión para bootstrap 3

Directiva compatible con

bootstrap 2 y bootstrap 3

app.constant("bootstrapVersion",2);

Usamos una constante con la versión de bootstrap

Usa un servicio para indicar de alguna forma la versión de bootstrap

app.directive("fila", ['bootstrapVersion',function (bootstrapVersion) {
return {
  restrict: "E",
  transclude:true,     
  template: function (tElement, tAttrs) {
    var css="";

    if (bootstrapVersion===2) {
      css="span12";
    } else {
      css="col-md-12";
    }

    tpl='<div class="row"><div class="' + css + '"><ng-transclude></ng-transclude></div></div>';

    return tpl;
  }
};
}]);

El template de la directiva cambia con la versión de bootstrap

NOTA: Lo mismo se hace con templateUrl

Permite personalizar tu directiva con otras directivas que actuarán como puntos de expansión

Ejemplo

Bootstrap tiene un componente de alertas que incluye el botón de cerrar

<div class="alert alert-warning alert-dismissible" role="alert">
  <button type="button" class="close" data-dismiss="alert">
      <span aria-hidden="true">×</span>
  </button>
  <strong>Aviso</strong>Directiva se personaliza
</div>

Pero queremos en la aplicación personalizar el botón de cerrar

<div class="alert alert-warning alert-dismissible" role="alert">
  <button type="button"  data-dismiss="alert" style="float:right">Cerrar</button>
  <strong>Aviso</strong>Esta directiva se personaliza
</div>
<div class="alert alert-warning alert-dismissible" role="alert">
  <button type="button" class="close" data-dismiss="alert">
      <span aria-hidden="true">×</span>
  </button>
  <strong>{{tipo}}</strong>{{mensaje}}
</div>

Creamos una directiva llamada "alert"

template de la directiva

<alert tipo="Aviso" mensaje="Directiva se personaliza" ></alert>

Usando la directiva

<div class="alert alert-warning alert-dismissible" role="alert">
  <close-alert-button></close-alert-button>
  <strong>{{tipo}}</strong>{{mensaje}}
</div>

La directiva permite

la personalización del el botón

mediante

una nueva directiva llamada

"closeAlertButton"

app.directive("closeAlertButtonn", [function () {
  return {
    restrict: "E",
    template: '<button type="button"  data-dismiss="alert" style="float:right">Cerrar</button>'
  };
}]);

En la aplicación creamos la nueva directiva "closeAlertButton" para personalizar el botón

¿Y si no queremos personalizar el botón?

app.directive("defaultCloseAlertButton", [function () {
  return {
    restrict: "E",
    template: '<button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">×</span></button>'
  };
}]);

Se crea la directiva "defaultCloseAlertButton"

que se usa para cuando no existe "closeAlertButton"

El formato del botón es el estándar de bootstrap

¿Como comprobamos si existe o no la directiva del usuario "closeAlertButton"?

var existe=$injector.has('closeAlertButtonDirective');

El método "has" de servicio "$injector" nos permite averiguarlo

NOTA: Al método "has" se le pasa el nombre de la directiva mas el texto "Directive"

template: function () {
    var button;
    if ($injector.has('closeAlertButtonDirective') === true) {
        button = "<close-alert-button></close-alert-button>"
    } else {
        button = "<default-close-alert-button></default-close-alert-button>"
    }

    return '<div class="alert alert-warning alert-dismissible" role="alert">' + button + ' <strong>{{tipo}}</strong> {{mensaje}}</div>'
}

Finalmente la función template queda de la siguiente forma

El template es distinto dependiendo de si existe o no la directiva "closeAlertButton"

Las funciones de las directivas

template

compile

pre-link

post-link

app.directive("directiveName", [function () {
    var directiveDefinitionObject = {
        template:function (tElement, tAttrs) {
            return "";
        },
        compile: function (tElement, tAttrs) {
            return {
                pre: function (scope, iElement, iAttrs, controller, transcludeFn) {
                },
                post: function (scope, iElement, iAttrs, controller, transcludeFn) {
                }
            };
        }
    };

    return directiveDefinitionObject;
}]);

Estructura básica de una directiva

  • template
  • compile
  • pre-link
  • post-link

template

function (tElement, tAttrs) {

}

La función "template"

  • tElement: Es el elemento HTML en la que se encuentra la directiva.
    • Es similar a un Elemento de jQuery (jqLite)
  • tAttrs: Es un objeto en el que cada propiedad es cada una de los atributos.
    • Sus nombres están normalizados.
    • Tiene métodos como "$set" y "$observe"

Usando tElement

<mi-directiva
    data-nombre="Lorenzo"
    x-apellido="Gonzalez"
    mi-ciudad="Valencia"
    mi_instituto="Mislata"
></elemento>

tElement.attr("data-nombre")       ==>           "Lorenzo"

tElement.attr("x-apellido")             ==>           "Gonzalez"

tElement.attr("mi-ciudad")             ==>           "Valencia"

tElement.attr("mi_instituto")          ==>           "Mislata"

Los nombres NO están normalizados

Usando tAttrs

<mi-directiva
    data-nombre="Lorenzo"
    x-apellido="Gonzalez"
    mi-ciudad="Valencia"
    mi_instituto="Mislata"
></elemento>

tAttrs.nombre                              ==>           "Lorenzo"

tAttrs.apellido                              ==>           "Gonzalez"

tAttrs.miCiudad                           ==>           "Valencia"

tAttrs.miInstituto                         ==>           "Mislata"

 

Los nombres SI están normalizados

Desde la función "template" no se tiene acceso al "scope" de la directiva

<elemento mi-directiva="{{nombre}}" ></elemento>

tElement.attr("mi-directiva")       ====>           "{{nombre}}"

tAttrs.miDirectiva                          ====>           "{{nombre}}"

Los valores no están interpolados

Consideraciones

al usar

template

  • Solo puede haber  una única  directiva con template en el elemento
  • El contenido del tag no se muestra. Para hacerlo es necesario el uso de  transclude .

2 directivas con template

app.directive("titulo", [function () {
    return {
        template: "<h1>Esto es el titulo</h1>"
    };
}]);
app.directive("subtitulo", [function () {
    return {
        template:  "<h2>Esto es el subtitulo</h2>"
    };
}]);
<!-- HTML Original -->
<div titulo>
</div>

<!-- RESULTADO GENERADO -->
<div titulo>
    <h1>Esto es el titulo</h1>
</div>
<!-- HTML Original -->
<div subtitulo>
</div>

<!-- RESULTADO GENERADO -->
<div subtitulo>
    <h2>Esto es el subtitulo</h2>
</div>
<!-- HTML Original -->
<div titulo subtitulo>
</div>

Error: [$compile:multidir] Multiple directives [subtitulo, titulo] asking for template on: <div titulo="" subtitulo="">

Resultados

Recomendación

Que las directivas con "template" únicamente

se puedan usar como elementos y de esa forma

que solo pueda haber una

app.directive("titulo", [function () {
    return {
        restrict: "E",
        template: "<h1>Esto es el titulo</h1>"
    };
}]);
<titulo></titulo>

 El contenido no

se muestra si hay template

app.directive("sinTemplate", [function () {
    return {
        
    };
}]);
app.directive("conTemplate", [function () {
    return {
        template:  "<h1>Esto es la plantilla</h1>"
    };
}]);

Resultados

<!-- HTML Original -->
<div sin-template>
    <h1>Esto es el contenido</h1>
</div>

<!-- RESULTADO GENERADO -->
<div sin-template>
    <h1>Esto es el contenido</h1>
</div>
<!-- HTML Original -->
<div con-template>
    <h2>Esto es el contenido</h2>
</div>

<!-- RESULTADO GENERADO -->
<div con-template>
    <h1>Esto es la plantilla</h1>
</div>

transclude

La directiva ngTransclude nos permite incluir el contenido del elemento en la plantilla de la directiva.

PERO: El scope que se usa no es el de la directiva

app.directive("titulo", [function () {
    return {
        restrict: "E",
        transclude:true,
        template:  "<div><h1>Esto es la plantilla</h1><ng-transclude></ng-transclude></div>"
    };
}]);
app.directive("subtitulo", [function () {
    return {
        restrict: "E",
        template:  "<h2>Esto es la directiva subtitulo</h2>"
    };
}]);
<!-- HTML Original -->
<titulo>
    <subtitulo></subtitulo>
    <h2>Esto es el contenido</h2>
</titulo>


<!-- RESULTADO GENERADO -->
<div>
    <h1>Esto es la plantilla</h1>
    <h2>Esto es la directiva subtitulo</h2>
    <h2>Esto es el contenido</h2>
</div>

Resultado

El contenido puede tener a su vez nuevas directivas, como por ejemplo "subtitulo",

las cuales tambien se compilan

compile

Para mejorar el rendimiento es

la función donde hacer operaciones que comparten todas las instancias

Pero dicho mas llanamente

Es donde hacer cosas sin acceso al scope y sin usar listeners del DOM

function (tElement, tAttrs) {
    return {
        pre: function (scope, iElement, iAttrs, controller, transcludeFn) {
        },
        post: function (scope, iElement, iAttrs, controller, transcludeFn) {
        }
    };
}

La función "compile"

  • Retorna un objeto con las dos funciones de "pre-link" y "post-link"
  • Los valores al igual que en la función template siguen sin estar interpolados

Cosas permitidas / prohibidas

  • Permitidas
    • Se pueden añadir nuevos elementos a "tElement"
    • Los nuevos elementos pueden contener a su vez nuevas directivas
  • Prohibidas
    • No se pueden añadir nuevas directivas al propio elemento
    • No se pueden añadir listeners ,etc. ya que se pierden al clonar el elemento.

Añadir nuevos elementos

app.directive("entreSemana", [function () {
    return {
        restrict: "A",
        priority:2,
        compile: function (tElement, tAttrs) {
            tElement.append("<option value='1'>Lunes</option>");
            tElement.append("<option value='2'>Martes</option>");
            tElement.append("<option value='3'>Miercoles</option>");
            tElement.append("<option value='4'>Jueves</option>");
            tElement.append("<option value='5'>Viernes</option>");
        }
    };

}]);

app.directive("finSemana", [function () {
    return {
        restrict: "A",
        priority:1,
        compile: function (tElement, tAttrs) {
            tElement.append("<option value='6'>Sabado</option>");
            tElement.append("<option value='7'>Domingo</option>");
        }
    };
}]);

Resultado

<!-- HTML Original -->
Dia de la semana de inicio de codemotion Madrid:
<select ng-model="codemotion.madrid" entre-semana fin-semana  ></select>



<!-- RESULTADO GENERADO-->
Dia de la semana de inicio de codemotion Madrid:
<select ng-model="codemotion.madrid" entre-semana fin-semana  >
   <option value='1'>Lunes</option>
   <option value='2'>Martes</option>
   <option value='3'>Miercoles</option>
   <option value='4'>Jueves</option>
   <option value='5'>Viernes</option>
   <option value='6'>Sabado</option>
   <option value='7'>Domingo</option>
</select>

Nuevas Directivas en los

nuevos elementos

app.directive("finSemana", [function () {
  return {
    restrict: "A",
    priority:1,
    compile: function (tElement, tAttrs) {
      tElement.append("<option value='6'>Sabado</option>");
      tElement.append("<option ng-style='{backgroundColor:\"red\"}' value='7'>Domingo</option>");
    }
  };
}]);

Se añade la directiva ng-style para ver si se aplica

Resultado

<!-- HTML Original -->
Dia de la semana de inicio de codemotion Madrid:
<select ng-model="codemotion.madrid" entre-semana fin-semana  ></select>



<!-- RESULTADO GENERADO-->
Dia de la semana de inicio de codemotion Madrid:
<select ng-model="codemotion.madrid" entre-semana fin-semana  >
   <option value='1'>Lunes</option>
   <option value='2'>Martes</option>
   <option value='3'>Miercoles</option>
   <option value='4'>Jueves</option>
   <option value='5'>Viernes</option>
   <option value='6'>Sabado</option>
   <option value='7' style="background-color: red;">Domingo</option>
</select>

Se ha procesado la directiva ng-style en el nuevo elemento

No permite añadir nuevas directivas al propio elemento

app.directive("requerido", [function () {
    return {
        restrict: "A",
        compile: function (tElement, tAttrs) {
          tAttrs.$set("ngRequired","true");          
        }
    };
}]);
  • La forma de modificar los valores de las directivas es con "tAttrs.$set()" y no con tElement.attr(), ésto permite que otras directivas detecten el cambio con "tAttrs.$observe"
  • Si que funcionaría si previamente existe la directiva

Resultado

<select 
    ng-model="codemotion.madrid" 
    name="madrid" 
    entre-semana 
    fin-semana 
    requerido 
></select>

Aplicamos la directiva "requerido" pero nunca será el campo requerido ya que no se pueden añadir nuevas directivas

Resultado

<select 
    ng-model="codemotion.madrid" 
    name="madrid" 
    entre-semana 
    fin-semana 
    requerido 
    ng-required="false"
></select>

Aplicamos la directiva "requerido" y el campo SI será requerido ya previamente existe la directiva "ng-required"

post-link

Es la función donde

hacer operaciones 

especificas de cada instancia

Pero dicho mas llanamente

Es donde todos incluimos la funcionalidad de nuestras directivas y con acceso al scope

function (scope, iElement, iAttrs, controller, transcludeFn) {
}

La función "post-link"

  • scope: El scope de la directiva
  • iElement: Los atributos siguen sin estar interpolados
  • iAttrs: Los datos por fin YA están interpolados

Empieza por "i" de instance ya que

es la "instancia" del elemento "tElement"

Desde la función "post-link" si se tiene acceso al "scope" de la directiva

<elemento mi-directiva="{{nombre}}" ></elemento>

iAttrs interpola pero iElement no lo hace

iElement.attr("mi-directiva")       ====>         "{{nombre}}"

iAttrs.miDirectiva                          ====>         "Hola Mundo"

$scope.nombre="Hola Mundo"

Cosas permitidas/prohibidas

  • Permitidas
    • Añadir nuevos elementos
    • Añadir listeners
  • Prohibidas
    • Añadir nuevas directivas                            
app.directive("finSemana", [function () {
 return {
   restrict: "A",
   priority: 1,
   compile: function (tElement, tAttrs) {
     return {
       pre: function (scope, iElement, iAttrs, controller, transcludeFn) {

       },
       post: function (scope, iElement, iAttrs, controller, transcludeFn) {
         iElement.append("<option value='6'>Sabado</option>");
         iElement.append("<option ng-style='{backgroundColor:\"red\"}' value='7'>Domingo</option>");
       }
     }

   }
 };
}]);

No se aplican nuevas directivas

Se añade ng-style para ver si se aplica

<!-- HTML Original -->
Dia de la semana de inicio de codemotion Madrid:
<select ng-model="codemotion.madrid" entre-semana fin-semana  ></select>



<!-- RESULTADO GENERADO-->
Dia de la semana de inicio de codemotion Madrid:
<select ng-model="codemotion.madrid" entre-semana fin-semana  >
   <option value='1'>Lunes</option>
   <option value='2'>Martes</option>
   <option value='3'>Miercoles</option>
   <option value='4'>Jueves</option>
   <option value='5'>Viernes</option>
   <option value='6'>Sabado</option>
   <option value='7'>Domingo</option>
</select>

Resultado

No se ha procesado la directiva ng-style

post-link

vs

compile

Cada una permite/prohibe cosas distintas así que no hay porque confundirlas

¿Porque AngularJS ha separado las 2 funciones?

La verdadera pregunta es

¡Por rendimiento!

Para directivas como

ng-repeat

La función compile solo se ejecuta una vez

y

las funciones de link se ejecutan para cada elemento de la lista

Ejemplo

scope.codemotion={
    madrid:1,
    berlin:1,
    telAviv:1,
    milan:1
}
<div ng-repeat="(campo,valor) in codemotion">
    Dia de la semana de inicio de codemotion {{campo}}
    <select ng-model="codemotion[campo]" entre-semana fin-semana  >
    </select>
</div>
<select ng-model="codemotion[campo]" entre-semana fin-semana  >
   <option value='1'>Lunes</option>
   <option value='2'>Martes</option>
   <option value='3'>Miercoles</option>
   <option value='4'>Jueves</option>
   <option value='5'>Viernes</option>
   <option value='6'>Sabado</option>
   <option value='7'>Domingo</option>
</select>

Resultado

Se genera este "tag" una única vez y simplemente se clona para cada elemento de la lista

 

Se mejora el rendimiento 

post-link

“Not safe to do DOM transformation since the compiler linking function will fail to locate the correct elements for linking.”

Según la documentación de

AngularJS

¿Y si modifico únicamente los valores de iAttrs?

Ejemplo

$scope.metadata={};

$scope.metadata.ciudad = {
    isArrayObjects: true,
    values: [
        {toString: "Madrid", pais: "España"},
        {toString: "Berlin", pais: "Alemania"},
        {toString: "Tel Aviv", pais: "Israel"},
        {toString: "Milan", pais: "Italia"},                
    ]
};

$scope.metadata.diaSemana = {
    isArrayObjects: false,
    values: [
        "Lunes","Martes","Miercoles","Jueves","Sabado","Domingo"
    ]
};

El objeto "metadata" para todos los <select>

$scope.ciudad;
$scope.diaSemana;

Uso de ng-options

<select 
    ng-model="ciudad" 
    ng-options="value as value.toString for value in metadata['ciudad'].values"  
></select>

<select 
    ng-model="diaSemana" 
    ng-options="value for value in metadata['diaSemana'].values"  
></select>

Demasiado código repetido en ng-options a lo largo del proyecto para user el objeto "metadata"

¿Solución?

Hacer una directiva usando "pre-link"

Directiva "ca-options"

pre: function (scope, iElement, iAttrs, controller, transcludeFn) {
  var model=iAttrs.ngModel;
  var isArrayObjects = scope.$eval(" metadata['" + model + "'].isArrayObjects");
  
  if (isArrayObjects === true) {
    iAttrs.ngOptions = "value as value.toString for value in metadata['" + model + "'].values";
  } else {
    iAttrs.ngOptions = "value for value in metadata['" + model + "'].values";
  }
}

La directiva "ca-options" solo tiene función pre-link

<select ng-model="ciudad" ca-options ></select>


<select ng-model="diaSemana" ca-options ></select>

Queremos eliminar el código repetitivo

Pregunta

¿Porque no hace falta que exista previamente la directiva "ngOptions"?

Ya que desde ninguna función se pueden añadir directivas al propio elemento

"ngOptions" no es la directiva que genera cada elemento "<option>"

sino

que es la directiva "select" la que los añade

Porque





  Añadir elementos Añadir listeners Acceso al scope Modificar attrs Cualquier modificación posterior Nuevas directivas a la directiva
template - - - - - -
compile Si y directivas - - Si - -
pre-link - Si Si Si - -
post-link Si Si Si - Si -

Resumen

Y nos dejamos ....

  • Recompilar la propia directiva para añadir nuevas directivas desde post-link
$compile(iElement)(scope)
  • Decorar directivas para modificar directivas de terceros
$provide.decorator('miDirectivaDirective', function($delegate) {
});
  • Los controladores y la función transclude de post-link
post: function (scope, iElement, iAttrs, controller, transcludeFn) {
}

Gracias a todos 
por vuestra atención

¿Preguntas?

lorenzo.profesor arroba gmail.com

 
https://github.com/logongas/codemotion2014

Directivas en angularjs. Codemotion 2014

By Lorenzo Gonzalez Gascón

Directivas en angularjs. Codemotion 2014

  • 12,656