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.