() translation by (you can also view the original English article)
Las directivas son uno de los componentes más poderosos de AngularJS, te ayudan a extender los elementos y atributos básicos de HTML y a crear código reusable y comprobable. En este tutorial, te mostrare cómo utilizar directivas de AngularJS con las mejores prácticas de la vida real.
Lo que quiero decir aquí con directivas, es mas que todo las personalizar directivas durante el tutorial. No voy a intentar enseñarte cómo utilizar directivas incorporadas como ng-repeat
, ng-show
, etc.. Te mostrare cómo utilizar directivas personalizadas para crear sus propios componentes.
Esquema
- Directivas simples
- Restricciones en Directivas
- Isolated Scope
- El Scope de las Directivas
- Herencia en Directivas
- Depuración en la Directiva
- Unit Testing en Directivas
- Prueba del Scope de la Directiva
- Conclusión
1. Directivas Simples
Vamos a decir que tienes una aplicación de comercio electrónico de libros y que estas mostrando el detalle de un libro específico en varias áreas, tales como los comentarios, páginas de Perfil de usuario, artículos, etc.. El widget de detalle del libro puede ser como se muestra a continuación:



Dentro de este widget hay una imagen del libro, título, descripción, comentarios y calificación. Recoger esa información y ponerla en un elemento específico del dom pueden ser difícil de lograr en cada lugar que desee utilizarlo. Vamos a Modular esta vista mediante una directiva de AngularJS.
1 |
angular.module('masteringAngularJsDirectives', []) |
2 |
.directive('book', function() { |
3 |
return { |
4 |
restrict: 'E', |
5 |
scope: { |
6 |
data: '=' |
7 |
},
|
8 |
templateUrl: 'templates/book-widget.html' |
9 |
}
|
10 |
})
|
Una función de directiva ha sido utilizada en el ejemplo anterior primeramente para crear una directiva El nombre de la Directiva es book
. Esta directiva devuelve un objeto y vamos a hablar un poco sobre este objeto. restrict
es para definir el tipo de directiva y puede ser un A
(Atributo), C
(Clase), E
(Elemento) y M
(coMentario). Usted puede ver el uso de cada uno de estos respectivamente a continuación.
Tipo | Uso |
---|---|
A | <div book></div> |
C | <div class="book"></div> |
E | <book data="book_data"></book> |
M | <!--directive: book--> |
scope
es para manejar el alcance de la Directiva. En el caso anterior, la data del book es transferida a la plantilla de la directiva utilizando el tipo de scope "="
. Voy a hablar en detalle sobre el scope en las siguientes secciones. templateUrl
es utilizado para llamar a una vista y así renderizar el contenido específico mediante el uso de datos transferidos al ámbito de la Directiva. También puede utilizar el template y proporcionan el código directamente al HTML, como esta:
1 |
.....
|
2 |
template: '<div>Book Info</div>' |
3 |
.....
|
En nuestro caso, tenemos una complicada estructura HTML, y es por eso que elegí la opción de templateUrl
.
2. Restricciones de Directiva
Directivas se definen en el archivo JavaScript de su proyecto AngularJS y que son utilizadas en la página HTML. Es posible utilizar las directivas de AngularJS en páginas HTML como se muestra a continuación:
A(Atributo)
En este caso, se utiliza el nombre de la directiva dentro de elementos HTML estándar. Digamos que tienes un menú basado en una aplicación de comercio electrónico. Este menú está formado según su rol actual. Puede definir una directiva para decidir si debe mostrarse el menú actual o no. Su menú HTML puede ser como a se muestra continuación:
1 |
<ul>
|
2 |
<li>Home</li> |
3 |
<li>Latest News</li> |
4 |
<li restricted>User Administration</li> |
5 |
<li restricted>Campaign Management</li> |
6 |
</ul>
|
y la Directiva como:
1 |
app.directive("restricted", function() { |
2 |
return { |
3 |
restrict: 'A', |
4 |
link: function(scope, element, attrs) { |
5 |
// Some auth check function
|
6 |
var isAuthorized = checkAuthorization(); |
7 |
if (!isAuthorized) { |
8 |
element.css('display', 'none'); |
9 |
}
|
10 |
}
|
11 |
}
|
12 |
})
|
Si utiliza la Directiva restricted
en el elemento de menú como un atributo, puede comprobar el nivel de acceso para cada menú. Si el usuario actual no está autorizado, no se mostrará este menú específico.
¿Entonces, qué es la función link
? Simplemente, la función link es la función que se puede utilizar para realizar operaciones específicas de la Directiva. La Directiva no es sólo una representación de código HTML proporcionado en algunas entradas. También puede enlazar funciones al elemento de la Directiva, llamar a un servicio y actualizar el valor de la Directiva, obtener atributos de la Directiva si es un tipo de directiva E
, etc.
C (Clase)
Usted puede utilizar el nombre de la directiva dentro de las clases de elemento HTML. Suponiendo que se utilice la directiva anterior como C
, se puede actualizar la Directiva restrict
como C
y utilizarlo como se muestra a continuación:
1 |
<ul>
|
2 |
<li>Home</li> |
3 |
<li>Latest News</li> |
4 |
<li class="nav restricted">User Administration</li> |
5 |
<li class="nav active restricted">Campaign Management</li> |
6 |
</ul>
|
Cada elemento ya tiene una clase para el estilo, y como se agrega la clase restricted
, en realidad es una directiva.
E (elemento)
No necesitas usar una directiva dentro de un elemento HTML. Puedes crear tu propio elemento mediante una directiva de AngularJS con una restricción de la E
. Digamos que tienes un widget de usuario en tu aplicación para mostrar username
, avatar
y reputation
en varios lugares en su aplicación. Puede que desee utilizar una directiva como esta:
1 |
app.directive("user", function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
link: function(scope, element, attrs) { |
5 |
scope.username = attrs.username; |
6 |
scope.avatar = attrs.avatar; |
7 |
scope.reputation = attrs.reputation; |
8 |
},
|
9 |
template: '<div>Username: {{username}}, Avatar: {{avatar}}, Reputation: {{reputation}}</div>' |
10 |
}
|
11 |
})
|
El código HTML será:
1 |
<user username="huseyinbabal" avatar="https://www.gravatar.com/avatar/ef36a722788f5d852e2635113b2b6b84?s=128&d=identicon&r=PG" reputation="8012"></user> |
En el ejemplo anterior, en un elemento personalizado se crean y se proporcionan algunos atributos como username
, avatar
y reputation
. Quiero llamar la atención sobre el cuerpo de la función link. Atributos del elemento se asignan al scope de la Directiva. El primer parámetro de la función link es el scope de la Directiva actual. El tercer parámetro de la Directiva es el objeto de atributo de la Directiva, lo que significa que puede leer cualquier atributo de la directiva personalizada mediante el uso de attrs.attr_name
. Los valores de atributo se asignan al alcance para que se utilicen dentro de la plantilla.
En realidad, puedes hacer esta operación de una manera más breve, y hablaré de eso más adelante. Este ejemplo es para comprender la idea principal del uso.
M (coMment)
Este uso no es muy común, pero mostraré cómo usarlo. Digamos que necesita un formulario de comentarios para que su aplicación lo use en muchos lugares. Puedes hacerlo usando la siguiente directiva:
1 |
app.directive("comment", function() { |
2 |
return { |
3 |
restrict: 'C', |
4 |
template: '<textarea class="comment"></textarea>' |
5 |
}
|
6 |
})
|
Y en el elemento HTML:
1 |
<!-- directive:comment -->
|
3. Scope aislado
Cada directiva tiene su propio alcance, pero debe
tener cuidado con el enlace de datos con la declaración de la directiva. Digamos que está implementando la parte basket
de su aplicación de
comercio electrónico. En la página de la cesta, ya ha agregado elementos
aquí anteriormente. Cada elemento tiene su campo de cantidad para
seleccionar cuántos elementos desea comprar, como a continuación:



Aquí está la declaración de directiva:
1 |
app.directive("item", function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
link: function(scope, element, attrs) { |
5 |
scope.name = attrs.name; |
6 |
},
|
7 |
template: '<div><strong>Name:</strong> {{name}} <strong>Select Amount:</strong> <select name="count" ng-model="count"><option value="1">1</option><option value="2">2</option></select> <strong>Selected Amount:</strong> {{count}}</div>' |
8 |
}
|
9 |
})
|
Y para mostrar tres elementos en HTML:
1 |
<item name="Item-1"></item> |
2 |
<item name="Item-2"></item> |
3 |
<item name="Item-3"></item> |
El
problema aquí es que cada vez que elija la cantidad del artículo
deseado, se actualizarán todas las secciones de cantidad de los
artículos. ¿Por qué? Porque hay un enlace de datos bidireccional con un count
de
nombres, pero el scope no está aislado. Para aislar el alcance,
simplemente agregue scope: {}
al atributo de la directiva en la
sección de retorno:
1 |
app.directive("item", function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
scope: {}, |
5 |
link: function(scope, element, attrs) { |
6 |
scope.name = attrs.name; |
7 |
},
|
8 |
template: '<div><strong>Name:</strong> {{name}} <strong>Select Amount:</strong> <select name="count" ng-model="count"><option value="1">1</option><option value="2">2</option></select> <strong>Selected Amount:</strong> {{count}}</div>' |
9 |
}
|
10 |
})
|
Esto lleva a
su directiva a tener su propio alcance aislado, por lo que el enlace de
datos bidireccional se producirá dentro de esta directiva por separado. También mencionaré sobre el atributo scope
más adelante.
4. Ámbitos de aplicación
La principal ventaja de la directiva es que es un componente reutilizable que se puede usar fácilmente; incluso puede proporcionar algunos atributos adicionales a esa directiva. Pero, ¿cómo es posible pasar un valor adicional, un enlace o una expresión a una directiva para que los datos se utilicen dentro de la directiva?
"@" Scope: este tipo de ámbito se utiliza para pasar el valor al alcance de la directiva. Supongamos que desea crear un widget para un mensaje de notificación:
1 |
app.controller("MessageCtrl", function() { |
2 |
$scope.message = "Product created!"; |
3 |
})
|
4 |
app.directive("notification", function() { |
5 |
return { |
6 |
restrict: 'E', |
7 |
scope: { |
8 |
message: '@' |
9 |
},
|
10 |
template: '<div class="alert">{{message}}</div>' |
11 |
}
|
12 |
});
|
y puedes usar:
1 |
<notification message="{{message}}"></notification> |
En este ejemplo, el valor del mensaje simplemente se asigna al scope de la directiva. El contenido HTML renderizado será:
1 |
<div class="alert">Product created!</div> |
"=" Scope: en este tipo de ámbito, las variables de ámbito se pasan en lugar de
los valores, lo que significa que no pasaremos {{message}}
, sino que
pasaremos message
. La razón detrás de esta
característica es la construcción de enlace de datos bidireccional entre
la directiva y los elementos o controladores de la página. Veámoslo en acción.
1 |
.directive("bookComment", function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
scope: { |
5 |
text: '=' |
6 |
},
|
7 |
template: '<input type="text" ng-model="text"/>' |
8 |
}
|
9 |
})
|
En esta
directiva, estamos tratando de crear un widget para mostrar el ingreso
de texto de comentarios para hacer un comentario para un libro
específico. Como puede ver, esta directiva requiere text
de atributo para construir un enlace de datos bidireccional entre
otros elementos en las páginas. Puedes usar esto en la página:
1 |
<span>This is the textbox on the directive</span> |
2 |
<book-comment text="commentText"></book-comment> |
Esto simplemente mostrará un cuadro de texto en la página, así que agreguemos algo más para interactuar con esta directiva:
1 |
<span>This is the textbox on the page</span> |
2 |
<input type="text" ng-model="commentText"/> |
3 |
<br/>
|
4 |
<span>This is the textbox on the directive</span> |
5 |
<book-comment text="commentText"></book-comment> |
Cada vez que escriba algo en el primer cuadro de texto, se tecleará también en el segundo cuadro de texto. Puedes hacer eso al revés. En
la directiva, pasamos la variable de ámbito commentText
en lugar del
valor, y esta variable es la referencia de enlace de datos al primer
cuadro de texto.
"&" Scope: podemos pasar el valor y hacer referencia a las directivas. En este tipo de alcance, veremos cómo pasar expresiones a la directiva. En casos de la vida real, es posible que deba pasar una función específica (expresión) a las directivas para evitar el acoplamiento. A veces, las directivas no necesitan saber mucho sobre la idea detrás de las expresiones. Por ejemplo, a una directiva le gustará el libro, pero no sabe cómo hacerlo. Para hacer eso, puedes seguir una estructura como esta:
1 |
.directive("likeBook", function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
scope: { |
5 |
like: '&' |
6 |
},
|
7 |
template: '<input type="button" ng-click="like()" value="Like"/>' |
8 |
}
|
9 |
})
|
En esta directiva, se pasará una expresión al botón de directiva a través del atributo like
. Vamos a definir una función en el controlador y pasarla a la directiva
dentro del HTML.
1 |
$scope.likeFunction = function() { |
2 |
alert("I like the book!") |
3 |
}
|
Esto estará dentro del controlador, y la plantilla será:
1 |
<like-book like="likeFunction()"></like-book> |
likeFunction()
proviene del
controlador y se pasa a la directiva. ¿Qué pasa si quieres pasar un
parámetro a likeFunction()
? Por ejemplo, puede necesitar pasar un valor
de clasificación a likeFunction()
. Es
muy simple: simplemente agregue un argumento a la función dentro del
controlador, y agregue un elemento de entrada a la directiva para
requerir el conteo de inicio del usuario. Puedes hacer eso como se
muestra a continuación:
1 |
.directive("likeBook", function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
scope: { |
5 |
like: '&' |
6 |
},
|
7 |
template: '<input type="text" ng-model="starCount" placeholder="Enter rate count here"/><br/>' + |
8 |
'<input type="button" ng-click="like({star: starCount})" value="Like"/>' |
9 |
}
|
10 |
})
|
1 |
$scope.likeFunction = function(star) { |
2 |
alert("I like the book!, and gave " + star + " star.") |
3 |
}
|
1 |
<like-book like="likeFunction(star)"></like-book> |
Como se puede ver, el text box viene de la Directiva. El valor del text box está enlazado al argumento de la función like({star: starCount})
. star
es para el controlador de la función y starCount
para el enlace o binding del valor en los textbox.
5. Herencia en la Directiva
A veces, puedes tener una característica que existe en varias directivas. Se puede poner en una directiva padre que heredadan por las directivas del hijo.
Déjeme darle un ejemplo de la vida real. Se quiere enviar datos estadísticos cuando los clientes mueven el cursor del ratón a la parte superior de un libro específico. Puede implementar un evento de clic para la Directiva book, pero ¿qué pasa si se utilizará por otra directiva? En este caso, puede usar la herencia de las directivas como a continuación:
1 |
app.directive('mouseClicked', function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
scope: {}, |
5 |
controller: "MouseClickedCtrl as mouseClicked" |
6 |
}
|
7 |
})
|
Se trata de una directiva de padres para ser heredada por las directivas de hijo. Como puedes ver hay un atributo para controlar la Directiva usando la Directiva "as". Vamos a definir este controlador también:
1 |
app.controller('MouseClickedCtrl', function($element) { |
2 |
var mouseClicked = this; |
3 |
|
4 |
mouseClicked.bookType = null; |
5 |
|
6 |
mouseClicked.setBookType = function(type) { |
7 |
mouseClicked.bookType = type |
8 |
};
|
9 |
|
10 |
$element.bind("click", function() { |
11 |
alert("Typeof book: " + mouseClicked.bookType + " sent for statistical analysis!"); |
12 |
})
|
13 |
})
|
En este
controlador, simplemente estamos configurando una instancia de
controlador de la variable bookType
mediante el uso de directivas
secundarias. Cada vez que haga clic en un libro o revista,
el tipo de elemento se enviará al servicio de fondo (utilicé una función
de alerta solo para mostrar los datos). ¿De qué manera las directivas infantiles podrán usar esta directiva?
1 |
app.directive('ebook', function() { |
2 |
return { |
3 |
require: "mouseClicked", |
4 |
link: function(scope, element, attrs, mouseClickedCtrl) { |
5 |
mouseClickedCtrl.setBookType("EBOOK"); |
6 |
}
|
7 |
}
|
8 |
})
|
9 |
.directive('magazine', function() { |
10 |
return { |
11 |
require: "mouseClicked", |
12 |
link: function(scope, element, attrs, mouseClickedCtrl) { |
13 |
mouseClickedCtrl.setBookType("MAGAZINE"); |
14 |
}
|
15 |
}
|
16 |
})
|
Como puede ver, las directivas secundarias usan la palabra clave require
para usar la directiva principal. Y un punto más importante es el cuarto argumento de la función de enlace
en las subdirectivas. Este
argumento se refiere al atributo del controlador de la directiva padre
que significa que la directiva hija puede usar la función del
controlador setBookType
dentro del controlador. Si el elemento actual es
un eBook, puede usar la primera directiva, y si es una revista, puede
usar la segunda:
1 |
<a><mouse-clicked ebook>Game of thrones (click me)</mouse-clicked></a><br/> |
2 |
<a><mouse-clicked magazine>PC World (click me)</mouse-clicked></a> |
Las directivas para niños son como una propiedad de la directiva principal. Hemos eliminado el uso del evento de clic del mouse para cada directiva hija al poner esa sección dentro de la directiva principal.
6. Depuración de directivas
Cuando utiliza directivas dentro de la plantilla, lo que ve en
la página es la versión compilada de la directiva. A veces, desea ver
el uso real de la directiva para fines de depuración. Para ver la
versión no compilada de la sección actual, puede usar ng-non-bindable
. Por ejemplo, supongamos que tiene un widget que imprime los libros más
populares, y aquí está el código para eso:
1 |
<ul>
|
2 |
<li ng-repeat="book in books">{{book}}</li> |
3 |
</ul>
|
La variable de ámbito del libro proviene del controlador, y el resultado de esto es el siguiente:



Si desea conocer el uso de directivas detrás de este resultado compilado, puede usar esta versión del código:
1 |
<ul ng-non-bindable=""> |
2 |
<li ng-repeat="book in books">{{book}}</li> |
3 |
</ul>
|
Esta vez, la salida será como a continuación:



Hasta ahora, es genial, pero ¿y si queremos ver las versiones compiladas y sin compilar del widget? Es hora de escribir una directiva personalizada que realice una operación de depuración avanzada.
1 |
app.directive('customDebug', function($compile) { |
2 |
return { |
3 |
terminal: true, |
4 |
link: function(scope, element) { |
5 |
var currentElement = element.clone(); |
6 |
currentElement.removeAttr("custom-debug"); |
7 |
var newElement = $compile(currentElement)(scope); |
8 |
element.attr("style", "border: 1px solid red"); |
9 |
element.after(newElement); |
10 |
}
|
11 |
}
|
12 |
})
|
En
esta directiva, estamos clonando el elemento que está en modo de
depuración para que no se modifique después de un conjunto de
operaciones. Después
de la clonación, elimine la directiva custom-debug
para
no actuar como modo de depuración, y luego compílelo con $complile
, que
ya está incluido en la directiva. Hemos dado un estilo al elemento de
modo de depuración para enfatizar el depurado. El resultado final será
el siguiente:



Puede guardar su tiempo de desarrollo utilizando este tipo de directiva de depuración para detectar la causa raíz de cualquier error en su proyecto.
7. Directivas Unit Testing
Como ya sabe, las pruebas unitarias son una parte muy importante del desarrollo para controlar totalmente el código que ha escrito y evitar posibles errores. No profundizaré en las pruebas unitarias, pero te daré una pista sobre cómo probar las directivas de varias maneras.
Usaré
Jasmine para las pruebas unitarias y Karma para el corredor de prueba de
la unidad. Para
usar Karma, simplemente instálalo globalmente ejecutando npm install -g karma karma-cli
(necesitas tener Node.js y npm instalados en tu
computadora). Después de la instalación, abra la línea de comando, vaya a
la carpeta raíz de su proyecto y escriba karma init
. Le hará un par de
preguntas a continuación para configurar sus requisitos de prueba.



Estoy usando Webstorm para el desarrollo y si también usas Webstorm, simplemente haga clic derecho en karma.conf.js y seleccione Run karma.conf.js. Esto ejecutará todas las pruebas que se configuran en el karma conf. También puede ejecutar las pruebas con la línea de comando karma start
en la carpeta raíz del proyecto. Eso es todo acerca de la configuración del ambiente, así que vamos a pasar a la parte de la prueba.
Digamos que queremos probar la Directiva book. Cuando pasamos un título a la Directiva, esta deberá ser compilada en una vista detallada como book. Así que, vamos a empezar.
1 |
describe("Book Tests", function() { |
2 |
var element; |
3 |
var scope; |
4 |
beforeEach(module("masteringAngularJsDirectives")) |
5 |
beforeEach(inject(function($compile, $rootScope) { |
6 |
scope = $rootScope; |
7 |
element = angular.element("<booktest title='test'></booktest>"); |
8 |
$compile(element)($rootScope) |
9 |
scope.$digest() |
10 |
}));
|
11 |
|
12 |
it("directive should be successfully compiled", function() { |
13 |
expect(element.html()).toBe("test") |
14 |
})
|
15 |
});
|
En la prueba anterior, estamos probando una nueva Directiva llamada booktest
. Esta directiva toma el argumento title
y crea un div utilizando este título. En esta prueba, antes de cada sección, estamos llamando de primero a nuestro módulo masteringAngularJsDirectives
. Entonces, estamos generando una directiva denominada booktest. En cada paso de la prueba, la salida de la Directivas será probada. Esta prueba es sólo para una comprobar el valor.
8. Probando el Scope de la Directiva
En esta sección, vamos a probar el scope(alcance) de la Directiva booktest
. Esta directiva genera una vista de detalle del libro en la página, y al hacer clic en esta sección de detalle, una variable de alcance llamada viewed
se establecerá como true
. En nuestra prueba, comprobaremos si viewed
está configurado como verdadero cuando se active el evento click. La Directiva es:
1 |
.directive('booktest', function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
scope: { |
5 |
title: '@' |
6 |
},
|
7 |
replace: true, |
8 |
template: '<div>{{title}}</div>', |
9 |
link: function(scope, element, attrs) { |
10 |
element.bind("click", function() { |
11 |
console.log("book viewed!"); |
12 |
scope.viewed = true; |
13 |
});
|
14 |
}
|
15 |
}
|
16 |
})
|
Para configurar un evento a un elemento de AngularJS dentro de la Directiva, puede utilizar el atributo de link
. Dentro de este atributo, tienes el elemento actual, directamente ligado a un evento click. Para probar esta Directiva, puede utilizar el siguiente:
1 |
describe("Book Tests", function() { |
2 |
var element; |
3 |
var scope; |
4 |
beforeEach(module("masteringAngularJsDirectives")) |
5 |
beforeEach(inject(function($compile, $rootScope) { |
6 |
scope = $rootScope; |
7 |
element = angular.element("<booktest title='test'></booktest>"); |
8 |
$compile(element)($rootScope) |
9 |
scope.$digest() |
10 |
}));
|
11 |
|
12 |
it("scope liked should be true when book liked", function() { |
13 |
element.triggerHandler("click"); |
14 |
expect(element.isolateScope().viewed).toBe(true); |
15 |
});
|
16 |
});
|
En la sección de prueba, un evento click se activa mediante el uso de element.triggerHandler("click")
. Cuando se desencadena un evento click, la variable visualizada debe establecerse como true
. Ese valor es reivindicado por using expect(element.isolateScope().viewed).toBe(true)
.
9. Conclusión
Para poder desarrollar proyectos web modulares y comprobables, AngularJS es el mejor en común. Las directivas son uno de los mejores componentes de AngularJS, y esto significa que cuanto más sepas sobre las directivas de AngularJS, los proyectos que desarrolles serán más modulares y comprobables.
En este tutorial, he intentado mostrar las mejores prácticas de la vida real acerca de las directivas y tenga en cuenta que tienes que hacer un montón de práctica para entender la lógica detrás de las directivas. Espero que este artículo te ayude a comprender bien las directivas de AngularJS.