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