9. Custom directives
A lo largo de los distintos capítulos hemos visto que casi todo lo que utilizamos en nuestras plantillas HTML es una directiva. Las directivas son los elementos que nos permiten extender el DOM, generando componentes con el comportamiento que nosotros queramos.
Aunque AngularJS trae un conjunto de directivas muy potente en su core, en alguna ocasion querremos crear elementos con cierta funcionalidad propia y reusable. En este capítulo veremos cómo podemos hacerlo a través de la creación de nuevas directivas.
9.1. Definiendo nuevas directivas
Podemos definir nuevas directivas gracias al método directive()
, que nos proporciona un module
de AngularJS. La definición es similar a como ya hemos visto para controladores, filtros o servicios. El nombre de la directiva debe definirse en camelCase, y la función debe devolver un objeto, conocido como Directive Definition Object (DDO).
En nuestro código javascript, definiremos nuestra directiva en camelCase (ej: |
9.2. Nuestra primera directiva
Vamos a crear una directiva muy sencilla, cuya etiqueta será login-button
. En ella, haciendo clic en el botón de login, seríamos redirigidos a la página de login de nuestra aplicación:
1
2
3 <div ng-app="directives1">
<login-button></login-button>
</div>
Al introducir esta directiva en nuestro módulo, AngularJS compilará el HTML e invocará esta directiva. El DDO de la directiva es:
1
2
3
4
5
6
7
8 angular
.module('directives1', [])
.directive('loginButton', function(){
return {
restrict : 'E',
template : '<a class="btn btn-primary btn-lg" ng-href="#/login"><span class="glyphicon glyphicon-log-in"></span> Acceder</a>'
}
});
Inspeccionando el código de la aplicación[1], vemos que el contenido de la etiqueta se ha reemplazado por el atributo template
de nuestro DDO.
1
2
3
4
5
6
7
8
9
10 <body>
<div ng-app="directives1" class="ng-scope">
<login-button>
<a class="btn btn-primary btn-lg" ng-href="#/login" href="#/login">
<span class="glyphicon glyphicon-log-in"></span>
Acceder
</a>
</login-button>
</div>
</body>
9.3. El atributo restrict
Hemos visto que la directiva loginButton
consiste en una etiqueta HTML. Esto se debe al valor del atributo restrict
. Éste acepta los siguientes valores:
-
E
para elementos:<login-button></login-button>
. -
A
para atributos:<span login-button></span>
. -
C
para clases:<div class="login-button"></div>
. -
M
para comentarios:<!-- directive: login-button -→
.
Pero una directiva no tiene por qué ser de un tipo únicamente. Podemos definir varios tipos el atributo restrict
. En el siguiente ejemplo[2], nuestra directiva será capaz de funcionar como atributo, elemento o clase.
1 restrict : 'EAC'
1
2
3
4
5 <login-button></login-button>
<div login-button></div>
<div class="login-button"></div>
Aunque disponemos de estas cuatro maneras de crear directivas, la declaración que mejor compatibilidad tienen con todos los navegadores es Como os habréis imaginado, cuando hablamos de problemas con navegadores estamos haciendo una referencia indirecta a Internet Explorer. Aquí hay cierta información de los problemas de compatibilidad de las directivas con algunas versiones de Internet explorer y cómo solventarlas. |
9.4. Paso de datos a la directiva
Nuestra directiva loginButton
está muy bien. Sin embargo, no todas las aplicaciones tienen el acceso en la ruta #/login
. Además, si quisiéramos internacionalizar nuestra aplicación, tampoco sería recomendable poner la palabra Acceso allí donde la hemos puesto.
Quizá sería mejor refactorizar nuestra directiva para que soporte estas posibilidades, y pasarle estos datos como atributos de la siguiente manera:
1
2
3
4 <div login-button
login-path="#/login"
login-text="Área de usuario">
</div>
Aunque podríamos haber cogido estos datos directamente del $scope
o $rootScope
, esto puede acarrear problemas si el dato se elimina. Para solucionar esto, Angular nos permite crear un scope hijo, o lo que se conoce como un isolate scope. Este segundo está completamente separado del scope padre en el DOM, y se crea de una manera sencilla: simplemente definiremos un atributo scope
en nuestro DDO[3]:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 angular
.module('directives3', [])
.directive('loginButton', function(){
return {
restrict : 'A',
scope: {
loginPath : '@',
loginText : '@'
},
template : '<a class="btn btn-primary btn-lg" ng-href="{{loginPath}}"><span class="glyphicon glyphicon-log-in"></span> {{loginText}}</a>'
}
});
Vemos cómo hemos introducido un objeto scope
, y además hemos modificado el template
para hacer binding con las variables definidas en él.
Vemos que la convención camelCase se mantiene también para los atributos del scope. |
La convención de nombrado por defectoes que el atributo y la propiedad del scope
se llamen igual. En algunas ocasiones podríamos querer que la variable del scope tuviese un nombre distinto. Para ello especificaríamos los nombres de la siguiente manera:
1
2
3
4 scope : {
loginPath : '@uri',
loginText : '@customText'
}
y, en nuestra plantilla:
1
2
3
4 <div login-button
uri="#/login"
custom-text="Área de usuario">
</div>
Aquí, estamos diciendo que se establezca el valor de la variable loginPath
del isolate scope con lo que pasamos como atributo uri
.
Ahora, imaginemos que no queremos tener la URL hardcodeada, ya que tenemos un servicio en nuestra aplicación que nos proporciona todas las URLs de la misma. Nuestro controlador pasa dicho servicio a la vista:
1
2
3 .controller('MainCtrl', function($scope, AppUrls){
$scope.urls = AppUrls;
})
y la plantilla pasa la url como parámetro de la directiva:
1
2
3
4 <div login-button
login-path="urls.login"
login-text="Área de usuario">
</div>
Tras este cambio, si observamos el fuente de nuestra aplicación, veremos que no tenemos el resultado que esperábamos, y en lugar de un esperado a href="#/login"
, nuestro enlace es a href="urls.login"
.
Para obtener el resultado esperado, tenemos que hacer una ligera modificación en el scope
de nuestra directiva:
1
2
3
4 scope : {
loginPath : '=',
loginText : '@'
}
Podemos ver un ejemplo completo de este funcionamiento en http://codepen.io/alexsuch/pen/mdAFa.
Vemos que hemos cambiado la primera @
por un =
. Este símbolo determina la estrategia de binding:
-
@
lee el valor de un atributo. El valor final siempre será una cadena. Al leerse tras la evaluación del DOM, también podemos usarlo de la formatitle="{{title}}"
, el valor del atributo es el que hayamos establecido en el$scope
para la variabletitle
. -
=
nos permite realizar el two-way data binding en nuestra directiva, bindando la propiedad delscope
de la directiva a una propiedad delscope
padre. Cuando utilicemos=
, usaremos el nombre de la propiedad sin las llaves{{}}
. -
&
permite realizar referencias a funciones en elscope
padre.
9.5. El atributo transclude
Vamos a realizar una modificación adicional en nuestro botón, haciendo que se parezca más a una etiqueta HTML. En un enlace normal introduciríamos el texto detro de la etiqueta, en lugar de como un atributo, ¿no?. Vamos a hacer lo mismo, con nuestra directiva, para que tenga la forma:
1
2
3 <div login-button login-path="urls.login">
Área de usuario
</div>
Para ello, haremos uso de la transclusión. Ésta consiste en la inclusión de un documento (o parte de un documento) dentro de otro documento. Realmente es algo que ya hemos visto en la parte de routing con las directivas ngView
o uiView
, donde introducíamos unas plantillas dentro de otras.
Es justo lo que vamos a hacer en nuestra directiva: queremos que el fragmento Área de usuario se introduzca en una zona concreta de la plantilla.
Para permitir la transclusión en AngularJS, debemos hacer dos cosas:
En primer lugar, nuestra directiva tiene que permitir la transclusión. Para ello, añadir el atributo transclusion
al DDO.
Además, tenemos que indicar dónde se va a realizar la transclusión. AngularJS nos proporciona la directiva ngTransclude
, que introduciremos en la parte de nuestra plantilla dode queramos realizarla.
Así, nuestro DDO queda de la siguiente manera:
1
2
3
4
5
6
7
8
9
10
11 {
restrict : 'A',
scope : {
loginPath : '='
},
transclude : true,
template : '<a class="btn btn-primary btn-lg" ng-href="{{loginPath}}"><span class="glyphicon glyphicon-log-in"></span> <span ng-transclude></span></a>'
}
Ahora, ya no es necesario el atributo loginText
, y por eso se ha eliminado. Se ha substituído por un <span ng-transclude></span>
en la plantilla.
Podemos ver este ejemplo funcionando en http://codepen.io/alexsuch/pen/DxjEu.
9.6. Un vistazo a todas las propiedades de una directiva
Ahora que tenemos un varios ejemplos de cómo crear una directiva, veamos cuáles son todas las propiedades que podemos definir en una directiva:
9.6.1. restrict
Como hemos visto, permite determinar cómo puede usarse una directiva:
-
`A`tributo
-
`E`lemento
-
`C`lase
-
Co`M`entario
9.6.2. scope
Lo utlizamos para crear un scope hijo (scope : true
) o un isolate scope (scope : {}
).
9.6.3. template
Define el contenido de la directiva. Puede incluir código HTML, data binding expressions y otras directivas.
9.6.4. templateUrl
Al igual que en el routing, podemos definir un path para la plantilla de nuestra directiva.
Definir un templateUrl
puede ser útil en componentes muy específicos para una aplicación. Sin embargo, cuando desarrollamos componentes reutilizables, lo mejor es definir la plantilla dentro de la directiva como un atributo template
.
9.6.5. controller
Nos permite definir un controlador, que se asociará a la plantilla de la directiva de igual manera que hacíamos en el routing.
Recibe como parámetros cuatro argumentos:
1
2
3
4
5
6
7
8
9
10 angular
.module('exampleModule', [])
.directive('exampleDirective', function(){
return {
restrict : 'A',
controller : function($scope, $element, $attrs, $transclude) {
//Código de nuestro controlador
}
};
})
$scope
Hace referencia al objeto scope
asociado a la directiva.
$element
Hace referencia al objeto jqLite (similiar a un objeto jQuery) de la directiva.
=====$attrs
Hace referencia a los atributos del elemento. Por ejemplo para un elemento
1 <div id="myId" class="blue-bordered"></div>
el objeto $attrs
tendría el valor:
1
2
3
4 {
id : 'myId',
class : 'blue-bordered'
}
$transclude
Esta función crea un clon del elemento a transcluir, permitiéndonos manipular el DOM.
En teoría, aunque podemos manipular el DOM desde un controlador, el lugar adecuado donde hacerlo es en el código de una directiva.
El siguiente ejemplo crea un enlace vacío con el texto a transcluir a continuación de nuestro elemento:
1
2
3
4
5
6
7
8
9
10 controller : function($scope, $element, $attrs,$transclude) {
$transclude(function(clone){
console.log('clone is', clone)
console.log('clone txt is', clone.text())
var a = angular.element('<span>');
a.text(clone.text());
$element.append(a);
});
},
9.6.6. transclude
Nos permite realizar la transclusión del bloque HTML que queramos, combinando su uso con la directiva ngTransclude
.
9.6.7. replace
Si inspeccionamos el código de nuestra aplicación, veremos que cuando introducimos una directiva se crea un elemento padre con la definición de la directiva, y dentro de él se desarrolla la directiva.
Cuando se establece con el valor true
, reemplazamos el elemento padre por el valor de la directiva, en lugar de introducirlo como hijo.
Es decir, pasamos de
1
2
3
4
5
6
7
8
9
10
11
12 <div ng-app="directives5" ng-controller="MainCtrl" class="ng-scope">
<div login-button="" login-path="urls.login" class="ng-isolate-scope">
<a class="btn btn-primary btn-lg" ng-href="#/login" href="#/login">
<span class="glyphicon glyphicon-log-in"></span>
<span ng-transclude="">
<span class="ng-scope">
Área de usuario
</span>
</span>
</a>
</div>
</div>
a
1
2
3
4
5
6
7
8
9
10 <div ng-app="directives5" ng-controller="MainCtrl" class="ng-scope">
<a class="btn btn-primary btn-lg" ng-href="#/login" href="#/login">
<span class="glyphicon glyphicon-log-in"></span>
<span ng-transclude="">
<span class="ng-scope">
Área de usuario
</span>
</span>
</a>
</div>
Vemos que, en el segundo caso, ha desaparecido el bloque div login-button
.
9.6.8. link
El template
o templateUrl
no tiene utilidad hasta que se compila contra un scope
. Hemos visto que una directiva no tiene un scope
por defecto, y utilizará el del padre a no ser que se lo indiquemos.
Para hacer uso del scope, utilizaremos la función link
, que recibe tres argumentos:
-
scope
: elscope
que se pasa a la directiva, pudiendo ser propio o el del padre. -
element
: un elemento jQLite (un subset de jQuery) donde se aplica nuestra directiva. Si tenemos jQuery instalado en nuestra aplicación, será un elemento jQuery en lugar de un lQLite. -
attrs
: un objeto que contiene todos los atributos del elemento donde aplicamos nuestra directiva, de igual manera que vimos con el controlador.
El uso principal de la función link
es para asociar listeners a elementos del DOM, observar cambios en propiedades del modelo, y validación de elementos.
9.6.9. require
La opción require
puede ser una cadena array de cadenas, correspondoentes a nombres de directivas. Al usarla, se asume que esas directivas indicadas en el array han sido previamente aplicadas en el propio elemento, o en su elemento padre (si se ha marcado con un ^
). Se utiliza para inyectar el controlador de la directiva requerida como cuarto argumento de la función link
de nuestra directiva.
Esta cadena o conjunto de cadenas se corresponde con el nombre de las directivas cuyo controlador queremos utilizar.
1
2
3
4 // ...
restrict : 'EA',
require : 'ngModel' // el elemento debe tener la directiva ngModel para poder utilizar su controlador
// ...
1
2
3
4 // ...
restrict : 'EA',
require : '^ngModel' // el elemento o su padre, deben tener la directiva ngModel
// ...
9.6.10. compile
Utilizaremos la función compile
para realizar transformaciones en el DOM antes de que se ejecute la función link
. Esta función recibe dos elementos:
-
element
: elemento sobre el que se aplicará la directiva -
attrs
: listado de atributos de la directiva.
La función compile
no tiene acceso al scope
, y debe devolver una función link
.
El esqueleto de una directiva cuando utilizamos una función compile
es:
1
2
3
4
5
6
7
8
9
10
11
12 angular
.module('compileSkel', [])
.directive('sample', function(){
return {
compile : function(element, attrs) {
//realizar transformaciones sobre el DOM
return function(scope, element, attrs){
//función link normal y corriente
};
}
};
});
En http://codepen.io/alexsuch/pen/LpcsK tenemos un ejemplo que usa la función compile
para establecer un estilo por defecto a una serie de elementos div
.
9.7. Directivas de validación
Éste es un buen ejemplo para varias cosas. Por un lado, veremos un ejemplo real del funcionamiento del atributo require
en una directiva.
Además, haremos uso del controlador ngModelController que habíamos visto en la parte de los formularios.
Por último, veremos cómo ampliar la API de validadción de AngularJS con directivas propias, ya que ésta es la manera recomendada de implementar la validación en AngularJS.
Para el ejemplo, introduciremos una directiva que valide DNIs. Como todos sabremos, la letra del DNI es un dígito de control que se obtiene al aplicar una fórmula matemática sobre el número. Nuestra directiva validará que la longitud del DNI y el dígito de control sean correctos. Tendrá una forma similar a:
1 <input type="text" ng-model="user.dni" is-dni />
Internet está lleno de sitios donde encontrar la fórmula de validación del DNI. Nosotros la hemos obtenido de aquí, porque también acepta NIEs.
Para este tipo de directivas, vamos a tener que hacer, sí o sí, una restricción de tipo atributo, así como un ngModelController
.
En versiones antiguas de AngularJS, la validación se hacía mediante el pipeline de $parsers
y $formatters
, como el siguiente ejemplo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 angular
.module('validator.nif', [])
.factory('nifValidator', function(){
return function (value) {
// Acepta NIEs (Extranjeros con X, Y o Z al principio)
// http://www.yporqueno.es/blog/javascript-validar-dni
var numero, letraDni, letra;
var expresion_regular_dni = /^[XYZ]?\d{5,8}[A-Z]$/;
var result;
value = ('' + value).toUpperCase();
if (expresion_regular_dni.test(value) === true) {
numero = value.substr(0, value.length - 1);
numero = numero.replace('X', 0);
numero = numero.replace('Y', 1);
numero = numero.replace('Z', 2);
letraDni = value.substr(value.length - 1, 1);
numero = numero % 23;
letra = 'TRWAGMYFPDXBNJZSQVHLCKET';
letra = letra.substring(numero, numero + 1);
if (letra != letraDni) {
result = false;
} else {
result = true;
}
} else {
result = false;
}
return result;
};
})
.directive('isNifOld', function (nifValidator) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
var validarNif = function (value) {
ngModelCtrl.$setValidity('isNif', nifValidator(value));
return value;
};
ngModelCtrl.$parsers.unshift(validarNif);
ngModelCtrl.$formatters.push(validarNif);
}
}
});
Sin embargo, en versiones posteriores a AngularJS 1.3, podemos asociar directamente una función al objeto $validators
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 angular
.module('validator.nif', [])
.factory('nifValidator', function(){
return function (value) {
// Acepta NIEs (Extranjeros con X, Y o Z al principio)
// http://www.yporqueno.es/blog/javascript-validar-dni
var numero, letraDni, letra;
var expresion_regular_dni = /^[XYZ]?\d{5,8}[A-Z]$/;
var result;
value = ('' + value).toUpperCase();
if (expresion_regular_dni.test(value) === true) {
numero = value.substr(0, value.length - 1);
numero = numero.replace('X', 0);
numero = numero.replace('Y', 1);
numero = numero.replace('Z', 2);
letraDni = value.substr(value.length - 1, 1);
numero = numero % 23;
letra = 'TRWAGMYFPDXBNJZSQVHLCKET';
letra = letra.substring(numero, numero + 1);
if (letra != letraDni) {
result = false;
} else {
result = true;
}
} else {
result = false;
}
console.log('result is', result);
return result;
};
})
.directive('isNif', function (nifValidator) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$validators.isNif = function (modelValue, viewValue) {
return nifValidator(modelValue || viewValue);
};
}
}
});
Tenemos un ejemplo funcionando de ambas directivas en este enlace. Se ha refactorizado la función de validación de NIFs a un servicio para usarla en varias directivas.
En este segundo ejemplo, no tenemos que establecer la validez del modelo en ninguno de los casos, ya que se hace de manera automática siempre y cuando nuestra función devuelva un valor booleano. Aparte de más sencillo, también es más fácil de leer y entender. Desde el HTML no hay diferencia alguna, ya que sólo afecta a la implementación de la directiva:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <form name="nifForm">
<div class="form-group">
<input type="text" name="nif1" is-nif-old ng-model="nif1" required class="form-control"/>
</div>
<div class="col-xs-12">
<pre>{{nifForm.nif1|json}}</pre>
</div>
<div class="form-group">
<input type="text" name="nif2" is-nif ng-model="nif2" required class="form-control"/>
</div>
<div class="col-xs-12">
<pre>{{nifForm.nif2|json}}</pre>
</div>
</form>
Si se produce un error de validación, en ambos casos se reflejará en el atributo $error del objeto ngModelController
. Por ejemplo, un campo vacío del formulario anterior contendrá dos errores: campo requerido y nif no válido:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 {
"$validators": {},
"$asyncValidators": {},
"$parsers": [
null
],
"$formatters": [
null,
null
],
"$viewChangeListeners": [],
"$untouched": false,
"$touched": true,
"$pristine": true,
"$dirty": false,
"$valid": false,
"$invalid": true,
"$error": {
"isNif": true, //ERROR POR NIF NO VALIDO
"required": true //ERROR POR CAMPO REQUERIDO
},
"$name": "nif1",
"$options": null
}
9.8. Mensajes de error de validación con ngMessages
La directiva ngMessages
es un switch-case
del DOM a la que pasas un objeto $error y ésta se encarga de pintar los mensajes de validación que sea necesario. Antes de eso (Angular 1.3 y previas), tenías que hacer una incómoda sucesión de ng-if
:
1
2
3
4
5
6
7
8
9
10 <form name="nifForm">
<div class="form-group">
<input type="text" name="nif1" is-nif-old ng-model="nif1" required class="form-control"/>
<p ng-if="nifForm.nif1.$error.required" class="label label-danger">Este campo es obligatorio</p>
<p ng-if="nifForm.nif1.$error.isNif" class="label label-danger">Formato de dni incorrecto</p>
</div>
<div class="col-xs-12">
<pre>{{nifForm.nif1|json}}</pre>
</div>
</form>
La manera de hacerlo es muy similar con ngMessages
:
1
2
3
4
5
6
7
8
9
10
11
12 <form name="nifForm">
<div class="form-group">
<input type="text" name="nif2" is-nif ng-model="nif2" required class="form-control"/>
<div ng-messages="nifForm.nif2.$error" multiple>
<p ng-message="required" class="label label-danger">Este campo es obligatorio</p>
<p ng-message="isNif" class="label label-danger">Formato de dni incorrecto</p>
</div>
</div>
<div class="col-xs-12">
<pre>{{nifForm.nif2|json}}</pre>
</div>
</form>
ngMessages
se distribuye como un módulo independiente del core de AngularJS, que tendremos que descargar e inyectar en nuestra aplicación:
1 <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular-messages.js"></script>
1
2 angular
.module('validator.nif', ['ngMessages'])
Al contrario que con ng-if
estamos pasando el objeto nifForm.nif2.$error
sólo una vez a la directiva. Ésta tomara la propiedad del objeto $error
y, si evalúa a cierto, se pintará el mensaje de error correspondiente.
Para reutilizar estados de validación genéricos, podemos utilizar la directiva ngMessageInclude
. Definiríamos una plantilla HTML que contendría todos los mensajes genéricos. Como ejemplo rápido, aquí validaremos dos DNIs, y utilizaremos la misma plantilla de validación para ambos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 <form name="nifForm">
<div class="form-group">
<input type="text" name="nif2" is-nif ng-model="nif2" required class="form-control"/>
<div ng-messages="nifForm.nif2.$error" multiple>
<div ng-messages-include="generic-messages"></div>
</div>
</div>
<div class="col-xs-12">
<pre>{{nifForm.nif2|json}}</pre>
</div>
<div class="form-group">
<input type="text" name="nif3" is-nif ng-model="nif2" required class="form-control"/>
<div ng-messages="nifForm.nif3.$error" multiple>
<div ng-messages-include="generic-messages"></div>
</div>
</div>
<div class="col-xs-12">
<pre>{{nifForm.nif3|json}}</pre>
</div>
</form>
<script type="text/ng-template" id="generic-messages">
<p ng-message="required" class="label label-danger">Este campo es obligatorio</p>
<p ng-message="isNif" class="label label-danger">Formato de dni incorrecto</p>
</script>
9.9. Ejercicios (2 puntos)
Aplica el tag directives
a la versión que quieres que se corrija.
9.9.1. Directiva de componente (1 puntos)
Vamos a crear una directiva llamada product
, que requiere un ngModel
. Como template tendrá el div que habíamos hecho para pintar los productos del carrito. Aquí está nuestra plantilla de directiva:
1
2
3
4
5
6
7
8
9
10
11
12 angular
.module('org.expertojava.carrito')
.directive('product', function(){
return {
require: '', //¿requiere algo?
restrict: '', //¿qué restrict le ponemos?
scope: {
product: '' //ver qué ponemos aquí.
},
templateUrl: '' //crear plantilla
};
})
Cambiaremos el bucle que mostraba los productos por el siguiente:
1
2
3 <div ng-repeat="product in products">
<product ng-model="product" />
</div>
o bien por este otro:
1
2
3 <div ng-repeat="product in products">
<div product ng-model="product"></div>
</div>
9.9.2. Directiva de validación (1 puntos)
Vamos a crear una directiva, llamada maxDecimals
, que usaremos en el formulario de edición del capítulo anterior. El dato será inválido si el número de decimales del input es mayor que valor que se asigne a max-decimals
.
En caso de error, deberá indicarse un mensaje indicando a qué se debe, haciendo uso de ngMessages
Ejemplo:
1 <input type="number" required max-decimals="2" />
La plantilla de nuestra directiva será:
1
2
3
4
5
6
7
8
9
10
11 angular
.module('org.expertojava.carrito')
.directive('maxDecimals', function(){
return {
require: 'ngModel',
restrict: '', //TODO: ¿Qué ponemos aquí?
link: function(scope, element, attrs, ngModelCtrl) {
//TODO: IMPLEMENTAR $validators
}
};
});