Banner Post

Tutorial: O que é Angular Directive?

Durante a construção deste site, pesquisei algumas estruturas de árvore prontas para utilizar na parte administrativa do site, para criação de categorias e posts para o blog. Mas como muitas delas entram em conflito com outras bibliotecas que possuo, resolvi criar a minha própria. Como gosto de criar as coisas de uma forma organizada e de aprender novas linguagens e estruturas, fui atrás de aprofundar meu conhecimento Angular e criar minha primeira biblioteca Angular para estrutura de árvore.

O que são Angular Directives?

De acordo com a documentação oficial, diretivas angular são marcadores em elementos DOM que dizem ao compilador de HTML do AngularJS comportamentos específicos para um elemento DOM. Em outras palavras, as diretivas ajudam o desenvolvedor a criar atributos e elementos dentro do HTML para dar comportamentos e criar estruturas via JavaScript. Com isso, o código pode ser componentizado, facilitando a leitura do HTML, evitando redundâncias de código, assim como o JSF faz para o Java.

A beleza das diretivas do AngularJS é que elas são interpretadas client-side, pelo browser do usuário, podendo até diminuir o payload enviado ao usuário final (dependendo do tamanho do JavaScript e o número de dados redundantes evitados). Mas o que considero um dos maiores ganhos das diretivas é para o web designer, uma vez que o código feito pode ser reutilizado pelas diferentes tecnologias server-side (Java, C#, NodeJS, Python). Com isso, o web designer não precisa aprender aquelas tags específicas de jsf, jstl, ... e trabalhar apenas com o bom e velho JavaScript, além do HTML e CSS, é claro.

Um exemplo simples

Considere então o seguinte código JS:

var app = angular.module('tapp', []);
app.controller('tctrl',function($scope,$http,$interval,$animate) {

}).directive('meuElemento', function() {
	return {
		template: '<b>Meu elemento personalizado!</b>'
	};
});

Estou criando uma diretiva simples chamada meuElemento. Como template, declarei o texto "Meu elemento personalizado" com a tag <b> para deixa-lo negrito, com isso estou dizendo para o Angular que, ao encontrar o meu elemento no HTML, para utilizar o template e inserir o conteúdo no HTML.

Então, para utilizar a diretiva criada, vamos ao HTML e substituimos o camel case por hífens (-), inserindo o meuElemento como meu-elemento dentro do HTML.

<div ng-app="tapp" ng-controller="tctrl">
	<meu-elemento></meu-elemento>
</div>

O resultado para o usuário seria apenas "Meu elemento personalizado!" em tela.

Caso prefira continuar utilizando elementos HTML normalmente (div, span, etc), isso ainda é possível adicionando a diretiva como atributo e tendo o mesmo efeito como resultado.

<div ng-app="tapp" ng-controller="tctrl">
	<div meu-elemento></div>
</div>

O problema dessa abordagem é que ela não passará no teste de validação de HTML, caso utilize ferramentas deste tipo. Mas existe solução! Para este tipo de problema podemos utilizar os atributos com prefixo data-. Este prefixo foi criado para exatamente este propósito, atributos personalizados. E para a nossa felicidade, o AngularJS leva isso em consideração na interpretação do DOM.

Utilizando uma página como template

Em alguns casos não é interessante utilizar o atributo template, pois o HTML pode ser complexo ou possuir muita informação, que ficaria de difícil manutenção dentro do JS. Para evitar isso, podemos utilizar o atributo templateUri.

var app = angular.module('tapp', []);
app.controller('tctrl',function($scope,$http,$interval,$animate) {

}).directive('meuElemento', function() {
	return {
		templateUri: 'MeuElemento.html'
	};
});

E em um HTML apartado, chamado MeuElemento.html, adicionamos o HTML desejado.

<b>Meu elemento personalizado!</b>

E o resultado produzido será o mesmo.

Uma funcionalidade bacana do angular é que ele deixar declarar templates HTML dentro da própria página utilizando a tag script com o tipo "text/ng-template" e um id para declarar o nome. Com isso, o exemplo abaixo poderia estar integralmente em um arquivo HTML que iria funcionar normalmente.

<html ng-app="tapp">

<head lang="pt">
<meta charset="utf-8"> 
<meta name="viewport" content="width=device-width, initial-scale=1">

<link rel="stylesheet" type="text/css" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>

<script type="text/ng-template" id="/template.html">
<div style="padding:5px;">
	<div class="alert alert-success">Meu atributo é um sucesso!</div>
</div>
</script>
<script>
var app = angular.module('tapp', []);
app.controller('tctrl',function($scope,$http,$interval,$animate) {

}).directive('meuElemento', function() {
	return {
		templateUrl: '/template.html'
	};
});

</script>

</head>

<body ng-controller="tctrl">
	<div meu-elemento></div>
</body>


</html>

Com este HTML, o resultado é a imagem abaixo.


Um exemplo prático

Neste exemplo, vamos criar uma tabela dinamica de clientes que poderia ser reutilizada em diversos momentos dentro de uma aplicação. Então criamos um template base para essa tabela.

<table class="tabelaClientes">
	<thead>
		<tr>
			<th>Nome</th>
			<th>Sobrenome</th>
			<th>Empresa</th>
			<th>E-mail</th>
		</tr>
	</thead>
	<tbody>
		<tr ng-repeat="cliente in clientes">
			<td>{{ cliente.nome }}</td>
			<td>{{ cliente.sobrenome }}</td>
			<td>{{ cliente.empresa }}</td>
			<td>{{ cliente.email }}</td>
		</tr>
	</tbody>
</table>

Depois crio o estilo para ela.

.tabelaClientes thead tr{
	border-bottom:1px solid black;
}
.tabelaClientes td{
	padding-right:6px;
}

E então criamos uma diretiva para utilizar o nosso template.

app.directive('tabelaClientes', function() {
	return {
		restrict: 'E',
		scope: {
			'clientes': '='
		},
		templateUrl: '/template.html'
	};
});

Aqui encontramos algumas novidades. Vou explicar um pouco melhor cada um dos itens novos.

  • restrict - Este atributo serve para restringir como deve ser usada a nossa diretiva. Os possíveis valores são:
    • A - atributo
    • E - elemento
    • C - class, classe de estilos
    • M - comentário
  • scope - Estamos criando um escopo isolado para essa diretiva. Assim como temos um $scope dentro de um controller, podemos ter um escopo próprio da nossa diretiva.

No caso, estamos declarando que a nossa diretiva deve ser utilizada somente como um elemento e dentro do escopo criamos clientes, que receberá os clientes a serem inseridos na tabela. Se olharmos o template que criamos anteriormente, dentro do tbody criamos o tr com ng-repeat="cliente in clientes". O clientes virá do escopo que estamos declarando. O valor '=' para clientes significa que vamos utilizar um atributo de mesmo nome.

<tabela-clientes clientes="meusClientes"></tabela-clientes>

Caso não queira utilizar o mesmo nome, pode alterar o nome do atributo da seguinte maneira.

//...
scope: {
    'clientes': '=lista'
}
//...

<tabela-clientes lista="meusClientes"></tabela-clientes>

Com isso a diretiva já está criada e referenciada no HTML, mas ainda não criamos a nossa lista de clientes meusClientes. Então, dentro do nosso controlador, vamos cria-la.

app.controller('tctrl',function($scope) {
	$scope.meusClientes = [
		{
			nome: 'Clark',
			sobrenome: 'Kent',
			empresa: 'Daily Planet',
			email: 'ckent@dailyplanet.com'
		},
		{
			nome: 'Peter',
			sobrenome: 'Parker',
			empresa: 'Daily Bugle',
			email: 'peterparker@dailybugle.com'
		},
		{
			nome: 'Groot',
			sobrenome: '',
			empresa: 'I am Groot',
			email: 'I am Groot'
		}
	];
});

Com isso chegamos ao resultado esperado, com uma tabela formatada com as informações de cliente como mostra a imagem abaixo.


Buscando as informações do servidor

Em casos reais, não teremos as informações de antemão como acontece no exemplo acima. Neste caso poderíamos simplesmente inserir a tabela no HTML sem problemas. Mas e se precisarmos carregar as informações do servidor dinamicamente?

Neste caso podemos apenas criar um serviço que retorna o JSON pronto com os clientes e alterar o valor da variável meusClientes e deixamos a diretiva fazer o resto.

Para testar esse comportamento, criei um botão abaixo da tabela.

<button class="btn btn-info" ng-click="carregar()">Carregar clientes</button>

E então temos que criar o novo método carregar dentro do controller.

app.controller('tctrl',function($scope, $http) {
    //...

    $scope.carregar=function(){
        $http({
            method:'GET',
            url:'clientes.js'
        }).then(function(res){
            $scope.meusClientes=res.data;
        });
    };
    //...
});

Perceba que adicionei o parâmetro $http no meu controller para que eu possa realizar chamadas assíncronas ao servidor. Então, no método carregar, estou chamando a url clientes.js para trazer a lista de clientes que devo mostrar na tabela. Neste caso, criei um arquivo clientes.js que irá retornar sempre os mesmos registros.

[{
	"nome": "Alberto",
	"sobrenome": "Almeida",
	"empresa": "XPTO Software",
	"email": "aalmeida@xpto.com"
}, {
	"nome": "Bianca",
	"sobrenome": "Bezerra",
	"empresa": "Empresa LTDA",
	"email": "bianca.bezerra@empresa.com.br"
}, {
	"nome": "Claudio",
	"sobrenome": "Coragem",
	"empresa": "MKT Forever",
	"email": "coragem@mktforever.com.br"
}, {
	"nome": "Daniela",
	"sobrenome": "Dantas",
	"empresa": "Mamão com Açucar - Presentes & Lembrancinhas",
	"email": "danieladantas152@hotmail.com"
}, {
	"nome": "Eduardo",
	"sobrenome": "Ezequiel",
	"empresa": "Santana Major",
	"email": "eduardoe@santanamajor.com.br"
}, {
	"nome": "Fabiana",
	"sobrenome": "Fagundes",
	"empresa": "Cortex Frontal",
	"email": "fabiana@cortexfrontal.com.br"
}]

Pronto. Agora temos uma diretiva pronta para rodar em produção.

Antes de apertar o botão:


Depois:



O resultado pode ser visto aqui!