Profundizando en Angular.js: Directivas personalizadas y Servicios

Tras el post de introducción a Angular y continuando con la saga, hoy hablaremos de como estructurar mejor el código mediante directivas personalizadas y Services:

Directivas personalizadas

Templates

Los templates, que se incluyen con la directiva ng-include, nos permiten incorporar -y facilitar la repetibilidad- de fragmentos HTML. También podemos incluir comportamientos de un controlador, por ejemplo.
Funcionaría del siguiente modo.

En mi index.html

<div ng-repeat="usuario in MiControlador.usuarios">
<h3 ng-include="'product-details.html'">
</h3>
</div>

En mi product-details.html

{{product.name}}
<em class="pull-right">{{product.description}}</em>

No obstante, recordemos que nuestra idea es trabajar con Directivas, que nos permiten escribir HTML que expresa el comportamiento de nuestra aplicación.

Por eso, nuestra intención es llevar esta idea de los templates, hacia el mundo de las Directivas.

Custom Directives

Vamos a crear una directiva personalizada para replicar el comportamiento anterior del template, sin tener que usar ng-include.

Nuestro objetivo, es poder reemplazar el código:

<div ng-repeat="usuario in MiControlador.usuarios">
<h3 ng-include="'product-details.html'">
</h3>
</div>

por el código:

<div ng-repeat="usuario in MiControlador.usuarios">
<product-details></product-details>
</div>

Que como podemos ver, queda más autoexplicativo e integrado con el resto de HTML.

Veámos como hacerlo:

Abrimos nuestro archivo javascript, y añadimos la siguiente declaración:

//some js code
app
.directive('productDetails', function(){
return{
//config object defining how the directive should work
};
});

DETALLE #1:
La palabra compuesta por guiones product-details en HTML, se traduce a estilo camelCase, transformándose en productDetails

DETALLE #2:
Obsérvese que NO utilizamos tags autocerrados tipo < product-details />. Esto se debe a que determinados browsers pueden dar problemas con el autocerrado en elementos personalizados. Por este motivo, siempre usaremos el formato < product-details> < /product-details>

Configuración de la custom directive

En este caso, el contenido de la directiva contendrá:

  • restriction: ‘E’ -> Tipo de directiva, en este caso de **E**lemento
  • templateUrl: ‘product-details.html’ -> Url del contenido

Resultado final de la custom directive

Nuestros archivos quedarán del siguiente modo:

index.html

<div ng-repeat="usuario in MiControlador.usuarios">
<product-details></product-details>
</div>

app.js

//some js code
//...
app
.directive('productDetails', function(){
return{
restrict
: 'E',
templateUrl
: 'product-details.html'
};
});

product-details.html

<h3>
{{product.name}}
<em class="pull-right">{{product.description}}</em>
</h3>

Al incluir < product-details> en nuestro index.html, lo que conseguimos es que a través de Angular, nuestro index.html se renderice del siguiente modo:

index.html

<div ng-repeat="usuario in MiControlador.usuarios">
<h3>
{{product.name}}
<em class="pull-right">{{product.description}}</em>
</h3>
</div>

Tipos de directivas custom

Element directives

del estilo que hemos visto:

<product-details></product-details>

Recomendado para UI-widgets

Attribute directives

Como complemento de un atributo:

<h3 product-details></h3>

Recomendado para comportamientos mixtos, como los tooltips.

En este caso, en nuestro javascript cambiaría el tipo de restricción a la A de atributo:

app.js

//some js code
//...
app
.directive('productDetails', function(){
return{
restrict
: 'A',
templateUrl
: 'product-details.html'
};
});

Directivas para controladores

Imaginemos que tenemos un fragmento de código en el que usamos nuestro controlador, y queremos estructurarlo mediante directivas. Pongamos que nuestro código inicial es este:

index.html

<!-- some html code -->
<section ng-controller="TabController as tab">
<ul class="nav nav-pills">
<li ng-class="{ active:tab.isSet(1) }">
<a href ng-click="tab.setTab(1)">First tab</a>
</li>
<li ng-class="{ active:tab.isSet(2) }">
<a href ng-click="tab.setTab(2)">Second tab</a>
</li>
</ul>

<!-- Tabs Content -->
<custom-first-tab ng-show="tab.isSet(1)"> </custom-first-tab>
<div custom-second-tab ng-show="tab.isSet(2)" ></div>
</section>
</div>

miApp.js

app.controller("TabController",function() {
this.tab = 1;

this.isSet = function(checkTab) {
return this.tab === checkTab;
};

this.setTab = function(setTab) {
this.tab = setTab;
};
});

Incluir el controlador en la directiva

Lo primero que vamos a hacer es crear un nuevo element directive, cuyo comportamiento definiremos en products-tab.html, y en la que incluiremos el código del controlador, y el alias que queremos usar:

miApp.js

app.directive("productTabs", function(){
return{
restrict
: 'E',
templateUrl
: "product-tabs.html",
controller
: function() {
this.tab = 1;

this.isSet = function(checkTab) {
return this.tab === checkTab;
};

this.setTab = function(setTab) {
this.tab = setTab;
};
},
controllerAs
:"tab"
};
});

Eliminar el antiguo controlador

Dado que los métodos de nuestro controlador ya han sido incluidos en la directiva, podemos prescindir del antiguo controlador y eliminar por completo el código:

app.controller("TabController",function() {
//...
});

Traspasar el elemento HTML a la directiva

Ahora que podemos llamar nuestra directiva, tendremos que definir su contenido en product-tabs.html, que será el mismo que teníamos antes en index.html, aunque en este caso ya no necesito llamar a ng-controller:

product-tabs.html

<section>
<ul class="nav nav-pills">
<li ng-class="{ active:tab.isSet(1) }">
<a href ng-click="tab.setTab(1)">First tab</a>
</li>
<li ng-class="{ active:tab.isSet(2) }">
<a href ng-click="tab.setTab(2)">Second tab</a>
</li>
</ul>

<!-- Tabs Content -->
<custom-first-tab ng-show="tab.isSet(1)"> </custom-first-tab>
<div custom-second-tab ng-show="tab.isSet(2)" ></div>
</section>
</div>

Utilizar la directiva en index.html

Finalmente, solo queda el detalle de eliminar de index.html el código traspasado a products.html, y en su lugar, utilizar nuestra nueva directiva:
index.html

<!-- some html code -->
<product-tabs></product-tabs>

Services

Dependencias

Recordemos primero el concepto de dependencias que explicábamos en el anterior post.
Cuando definimos vía el método module una aplicación en Angular, le podemos pasar dependencias a otros módulos.

De este modo, cuando nuestro código empieza a crecer, tiene sentido dividir el JS en distintos módulos, específicos para cada tarea.

Así pues, si mi aplicación tiene usuarios, tendría sentido dividir mi javascript en 2 módulos (el principal que gobierna la app y el de usuarios), que además puedo separar en sendos ficheros distintos para mayor comodidad:

app.js: el módulo principal, que incluiré con ng-app

(function(){
var app = angular.module('dashboard', ['users']);

app
.controller("DashboardController", function(){...});
//...
})();

users.js: todas las funcionalidades exclusivamente de usuarios

(function(){
var app = angular.module('users', [ ]);

app
.controller("UserController", function(){...});
app
.directive("userBio", function(){...});
//...
})();

Recordando, por supuesto, que deberé incluir también el nuevo archivo javascript en la página.

index.html:

<!DOCTYPE html>
<html>
<head>...
</head>

<body>
<script type="text/javascript" src="angular.min.js"></script>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript" src="users.js"></script>
</body>
</html>

Servicios

Los servicios (que se distinguen porque siempre empiezan por $) son herramientas de Angular que nos permiten dotar de capacidades adicionales a nuestro controlador, como por ejemplo:

  • $http: permite extraer datos JSON de un web service

  • $log: permite mostrar mensajes en la consola Javascript

  • $filter: permite filtrar un array

Servicio $http

Nos permite hacer una petición asíncrona a un servidor.

Hay dos formas de usar el servicio $http:

  • Como función con opciones como argumento (Aquí podemos utilizar todas las posibles peticiones: GET, POST, PUT, DELETE, OPTIONS, PATCH, TRACE…)
$http({method:'GET', url:'/users.json'});
  • A través de métodos específicos del servicio
    • get (visto)
    • post /put
    • delete
$http.get('/users.json',{apiKey:'miApiKey' });

$http
.post('/users.json',{param:'values' });

$http
.delete('/users.json');

En ámbos casos, se nos devuelve un objeto Promise, es decir, un objeto que nos permite aplicarle callbacks como:

  • success()
  • error()

DETALLE: Es interesante comentar que si le decimos a nuestra petición a través de $http que obtenga un JSON, la respuesta se descodificará automáticamente en objetos y arrays Javascript, sin ninguna necesidad de parsear los datos.

Dependency Injection

El modo de explicarle a un controlador que puede utilizar una directiva, es a través de una dependency injection, con la siguiente estructura:

app.controller('MiController', ['$http', '$log', function($http, $log){

} ]);

Es decir,

  • le comentamos al controlador los servicios de los que depende -de modo que Angular, a través de un injector se los pase como argumentos al ser ejecutado,

  • y acto seguido, definimos justamente los dos argumentos correspondientes, para utilizarlos dentro del controlador.

Utilizando los servicios

Siguiendo con el servicio $http, vamos a ver como lo utilizaríamos para recuperar, de un web service, un listado de usuarios:

app.controller('MiController', ['$http', function($http){
var miController = this;
miController
.users = [];

$http
.get('/users.json').success(function(data){
miController
.users = data;
});
} ]);

DETALLE 1 : ¿Por qué no utilizamos el this.users dentro del callback?

Pues por que dentro del callback -que es un método de $httpthis hace referencia al servicio $http;

DETALLE2: Dado que la petición es asíncrona puede tardar en obtener los resultados, inicializamos el array de usuarios a [], para asegurarnos de que no se muestren datos sin sentido antes de que se llame al método success();