Passo 8 - Roteamento e múltiplas Views
Nos passos anteriores, foi utilizado o conceito de "tela", que permite uma troca de contexto da interface gráfica. Entretanto, a abordagem utilizada (mostrar e ocultar conteúdo) não é a maneira mais adequada de realizar este procedimento, principalmente com o aumento da quantidade de funcionalidades (e de telas) do aplicativo.
Um aplicativo de única página (SPA) (do inglês Single Page Appplication) é um recurso de programação front-end que faz com que o aplicativo web não utilize o formato tradicional de troca de página, ou seja, há apenas uma única página e ocorrem trocas de telas. Esse conceito foi utilizado até o passo 7 e continuará sendo utilizado no restante deste tutorial do Angular.
Para saber mais
Se quiser saber mais sobre o conceito de SPA, pode começar lendo esse artigo da wikipedia (em inglês): https://en.wikipedia.org/wiki/Single-page_application.
Até o passo 7, quando o usuário clica no botão "Detalhes", a tela de lista de telefones é ocultada e é apresentada a tela de detalhes do telefone. O Pass 8 utiliza o módulo angular-route para fornecer uma alternativa mais adequada.
Módulo angular-route
O módulo angular-route fornece serviços necessários para que o aplicativo utilize o conceito de múltiplas views.
O arquivo package.json precisa incluir o módulo angular-route nas suas dependências.
Múltiplas views, Rotas e Template de Layout
Uma "rota" é um recurso que permite ao navegador mudar o endereço (a URL) atual sem, efetivamente, mudar de página. A princípio, isso pode soar estranho, mas é um recurso amplamente utilizado em desenvolvimento web moderno. O módulo angular-route fornece o serviço $route, que permite relacionar controllers, views e a URL atual do navegador.
Uma rota é padrão, e é representada no navegador por uma URL, por exemplo:
http://localhost:8080/#/telefones
Importante observar a presenta do caractere # (chamado de hash) na URL. Uma rota, portanto, é apresentada na URL a partir de #.
Um "template de layout" é responsável por definir um template que é comum à todas as views do aplicativo. As views são chamadas de "templates parciais" porque incluem somente a parte do template que é necessária para cada tela.
Estrutura do aplicativo
A partir de então, o aplicativo será organizado por "módulos". A estrutura de arquivos é a seguinte:
passo-8
│ app.css
│ app.js
│ index.html
│ package.json
│
├───data
│ └───phones
│ dell-streak-7.json
| ...
│
├───img
│ └───phones
│ dell-streak-7.0.jpg
| ...
│
├───node_modules
| ...
│
└───telefones
detalhes.html
lista.html
modulo.js
O diretório telefones representa o módulo Telefones, que apresenta a lista e os detalhes de telefones. A ideia de separar o aplicativo em módulos representa uma proposta de arquitetura para o software que pretende isolar ou separar partes do software em módulos, isto é, se consegue, com isso, modularização.
Template de layout
O arquivo index.html, utilizado como template de layout é bastante modificado em relação ao Passo 7, como mostra o trecho de código a seguir:
<!doctype html>
<html lang="pt-br" ng-app="phonecat">
<head>
...
<title ng-bind="pageTitle"></title>
...
<script src="node_modules/angular/angular.min.js"></script>
<script src="node_modules/angular-route/angular-route.min.js"></script>
<script src="telefones/modulo.js"></script>
<script src="app.js"></script>
...
</head>
<body>
<div class="container">
<div ng-view></div>
</div>
</body>
</html>
A diretiva ng-bind está sendo aplicada ao elemento title para que o título da janela seja definido dinamicamente, no controller. O valor do atributo representa uma propriedade do model. Neste caso, a propriedade é pageTitle. Os passos anteriores apresentaram como interagir com o escopo de um controller. Como será visto posteriormente, o Passo 8 demonstra como interagir com o escopo raiz do aplicativo.
Na seção de arquivos JavaScript importados no arquivo index.html estão os arquivos:
node_modules/angular-route/angular-route.min.js(do móduloangular-route)telefones/modulo.js(que implementa o módulo telefone)
A ordem de inclusão dos arquivos JavaScript é importante, uma vez que o arquivo app.js depende do módulo telefone.
Diretiva ng-view
O módulo angular-route fornece a diretiva ng-view. Por meio dessa diretiva os templates parciais são dinamicamente embutidos neste local (dentro de <div class="container">).
A figura a seguir ajuda a ilustrar este conceito.
A figura demonstra que os templates parciais são incluídos de modo exclusivo dentro da div que está com a diretiva ng-view.
O módulo angular-route permite a utilização de apenas uma diretiva ng-view no template de layout.
Código JavaScript do aplicativo
O código JavaScript do aplicativo PhoneCat muda bastante em relação ao Passo 7.
O arquivo app.js passa a ter o seguinte conteúdo:
'use strict';
angular.module('phonecat', ['ngRoute', 'moduloTelefone'])
.config(function($routeProvider){
$routeProvider
.when('/telefones', {
templateUrl: 'telefones/lista.html',
controller: 'TelefonesListaController'
})
.when('/telefones/:id', {
templateUrl: 'telefones/detalhes.html',
controller: 'TelefonesDetalhesController'
})
.otherwise({
redirectTo: '/telefones'
});
});
Dependências
A primeira novidade está em relação às dependências (segundo parâmetro da função module()):
angular.module('phonecat', ['ngRoute', 'moduloTelefone'])
Anteriormente, o aplicativo não possuía dependências, agora, são duas:
ngRoute(pacote/móduloangular-route)moduloTelefone(definido no arquivotelefones/modulo.js)
Função config() e rotas
O objeto criado pela função angular.module() possui a função config(), que é utilizada para realizar tarefas de configuração do módulo, que executam no momento em que o módulo é carregado. Neste caso, é injetado na função config() o objeto $routeProvider, que representa uma API de acesso ao módulo angular-route para, principalmente, definir rotas.
No código da função config() o objeto $routeProvider é utilizado para criar rotas. Há duas rotas:
/telefones/telefones/:id
Como já informado, uma rota é um padrão. Assim, quando o padrão definido por uma rota estiver presente na URL, o módulo angular-route entrará em ação para definir o que acontecerá. Por exemplo, a URL:
http://localhost/#/telefones
atende a rota /telefones. De forma semelhante, a URL:
http://localhost/#/telefones/galaxy
atende a rota /telefones/:id.
Por definição, isso é semelhante a um evento de troca da URL, ou seja, . Por isso o objeto $routeProvider fornece a função when() que recebe dois parâmetros:
- o padrão da rota na URL
- um objeto que configura a vinculação entre a rota, template parcial e controller
A rota /telefones é definida por:
.when('/telefones', {
templateUrl: 'telefones/lista.html',
controller: 'TelefonesListaController'
})
O segundo parâmetro da função when() indica que, quando a rota atual for /telefones, será utilizado o template parcial definido no arquivo telefones/lista.html e o controller TelefonesListaController.
A rota /telefones/:id é definida por:
when('/telefones/:id', {
templateUrl: 'telefones/detalhes.html',
controller: 'TelefonesDetalhesController'
})
A rota /telefones/:id inclui um parâmetro de rota. Um parâmetro de rota representa uma parte da rota que pode ser substituída por um valor, o qual poderá ser tratado posteriormente no controller. O template parcial está definido no arquivo telefones/detalhes.html e o controller é TelefonesDetalhesController.
Por fim, o objeto $routeProvider fornece a função otherwise():
otherwise({
redirectTo: '/telefones'
})
A função otherwise() recebe como parâmetro um objeto que determina o que acontece quando nenhuma rota for encontrada. Neste caso, o módulo angular-route redirecionará para a rota /telefones.
Importante relembrar que esses templates parciais e controllers estão definidos no módulo Telefones, que será apresentado a seguir.
Módulo Telefones
O módulo Telefones é definido no arquivo /telefones/modulo.js e seu código é apresentado a seguir.
'use strict';
angular.module('moduloTelefone', [])
.controller('TelefonesListaController',
function($rootScope, $scope, $http, $location){
$rootScope.pageTitle = 'Telefones - PhoneCat';
$http.get('data/phones/phones.json').then(function(response){
$scope.telefones = response.data;
});
})
.controller('TelefonesDetalhesController',
function($rootScope, $scope, $http, $routeParams){
$http.get('data/phones/' + $routeParams.id + '.json').then(
function(response){
$scope.telefone = response.data;
$scope.telefone.imageUrl = $scope.telefone.images[0];
$rootScope.pageTitle = $scope.telefone.name + ' - Phonecat';
});
$scope.mostrarImagem = function(imagem) {
$scope.telefone.imageUrl = imagem;
};
});
O módulo declara dois controllers:
TelefonesListaController: implementa a funcionalidade de apresentar a lista de telefones; eTelefonesDetalhesController: implementa a funcionalidade de apresentar os detalhes de um telefone.
Estes módulos e os templates parciais associados serão apresentados a seguir.
Lista de telefones
Template parcial da lista de telefones
O template parcial da lista de telefones está definido no arquivo telefones/lista.html. Seu código é exatamente o mesmo de parte do template principal do Passo 7 (arquivo index.html) que representava a "tela" de lista de telefones. Por este motivo, o código não será apresentado novamente aqui.
Controller TelefonesListaController
O controller TelefonesListaController é definido da seguinte forma:
.controller('TelefonesListaController',
function($rootScope, $scope, $http){
$rootScope.pageTitle = 'Telefones - PhoneCat';
$http.get('data/phones/phones.json').then(function(response){
$scope.telefones = response.data;
});
})
Na função que define o controller são injetados três objetos:
$rootScope$scope$http
Dentre estes, os objetos $scope e $http já são conhecidos. O objeto $rootScope permite acessar o escopo raiz do aplicativo. Na prática, é semelhante ao $scope (ou seja, permite acessar o model) tendo o escopo como única diferença. Neste caso, $rootScope é utilizado para alterar o título da página, modificando a propriedade pageTitle do model:
$rootScope.pageTitle = 'Telefones - PhoneCat';
O restante do controller é idêntico ao definido no Passo 7 e não será detalhado aqui.
Detalhes de um telefone
Template parcial dos detalhes de um telefone
O template parcial dos detalhes de um telefone está definido no arquivo telefones/detalhes.html. Seu código é exatamente o mesmo de parte do template principal do Passo 7 (arquivo index.html) que representava a "tela" de detalhes de um telefone. Por este motivo, o código não será apresentado novamente aqui.
Controller TelefoneDetalhesController
O controller TelefonesDetalhesController é definido da seguinte forma:
.controller('TelefonesDetalhesController',
function($rootScope, $scope, $http, $routeParams){
$http.get('data/phones/' + $routeParams.id + '.json').then(
function(response){
$scope.telefone = response.data;
$scope.telefone.imageUrl = $scope.telefone.images[0];
$rootScope.pageTitle = $scope.telefone.name + ' - Phonecat';
});
$scope.mostrarImagem = function(imagem) {
$scope.telefone.imageUrl = imagem;
};
});
Na função que define o controller são injetados quatro objetos:
$rootScope$scope$http$routeParams
Dentre estes objetos, o que precisa de destaque é $routeParams, que é fornecido pelo módulo angular-route.
Como já informado, uma rota pode possuir um parâmetro de rota, que é definido seguindo a sintaxe: sinal de dois pontos seguido pelo nome do parâmetro. Assim, a rota /telefones/:id possui um parâmetro chamado id. Para ter acesso ao parâmetro de rota, é utilizado o objeto $routeParams. O controller TelefonesDetalhesController o utiliza para esssa função:
$http.get('data/phones/' + $routeParams.id + '.json')
O objeto $rootScope é utilizado novamente para definir o valor da propriedade pageTitle. Desta vez, o objetivo é fazer com o que o título da janela apresente o nome do telefone. Isso está presente na função callback da função $http.get():
$http.get('data/phones/' + $routeParams.id + '.json').then(
function(response){
$scope.telefone = response.data;
$scope.telefone.imageUrl = $scope.telefone.images[0];
$rootScope.pageTitle = $scope.telefone.name + ' - Phonecat';
});
O restante do controller é semelhante ao já apresentado, por isso os detalhes do código, bem como sua reprodução, serão omitidos.
Conclusões
O Passo 8 implementa várias mudanças no aplicativo. Primeiro, a arquitetura do software foi modificada por meio da modularização, a qual impacta também a organização dos arquivos do projeto. Segundo, o recurso de rotas permitiu uma nova visão sobre como mudar "telas" do aplicativo.
Note Exercício
Estenda o Passo 8, criando funcionalidades que, implementando o módulo Fabricantes, permitam ao usuário:
- Ver a lista de fabricantes de telefones celulares (ex: rota
/fabricantes) - Ver os detalhes de um fabricante (tela de detalhes do fabricante) com nome, descrição e lista de telefones [do fabricante em questão]
- Filtrar a lista de telefones por fabricante (módulo Telefones)
Utilize arquivos .json para representar os dados dos fabricantes. Pode ser necessário alterar arquivos .json dos telefones para representar o "relacionamento" entre fabricante e telefone.