3. O cliente Angular JS
3.1. Referências para o framework Angular JS
No início deste documento, foram fornecidas duas referências para o framework Angular JS. Listamo-las novamente aqui:
- [ref1]: o livro «Pro AngularJS», escrito por Adam Freeman e publicado pela Apress. É um livro excelente. O código-fonte dos exemplos deste livro está disponível gratuitamente no URL [http://www.apress.com/downloadable/download/sample/sample_id/1527/];
- [ref2]: a documentação oficial do Angular JS [https://docs.angularjs.org/guide];
O AngularJS merece um livro só para si. O livro de Adam Freeman tem mais de 600 páginas, e nenhuma delas é desperdiçada. Iremos descrever uma aplicação Angular e, ao longo dessa descrição, discutiremos os fundamentos desta estrutura. No entanto, limitar-nos-emos apenas às explicações necessárias para compreender a solução proposta. O Angular é um framework extremamente rico e existem muitas formas de alcançar o mesmo resultado. Isto pode ser um desafio porque, quando se está a começar, não se sabe se se está a utilizar uma solução que é melhor ou pior do que outra. É o caso da solução aqui apresentada. Poderia ter sido escrita de forma diferente e talvez utilizando melhores práticas.
3.2. Arquitetura do cliente Angular
A arquitetura do cliente Angular assemelha-se à de uma aplicação web MVC clássica, com algumas diferenças. Uma aplicação web Spring MVC, por exemplo, tem a seguinte arquitetura:
![]() |
O processamento de um pedido do cliente decorre da seguinte forma:
- solicitação - os URLs solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... O [Dispatcher Servlet] é a classe Spring que lida com os URLs recebidos. Ele «encaminha» o URL para a ação que deve tratá-lo. Estas ações são métodos de classes específicas chamadas [Controllers]. O C em MVC, neste contexto, é a cadeia [Dispatcher Servlet, Controller, Action]. Se nenhuma ação tiver sido configurada para tratar a URL recebida, o [Dispatcher Servlet] responderá que a URL solicitada não foi encontrada (erro 404 NOT FOUND);
- o processamento
- a ação selecionada pode utilizar os parâmetros que o [Servlet Dispatcher] lhe passou. Estes podem provir de várias fontes:
- o caminho [/param1/param2/...] da URL,
- os parâmetros da URL [p1=v1&p2=v2],
- dos parâmetros enviados pelo navegador com o seu pedido;
- ao processar a solicitação do utilizador, a ação pode necessitar da camada [de negócios] [2b]. Uma vez processada a solicitação do cliente, ela pode desencadear várias respostas. Um exemplo clássico é:
- uma página de erro, se a solicitação não puder ser processada corretamente
- uma página de confirmação, caso contrário
- a ação instrui que uma vista específica seja exibida [3]. Esta vista exibirá dados conhecidos como o modelo de vista. Este é o M em MVC. A ação criará este modelo M [2c] e instruirá que uma vista V seja exibida [3];
- resposta - a vista V selecionada utiliza o modelo M construído pela ação para inicializar as partes dinâmicas da resposta HTML que deve enviar ao cliente e, em seguida, envia essa resposta.
A arquitetura do nosso cliente Angular será semelhante, com terminologia ligeiramente diferente. Em primeiro lugar, as aplicações Angular são geralmente aplicações web de página única (SPAs):

- o utilizador solicita o URL inicial da aplicação no formato: http://machine:port/contexte. O navegador consulta um servidor web para recuperar o documento solicitado. Trata-se de uma página HTML com estilo CSS e dinamizada por JavaScript;
- depois, o utilizador interage com as vistas que lhe são apresentadas. Podemos distinguir vários tipos de interações:
- aquelas que não requerem qualquer interação com fontes externas, tais como ocultar ou mostrar elementos da vista. Estas são tratadas por JavaScript incorporado;
- aquelas que requerem dados de um serviço web remoto. Estes dados serão recuperados através de um pedido AJAX (Asynchronous JavaScript and XML), será construído um modelo e será apresentada uma vista;
- aquelas que requerem uma vista diferente da vista inicial. Será solicitada através de uma chamada AJAX ao servidor que serviu a página inicial. Em seguida, o processo anterior repetir-se-á. A página resultante será armazenada em cache no navegador. Na próxima solicitação, não será obtida do servidor HTML remoto;
Em última análise, o navegador efetua apenas uma única solicitação HTTP — aquela que carrega a página inicial. As solicitações HTTP subsequentes ao servidor da página HTML ou a serviços web remotos são feitas pelo JavaScript incorporado nas páginas.
Vamos agora apresentar a arquitetura da aplicação dentro do navegador. Ignoraremos o servidor HTML que fornece as páginas HTML da aplicação. Para facilitar a explicação, podemos assumir que todas elas estão presentes na cache do navegador.
![]() |
Primeiro, precisamos de situar esta arquitetura:
- em [1], estamos num navegador;
- em [2], um utilizador interage com as vistas apresentadas pelo navegador;
- em [3], os dados são recuperados da rede, frequentemente a partir de serviços web;
O utilizador interage com as vistas: preenche formulários e submete-os. Vamos explicar este processo utilizando a vista V1 acima. Vamos assumir que esta é a vista inicial da aplicação. Foi obtida da seguinte forma:
- o utilizador solicita o URL inicial da aplicação no formato: http://machine:port/contexte;
- o navegador solicitou o documento associado a esta URL. Recebeu a página HTML/CSS/JS para a vista V1;
- o JavaScript incorporado na página assumiu então o controlo e passou-o ao controlador C1 [5];
- o controlador construiu o modelo M1 [8] [9] para a vista V1. A construção deste modelo pode ter exigido a utilização de serviços internos [6] e a consulta de serviços externos [7];
O utilizador tem agora uma vista V1 à sua frente. Imaginemos que se trata de um formulário. Preenche-o e, em seguida, submete-o:
- em [4], o utilizador submete o formulário;
- em [5], este evento será tratado por um dos métodos do controlador C1;
Se o evento resultar apenas numa simples alteração na vista V1 (ocultar/mostrar campos), o controlador C1 modificará o modelo M1 da vista V1 e, em seguida, exibirá a vista V1 novamente. Para fazer isso, poderá ser necessário um dos serviços da camada [serviços] [6].
Se o evento exigir dados externos:
- em [6], o controlador C1 solicitará à camada [DAO] que os recupere;
- em [7], a camada [DAO] fará uma ou mais chamadas AJAX para os recuperar;
- em [8] e [9], o modelo M1 será modificado e a vista V1 exibida;
Se o evento desencadear uma alteração de vista, em ambos os casos anteriores, em vez de exibir a vista V1, o controlador C1 solicitará um novo URL [10]. Trata-se de um URL interno no navegador. Não resulta imediatamente numa solicitação HTTP ao servidor da página HTML. Esta alteração de URL é tratada por um router configurado de forma a que cada URL interna corresponda a uma vista V e ao seu controlador C. O router exibe então a nova vista Vn. Antes da exibição, o seu controlador Cn assume o controlo, constrói o modelo Mn e, em seguida, exibe a vista Vn [11]. Se a página HTML para a vista Vn não estiver armazenada em cache no navegador, será solicitada ao servidor de páginas HTML.
A camada [Apresentação] desta arquitetura é semelhante à arquitetura JSF (Java Server Faces):
- a vista V corresponde à vista Facelet do JSF;
- o controlador C corresponde ao bean JSF, uma classe Java que contém tanto o modelo M da vista V como os seus manipuladores de eventos;
A camada [Serviços] difere das camadas [Serviços] a que estamos habituados. No desenvolvimento web do lado do servidor, temos na maioria das vezes a seguinte arquitetura em camadas:
![]() |
No diagrama acima, a camada [web] comunica com a camada [DAO] apenas através da camada [business]. Nada nos impediria de injetar uma referência à camada [DAO] na camada [web] para permitir esta comunicação. Mas evitamos fazê-lo.
Com o Angular, não nos limitamos. A arquitetura passa então a ser a seguinte:
![]() |
- em [1], a camada [de apresentação] pode comunicar diretamente com qualquer serviço;
- em [2], os serviços têm conhecimento uns dos outros. Um serviço pode utilizar um ou mais outros serviços.
3.3. As vistas do cliente Angular
As vistas do cliente Angular já foram apresentadas na Secção 1.3.3. Para facilitar a compreensão deste novo capítulo, repetimo-las aqui. A primeira vista é a seguinte:
![]() |
- [6], a página de início de sessão da aplicação. Trata-se de uma aplicação de agendamento de consultas médicas;
- em [7], uma caixa de seleção que permite ao utilizador ativar ou desativar o modo [debug]. Este modo é indicado pela presença do painel [8], que exibe o modelo da vista atual;
- em [9], um tempo de espera artificial em milissegundos. O valor padrão é 0 (sem espera). Se N for o valor deste tempo de espera, qualquer ação do utilizador será executada após um tempo de espera de N milissegundos. Isto permite-lhe ver a gestão de espera implementada pela aplicação;
- em [10], o URL do servidor Spring 4. Com base no que precedeu, este é [http://localhost:8080];
- em [11] e [12], o nome de utilizador e a palavra-passe do utilizador que pretende utilizar a aplicação. Existem dois utilizadores: admin/admin (login/palavra-passe) com uma função (ADMIN) e user/user com uma função (USER). Apenas a função ADMIN tem permissão para utilizar a aplicação. A função USER é incluída exclusivamente para demonstrar a resposta do servidor neste caso de utilização;
- em [13], o botão que permite ligar-se ao servidor;
- em [14], o idioma da aplicação. Existem dois: francês (padrão) e inglês.
![]() |
- em [1], faz o login;
![]() |
- depois de iniciar sessão, pode escolher o médico com quem deseja marcar uma consulta [2] e a data da consulta [3];
- Em [4], solicita a visualização da agenda do médico selecionado para o dia escolhido;
![]() |
- Assim que a agenda do médico for apresentada, pode reservar um horário [5];
![]() |
- Em [6], selecione o paciente para a consulta e confirme a sua seleção em [7];
![]() |
Assim que a consulta for confirmada, será automaticamente redirecionado para a agenda, onde a nova consulta já estará listada. Esta consulta pode ser eliminada posteriormente [7].
As principais funcionalidades já foram descritas. São simples. As que não foram descritas são funções de navegação para regressar a uma visualização anterior. Vamos concluir com as definições de idioma:
![]() |
- em [1], muda-se do francês para o inglês;

- em [2], a visualização muda para inglês, incluindo o calendário;
3.4. Configuração do projeto Angular
Vamos construir o nosso cliente Angular passo a passo. Estamos a utilizar o IDE WebStorm.
Vamos criar uma pasta vazia [rdvmedecins-angular-v1] e, em seguida, abri-la com o WebStorm:
![]() |
- em [1], abra uma pasta;
- Em [2], selecionamos a pasta que criámos;
- em [3], obtemos um projeto WebStorm vazio;
![]() |
- em [4], configuramos o projeto através da opção [Arquivo / Configurações];
- em [5] e [6], configuramos a propriedade [Ortografia], que gere a verificação ortográfica. Por predefinição, esta está ativada. Uma vez que o software descarregado está em inglês, os nossos comentários em francês nos programas serão sublinhados como potenciais erros ortográficos. Por isso, desativamos esta verificação ortográfica [7];
![]() |
- Em [8], crie um novo ficheiro;
- Em [9], optamos por criar o ficheiro [package.json], que descreve a aplicação utilizando a sintaxe JSON;
- em [10], o ficheiro gerado é modificado conforme mostrado em [11];
- em [12], guarde este ficheiro tanto em [package.json] como em [bower.json];
![]() |
- Em [13], reconfigure o projeto;
![]() |
- em [14], configure a propriedade [Javascript / Bower], que nos permitirá declarar as bibliotecas JavaScript de que precisamos;
- em [15], especifique o ficheiro [bower.json] que acabámos de criar;
![]() |
- em [16], adicione uma biblioteca JavaScript;
- em [17], são apresentadas todas as bibliotecas JavaScript disponíveis para download;
- Em [18], podemos introduzir um termo para filtrar a lista [17]. Aqui, especificamos que queremos a biblioteca [Angular JS];
- em [19], aparecem os detalhes da biblioteca. Aqui vemos que a versão 1.2.18 do Angular será descarregada;
- em [20], fazemos o download;
![]() |
- em [21], vemos que foi descarregada;
- em [22], vemos a versão descarregada. Na verdade, é a 1.2.19;
- em [23], vemos a versão mais recente disponível;
![]() |
- em [24], seguindo o mesmo procedimento de antes, descarregamos as seguintes bibliotecas:
para codificar a cadeia de caracteres "user:password" em Base64; | ||
para internacionalizar o calendário | ||
para encaminhar os URLs internos da aplicação para o controlador e a vista corretos; | ||
permite a internacionalização das vistas. É um projeto independente do Angular. Aqui, serão utilizadas duas línguas: francês e inglês; | ||
fornece componentes visuais compatíveis com o Bootstrap. Iremos utilizar o seu calendário aqui; | ||
a estrutura CSS Bootstrap. Será utilizada para construir as visualizações; | ||
fornece um componente visual do tipo «tabela». É «responsivo» no sentido de que se adapta ao tamanho do ecrã; | ||
fornece um componente de «lista suspensa»; |
![]() |
- Em [25], as bibliotecas descarregadas foram instaladas na pasta [bower_components];
- em [26], vemos que a biblioteca jQuery foi descarregada. Isto deve-se ao facto de o Bootstrap a utilizar. O sistema de instalação de dependências JavaScript num projeto é análogo ao Maven no mundo Java: se uma biblioteca descarregada tiver as suas próprias dependências, essas dependências são descarregadas automaticamente;
O ficheiro [bower.json] foi alterado:
Todas as dependências descarregadas foram listadas no ficheiro.
3.5. A página inicial do cliente Angular
Criamos uma versão inicial da página inicial do cliente Angular:
![]() |
- em [1] e [2], criamos um ficheiro HTML chamado [app-01] [3] e [4];
O ficheiro [app-01.html] servirá como nossa página principal por enquanto. Vamos configurar a importação dos ficheiros CSS e JS necessários para a aplicação:
<!DOCTYPE html>
<html>
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tahé">
<!-- on CSS -->
<link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
- linhas 11-12: os ficheiros CSS para o Bootstrap;
- linha 13: o ficheiro CSS para o componente [boostrap-select];
- linha 14: o ficheiro CSS para o componente [footable];
- linhas 21-24: os ficheiros JS para os componentes Bootstrap;
- linha 21: os componentes Bootstrap são alimentados por jQuery;
- linha 22: o ficheiro JS do Bootstrap;
- linha 23: o ficheiro JS para o componente [boostrap-select];
- linha 24: o ficheiro JS para o componente [footable];
- linhas 26–30: os ficheiros JS para o Angular e projetos relacionados;
- linha 26: o ficheiro JS do Angular. Deve ser carregado após o jQuery, caso essa biblioteca seja utilizada;
- linha 27: o ficheiro JS para o projeto [angular-ui-bootstrap];
- linha 28: o ficheiro JS para o router [angular-route];
- linha 29: o ficheiro JS para o módulo de internacionalização da aplicação Angular;
- linha 30: o ficheiro JS para o módulo [angular-base64];
A validade do ficheiro [app-01.html] pode ser verificada:
![]() |
- em [1], solicitamos uma inspeção de código;
- em [2], o resultado quando tudo está correto;
Recomenda-se esta inspeção sistemática do código antes da execução. Aqui, esta verificação permite a deteção de quaisquer erros nas referências a ficheiros CSS e JS. Se um caminho estiver incorreto, o inspetor de código irá assinalá-lo.
- Em [3], a página pode ser carregada num navegador através de um depurador. O seguinte resultado é apresentado no navegador:
![]() |
- em [4], a página [app-01.html] foi servida por um servidor WebStorm interno a funcionar aqui na porta 63342;
- Em [5], a consola do depurador. Se tivessem ocorrido erros, estes teriam aparecido aqui. É também aqui que é apresentada a saída de ecrã gerada pela instrução JavaScript [console.log(expressão)]. Iremos utilizar amplamente esta funcionalidade;
O modo de depuração permite-lhe modificar a página no WebStorm e ver os resultados dessas alterações no navegador sem ter de recarregar a página. Portanto, se adicionarmos a linha 3 abaixo:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2>Version 1</h2>
</div>
e quando voltamos ao navegador, vemos que a página mudou:
![]() |
3.6. Introdução ao Bootstrap
Vamos agora ilustrar algumas das funcionalidades do Bootstrap utilizadas na aplicação. Tenho apenas um conhecimento limitado deste framework, adquirido através da cópia e colagem de código encontrado na Internet. Explicarei o papel das classes CSS que acredito compreender. Abster-me-ei de comentar as restantes.
3.6.1. Exemplo 1
No Angular, as operações que obtêm informações de fontes externas são assíncronas. Isto significa que a operação é iniciada e o controlo regressa imediatamente à vista, permitindo que o utilizador continue a interagir com ela. A aplicação é notificada de que a operação foi concluída através de um evento. Este evento é tratado por uma função JavaScript que pode, em seguida, atualizar ou alterar a vista atual. Se for provável que a operação demore muito tempo, é útil dar ao utilizador a opção de a cancelar. Ofereceremos esta opção de forma sistemática. Para tal, utilizaremos um banner Bootstrap:

Para alcançar este resultado, duplicamos [app-01.html] para [app-02.html] e modificamos as seguintes linhas:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="alert alert-warning">
<h1>Opération en cours. Veuillez patienter...
<button class="btn btn-primary pull-right">Annuler</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
</div>
- linha 1: a classe CSS [container] define uma área de exibição no navegador;
- linha 3: a classe CSS [alert] exibe uma área colorida. A classe [alert-warning] utiliza uma cor predefinida;
- linha 5: a classe [btn] define o estilo de um botão. A classe [btn-primary] atribui-lhe uma cor específica. A classe [pull-right] posiciona-o à direita do banner de alerta;
- linha 6: uma imagem animada de carregamento;
3.6.2. Exemplo 2
As diferentes vistas da aplicação terão um título comum:

Para o conseguir, duplicamos [app-01.html] para [app-03.html] e modificamos as seguintes linhas:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="assets/images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>Les Médecins associés</h1>
</div>
</div>
</div>
</div>
- A área colorida é criada utilizando a classe [jumbotron] na linha 4;
- Linha 5: A classe [row] define uma linha com 12 colunas;
- linha 6: a classe [col-md-2] define uma área de duas colunas dentro da linha;
- linha 7: uma imagem é colocada nessas duas colunas;
- Linhas 9–11: O texto é colocado nas restantes 10 colunas;
3.6.3. Exemplo 3
As visualizações terão uma barra de controlo superior. Esta conterá opções de controlo, links ou botões. Também conterá elementos de formulário. Por exemplo:
![]() |
Para obter este resultado, duplicamos [app-01.html] para [app-04.html] e modificamos as seguintes linhas:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<!-- debug mode -->
<label style="width: 100px">
<input type="checkbox">
<span style="color: white">Debug</span>
</label>
<!-- identification form -->
<div class="form-group">
<input type="text" class="form-control" placeholder="Temps d'attente"
style="width: 150px"/>
<input type="text" class="form-control" placeholder="URL du service web"
style="width: 200px"/>
<input type="text" class="form-control" placeholder="Login"
style="width: 100px"/>
<input type="password" class="form-control" placeholder="Mot de passe"
style="width: 100px"/>
</div>
<button class="btn btn-success">
Connexion
</button>
</form>
</div>
<button class="btn btn-success">
Connexion
</button>
</form>
</div>
</div>
</div>
</div>
- linha 4: a classe [navbar] define o estilo da barra de navegação. A classe [navbar-inverse] atribui-lhe um fundo preto. A classe [navbar-fixed-top] garante que, quando se percorre a página apresentada pelo navegador, a barra de navegação permanece na parte superior do ecrã;
- Linhas 6–14: definem a área [1]. Trata-se normalmente de uma série de classes que não compreendo. Utilizo o componente tal como está;
- linha 15: define uma área «responsiva» da barra de navegação. Num smartphone, esta área recolhe-se numa área de menu;
- linha 16: a classe [navbar-form] envolve um formulário na barra de comandos. A classe [navbar-right] posiciona-o à direita do formulário;
- linhas 23–32: os quatro campos de entrada do formulário da linha 17 [3]. Estão dentro de uma classe [form-group] que envolve os elementos de um formulário, e cada um deles tem a classe [form-control];
- linha 33: a classe [btn] que já vimos, complementada com a classe [btn-success], que lhe confere a cor verde;
3.6.4. Exemplo 4
A barra de controlo permitirá alterar o idioma utilizando uma lista suspensa:

Para o conseguir, duplicamos [app-01.html] para [app-05.html] e adicionamos as seguintes linhas à barra de controlo:
<button class="btn btn-success">
Connexion
</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger">
Langues
</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="">Français</a>
</li>
<li>
<a href="">English</a>
</li>
</ul>
</div>
</form>
As linhas adicionadas são as linhas 4–21.
- Linha 5: A classe [btn-group] envolve um grupo de botões. Existem dois deles nas linhas 6 e 9;
- Linhas 6–8: O primeiro botão define o rótulo da lista suspensa. A classe [btn-danger] atribui-lhe uma cor vermelha;
- linhas 9–12: o segundo botão é o botão da lista suspensa. Está colocado ao lado do primeiro, dando a impressão de um único componente;
- linha 10: exibe a seta para baixo, indicando que o botão é uma lista suspensa;
- linha 11: para leitores de ecrã;
- linhas 13–20: os itens da lista suspensa são os elementos de uma lista não ordenada;
3.6.5. Exemplo 5
Para enviar um formulário ou navegar, o utilizador terá opções ou botões na barra de controlo, conforme mostrado abaixo:
![]() |
Foram adicionadas opções de menu em [1]. Para tal, duplicamos [app-01.html] para [app-06.html] e adicionamos as seguintes linhas:
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
...
</div>
<!-- menu options -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span>Home</span>
</a>
</li>
<li class="active">
<a href="">
<span>Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span>Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span>Annuler</span>
</a>
</li>
</ul>
<!-- right buttons -->
<form class="navbar-form navbar-right" role="form">
...
</form>
</div>
</div>
</div>
</div>
- As opções do menu são geradas pelas linhas 8–29. Estas são, mais uma vez, elementos de uma lista <ul>. A classe [active] torna o texto sublinhado, indicando que a opção é clicável.
3.6.6. Exemplo 6
Vamos apresentar médicos e clientes em listas suspensas, conforme mostrado abaixo:
![]() |
A lista suspensa utilizada não é um componente nativo do Bootstrap. Trata-se do componente [bootstrap-select] (http://silviomoreto.github.io/bootstrap-select/). Para obter este resultado, duplicamos o ficheiro [app-01.html] para [app-07.html] e adicionamos as seguintes linhas:
<!DOCTYPE html>
<html>
<head>
...
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2><label for="medecins">Médecins</label></h2>
<select id="medecins" data-style="btn btn-primary" class="selectpicker">
<option value="1">Mme Marie PELISSIER</option>
<option value="1">Mr Jacques BROMARD</option>
<option value="1">Mr Philippe JANDOT</option>
<option value="1">Mme Justine JACQUEMOT</option>
</select>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- local script -->
<script>
$('.selectpicker').selectpicker();
</script>
</body>
</html>
- Linha 5: Deve importar a folha de estilos [bootstrap-select];
- linha 13: o atributo [data-style] é utilizado pelo [bootstrap-select]. É utilizado para definir o estilo da lista suspensa. Aqui, atribuímos-lhe a aparência de um botão azul [btn-primary];
- linha 13: o atributo [class] é utilizado na linha 23. Pode ser qualquer coisa;
- Linhas 14–17: os elementos da lista suspensa. Estas são tags HTML padrão;
- linha 22: o JS [bootstrap-select] deve ser importado;
- linhas 24–26: um script JavaScript executado quando a página termina de carregar;
- linha 25: uma instrução jQuery. Aplicamos o método [selectpicker] (selectpicker()) a todos os elementos com a classe [selectpicker] ($('.selectpicker')). Existe apenas um: a tag <select> na linha 13. O método [selectpicker] provém do ficheiro JS referenciado na linha 22;
3.6.7. Exemplo 7
Para apresentar a agenda de um médico, utilizaremos uma tabela responsiva fornecida pela biblioteca JS [footable]:
![]() |
- em [1]: a tabela com uma apresentação normal;
- em [2]: a tabela quando a janela do navegador é redimensionada. A coluna [Ação] passa automaticamente para a linha seguinte. Isto é chamado de componente «responsivo» ou simplesmente adaptativo.
Duplicamos [app-01.html] para [app-08.html] e adicionamos as seguintes linhas:
...
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
...
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="row alert alert-warning">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
...
<script src="bower_components/footable/dist/footable.min.js" type="text/javascript"></script>
- As linhas 2 e 60 já estão presentes em [app-01.html]. Estes são os ficheiros CSS e JS fornecidos pela biblioteca [footable];
- A linha 3 faz referência ao seguinte ficheiro CSS:
@CHARSET "UTF-8";
#creneaux th {
text-align: center;
}
#creneaux td {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
Os estilos [status-*] provêm de um exemplo de utilização da tabela [footable] disponível no site da biblioteca.
- linha 8: coloca a tabela numa linha [row] e numa caixa de alerta colorida [alert alert-warning];
- linha 9: a tabela ocupará 6 colunas [col-md-6];
- linha 10: a tabela HTML é formatada pelo Bootstrap [class='table'];
- linha 13: o atributo [data-toggle] especifica a coluna que contém o símbolo [+/-] que expande/colapsa a linha;
- linha 19: o atributo [data-hide='phone'] especifica que a coluna deve ser ocultada se o ecrã tiver o tamanho de um ecrã de telemóvel. O valor 'tablet' também pode ser utilizado;
3.6.8. Exemplo 8
Para ajudar o utilizador, vamos criar dicas de ferramenta em torno dos principais componentes das vistas:
![]() |
Para tal, duplicamos [app-01.html] para [app-09.html] e adicionamos as seguintes linhas:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<!-- menu options -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- local script -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>
As dicas de ferramenta são fornecidas pela biblioteca [angular-ui-bootstrap], que por sua vez depende da biblioteca [angular]. A linha 50 importa a biblioteca [angular-ui-bootstrap]. Para implementar os componentes da biblioteca [angular-ui-bootstrap], precisamos de criar um módulo Angular. Isto é feito nas linhas 52–55. Estas linhas definem um módulo Angular denominado [rdvmedecins] (primeiro parâmetro). Um módulo Angular pode utilizar outros módulos Angular. Estes são designados por dependências do módulo. São fornecidos numa matriz como segundo parâmetro da função [angular.module]. Aqui, o módulo denominado [ui.bootstrap] é fornecido pela biblioteca [angular-ui-bootstrap]. Este módulo irá fornecer-nos as dicas de ferramentas.
A linha 54 define um módulo Angular. Por predefinição, isto não tem qualquer efeito na página. Especificamos que a página deve ser gerida pelo Angular, associando-a a um módulo Angular. É isso que é feito na linha 2. O atributo [ng-app='rdvmedecins'] associa a página ao módulo criado na linha 54. A página será então analisada pelo Angular. Os atributos [tooltip] serão detetados e tratados pelo módulo [ui.bootstrap].
A sintaxe da dica de ferramenta é a seguinte:
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
Acima, adicionamos uma dica de ferramenta ao texto [Página inicial]:
- [tooltip]: define o texto da dica de ferramenta;
- [tooltip-placement]: define a sua posição (bottom, top, left, right);
O Angular JS permite adicionar novas tags ou atributos aos já existentes no HTML. Esta extensão do HTML é conseguida através das diretivas do Angular. Aqui, os atributos [tooltip] e [tooltip-placement] são criados pelo [angular-ui-bootstrap].
3.6.9. Exemplo 9
Para ajudar o utilizador a escolher a data de um compromisso, iremos disponibilizar um calendário:

Tal como acontece com as dicas de ferramenta, este calendário é fornecido pela biblioteca [angular-ui-bootstrap]. Para obter este resultado, duplicamos [app-01.html] para [app-10.html] e adicionamos as seguintes linhas:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
</div>
</div>
</div>
</div>
</div>
</div>
...
<!-- local script -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap'])
</script>
</body>
</html>
Tal como anteriormente, a página está associada a um módulo Angular (linhas 2 e 28). O calendário é definido pela tag <datepicker> na linha 16, fornecida pela biblioteca [angular-ui-bootstrap]:
- [show-weeks='true']: para exibir os números das semanas;
- [class='well']: para envolver o calendário numa caixa cinzenta com cantos arredondados;
- [ng-model='day']: os atributos [ng-*] são atributos do Angular. O atributo [ng-model] designa os dados que serão colocados no modelo de visualização. Quando o utilizador clica numa data, esta é colocada na variável [day] do modelo. Esta variável é utilizada na linha 10. A sintaxe {{expression}} avalia uma expressão composta por elementos do modelo. Aqui, {{day}} exibirá o valor da variável [day] do modelo. Uma característica fundamental do Angular é que a vista é atualizada automaticamente em resposta a alterações na variável [day]. Assim, quando o utilizador alterar as datas, essas alterações serão imediatamente exibidas na linha 10. De um modo geral, o processo funciona da seguinte forma:
- uma vista V está associada a um modelo M;
- o Angular observa o modelo M e atualiza automaticamente a vista V quando há uma alteração no seu modelo M;
A sintaxe {{day|date}} é chamada de filtro. Não é o valor de [day] que é exibido, mas o valor de [day] filtrado por um filtro chamado [date]. Este filtro está predefinido no Angular. É utilizado para formatar datas. Aceita parâmetros que especificam o formato desejado. Assim, a expressão {{day | date:'fullDate'}} indica que queremos o formato de data completo, neste caso [Friday, June 20, 2014], porque o calendário está em inglês por predefinição. Abordaremos a sua internacionalização em breve.
3.6.10. Conclusão
Apresentámos os elementos do framework CSS Bootstrap que iremos utilizar. Estes eram componentes passivos: os seus eventos não eram tratados. Por isso, clicar em botões ou links não fazia nada. Estes eventos serão tratados em JavaScript. É possível utilizar esta linguagem sem frameworks, mas tal como no lado do servidor, certos frameworks são essenciais no lado do cliente. É o caso do framework AngularJS, que traz uma nova abordagem ao desenvolvimento de aplicações JavaScript executadas por um navegador. Vamos apresentá-la agora.
3.7. Introdução ao Angular JS
Vamos agora ilustrar algumas das funcionalidades do framework Angular JS utilizado na aplicação. Já nos deparámos com algumas delas:
- uma página HTML é executada pelo Angular JS se tiver um módulo associado:
<html ng-app="rdvmedecins">
- O Angular permite criar novas tags e atributos HTML utilizando diretivas:
- O Angular permite criar filtros:
- Uma vista V apresenta um modelo M. O Angular monitoriza o modelo M e atualiza automaticamente a vista V sempre que há uma alteração no modelo M. O valor de uma variável no modelo M é apresentado na vista V utilizando:
Começaremos por aprofundar a implementação do padrão de design Modelo–Vista–Controlador no Angular. Vamos rever as relações entre eles de uma perspetiva arquitetónica:
![]() |
- A vista V1 apresenta o modelo M1 construído pelo controlador C1. O controlador C1 contém não só o modelo M1, mas também os manipuladores de eventos para a vista V1. Estamos nos ciclos 5, 8 e 9:
- [5]: Ocorre um evento na vista V1. Este é tratado pelo controlador C1;
- o controlador executa a sua tarefa [6-7] e, em seguida, constrói o modelo M1 [8];
- [9]: A vista V1 apresenta o novo modelo M1. Como mencionámos, este passo final é automático. Ao contrário de outras estruturas MVC, não há um envio explícito (C1 envia o modelo M1 para V1) nem uma receção explícita (a vista V1 obtém o modelo M1 de C1). Existe um envio implícito que o programador não vê;
- depois, o ciclo 5, 8, 9 recomeça;
3.7.1. Exemplo 1: O modelo MVC do Angular
Vamos revisitar o exemplo do calendário. Já vimos a diretiva que o gera:
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
Esta diretiva suporta outros atributos além dos mostrados acima, incluindo o atributo [min-date], que define a data mais antiga que pode ser selecionada no calendário. Isto será útil para nós. Quando o utilizador seleciona uma data de compromisso, esta deve ser igual ou posterior à data atual. Por isso, escreveremos:
<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>
onde [dateMin] será uma variável no modelo da página com um valor igual à data de hoje. Isto resultará na seguinte página:
![]() |
- em [1], estamos a 19 de junho de 2014. O cursor indica que 19 de junho pode ser selecionado;
- em [2], o cursor indica que 18 de junho não pode ser selecionado;
Duplicamos [app-10.html] para [app-11.html] e efetuamos as seguintes alterações:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
...
<!-- local script -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
// contrôleur
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// date minimale
$scope.minDate = new Date();
}]);
</script>
</body>
</html>
Vamos primeiro examinar o script local nas linhas 26–37:
- linha 28: criação do módulo [rdvmedecins] com a sua dependência do módulo [ui.bootstrap], que fornece o calendário;
- linhas 30–35: criação de um controlador. É este que irá conter o modelo da nossa página. Não haverá nenhum manipulador de eventos aqui;
- Linhas 30–31: O controlador [rdvMedecinsCtrl] pertence ao módulo [rdvmedecins]. Pode adicionar quantos controladores quiser a um módulo. Na nossa aplicação, teremos:
- um módulo de gestão da aplicação;
- um controlador por vista;
- o segundo parâmetro da função [controller] é uma matriz da forma ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)]. O último parâmetro é a função que implementa o controlador. Os seus parâmetros são objetos que o AngularJS fornecerá à função.
Voltemos à arquitetura de uma aplicação Angular:
![]() |
Acima, o controlador C1 contém todos os manipuladores de eventos para a vista V1, bem como o seu modelo M1. Os manipuladores de eventos podem necessitar de um ou mais serviços [6] para executar as suas tarefas. Passamos todos estes como parâmetros para a função construtora do controlador:
Os serviços Si são singletons. O Angular cria uma única instância de cada um. São identificados por um nome Si. Por que razão aparecem duas vezes na tabela acima? Em produção, os scripts JS são minificados. Durante este processo de minificação, a tabela acima passa a ser:
Os parâmetros perdem os seus nomes. No entanto, estes são os nomes dos serviços. Por isso, é importante preservar estes nomes. É por isso que são passados como strings como parâmetros que precedem a função. As strings não são alteradas durante o processo de minificação. Quando o Angular constrói o controlador utilizando o novo array, substituirá a1 por S1, a2 por S2 e assim por diante. A ordem dos parâmetros é, portanto, importante. Deve corresponder à ordem dos serviços que precedem a definição da função.
Voltemos à definição do controlador [rdvMedecinsCtrl]:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// minimum date
$scope.minDate = new Date();
}]);
- linhas 3-4: o único objeto injetado no controlador é o objeto $scope. Trata-se de um objeto predefinido que representa o modelo M das vistas associadas ao controlador. Para enriquecer o modelo de uma vista, basta adicionar campos ao objeto $scope;
- o que é feito na linha 6. Criamos o campo [minDate] com a data atual como seu valor;
A vista V utiliza este modelo M da seguinte forma:
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
...
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
...
</div>
...
- linha 1: o corpo da página está associado ao controlador [rdvMedecinsCtrl] através do atributo [ng-controller]. Isto significa que tudo o que estiver dentro da tag <body> utilizará o controlador [rdvMedecinsCtrl] para gerir os seus eventos e recuperar o seu modelo M. Uma página HTML pode depender de vários controladores, estejam eles aninhados uns nos outros ou não:
Acima:
- o conteúdo de [div1] (linhas 1–10) apresenta o modelo M1 gerido pelo controlador c1. As tags nesta área podem fazer referência a manipuladores de eventos do controlador c1;
- o conteúdo de [div11] (linhas 3-4) exibe o modelo M11 gerido pelo controlador c11, bem como o modelo M1. Existe herança de modelos. As tags nesta área podem referenciar tanto os manipuladores de eventos do controlador c11 como os do controlador c1. Não podem referenciar nem o modelo M12 do controlador c12 nem os seus manipuladores de eventos. O controlador c12 não está definido entre as linhas 3–5;
- linhas 7–9: podemos aplicar um raciocínio semelhante ao utilizado anteriormente;
Voltemos ao código do calendário:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
O atributo [min-date] é inicializado com o valor [minDate] do modelo. Implicitamente, trata-se de [$scope.minDate]. O campo é sempre consultado no objeto $scope.
3.7.2. Exemplo 2: Localização de datas
Por enquanto, o calendário não nos é muito útil, uma vez que está em inglês. É possível localizá-lo:
![]() |
- em [1], temos um calendário em francês;
- em [2], mudamos para inglês;
- em [3], o calendário em inglês;
Duplicamos a página [app-11.html] para [app-12.html] e, em seguida, modificamos esta última da seguinte forma:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- the calendar-->
<div class="col-md-4">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
<!-- languages -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins.js"></script>
</body>
</html>
Existem poucas alterações. A única adição são as linhas 21–31, que contêm a lista suspensa de idiomas. Pela primeira vez, encontramos um manipulador de eventos nas linhas 27–28:
- linha 27: o atributo [ng-click] é um atributo do Angular que especifica o manipulador de eventos a ser executado quando o elemento com este atributo for clicado. Aqui, a função [$scope.setLang('fr')] será executada. Ela definirá o calendário para francês;
- linha 28: aqui, definimos o calendário para inglês;
- linha 35: uma vez que o JavaScript do controlador é bastante extenso, colocamo-lo num ficheiro chamado [rdvmedecins.js];
O Angular gere a localização da visualização com um módulo chamado [ngLocale]. A definição do nosso módulo [rdvmedecins] será, portanto, a seguinte:
// --------------------- Angular module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);
Linha 2: Não se esqueça das dependências, pois as mensagens de erro do Angular podem, por vezes, ser vagas. A omissão de uma dependência é particularmente difícil de detetar. Aqui, temos uma nova dependência do módulo [ngLocale].
Por predefinição, o Angular apenas trata da localização de datas, números, etc., que têm variantes locais. Não trata da internacionalização de texto. Para isso, utilizaremos a biblioteca [angular-translate]. A localização é tratada pela biblioteca [angular-i18n]. Esta biblioteca inclui tantos ficheiros quantas as variantes existentes para datas, números, etc.
![]() |
Para o calendário francês, utilizaremos o ficheiro [angular-locale_fr-fr.js] e, para o calendário inglês, o ficheiro [angular-locale_en-us.js]. Vejamos, por exemplo, o que contém o ficheiro [angular-locale_fr-fr.js]:
'use strict';
angular.module("ngLocale", [], ["$provide", function($provide) {
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
$provide.value("$locale", {
"DATETIME_FORMATS": {
"AMPMS": [
"AM",
"PM"
],
"DAY": [
"dimanche",
"lundi",
"mardi",
"mercredi",
"jeudi",
"vendredi",
"samedi"
],
"MONTH": [
"janvier",
"f\u00e9vrier",
"mars",
"avril",
"mai",
"juin",
"juillet",
"ao\u00fbt",
"septembre",
"octobre",
"novembre",
"d\u00e9cembre"
],
"SHORTDAY": [
"dim.",
"lun.",
"mar.",
"mer.",
"jeu.",
"ven.",
"sam."
],
"SHORTMONTH": [
"janv.",
"f\u00e9vr.",
"mars",
"avr.",
"mai",
"juin",
"juil.",
"ao\u00fbt",
"sept.",
"oct.",
"nov.",
"d\u00e9c."
],
"fullDate": "EEEE d MMMM y",
"longDate": "d MMMM y",
"medium": "d MMM y HH:mm:ss",
"mediumDate": "d MMM y",
"mediumTime": "HH:mm:ss",
"short": "dd/MM/yy HH:mm",
"shortDate": "dd/MM/yy",
"shortTime": "HH:mm"
},
"NUMBER_FORMATS": {
"CURRENCY_SYM": "\u20ac",
"DECIMAL_SEP": ",",
"GROUP_SEP": "\u00a0",
"PATTERNS": [
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 3,
"minFrac": 0,
"minInt": 1,
"negPre": "-",
"negSuf": "",
"posPre": "",
"posSuf": ""
},
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 2,
"minFrac": 2,
"minInt": 1,
"negPre": "(",
"negSuf": "\u00a0\u00a4)",
"posPre": "",
"posSuf": "\u00a0\u00a4"
}
]
},
"id": "fr-fr",
"pluralCat": function (n) { if (n >= 0 && n <= 2 && n != 2) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
});
}]);
Aqui estão os elementos utilizados para criar um calendário francês:
- linhas 10–18: a matriz dos dias da semana;
- linhas 19–32: a matriz dos meses do ano;
- linhas 33–41: a tabela abreviada dos dias da semana;
- linhas 42–55: a tabela de meses abreviados do ano;
- linhas 56–63: formatos de data e hora. A linha 62 mostra o formato «dd/mm/aa» utilizado para datas em francês;
- linhas 65–95: informações sobre formatação de números. Isto não é relevante aqui;
- linha 96: o identificador «fr-fr» para a localização do ficheiro (fr-fr: francês da França, fr-ca: francês do Canadá, ...)
No ficheiro [angular-locale_en-us.js], temos exatamente o mesmo, mas desta vez para o inglês dos EUA (en-us).
O código acima não é muito fácil de ler. Se o ler com atenção, verá que todo este código define a variável [$locale] na linha 4. É alterando o valor desta variável que conseguimos a internacionalização de datas, números, moeda, etc. Curiosamente, o Angular não permite alterar a variável [$locale] em tempo de execução. Define-a de uma vez por todas importando o ficheiro para a localização desejada:
<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>
Não faz sentido importar todos os ficheiros para as localizações desejadas, porque, como vimos, cada ficheiro faz apenas uma coisa: define a variável [$locale]. O último ficheiro importado tem precedência e não há forma de alterar a localização posteriormente.
Ao pesquisar na Internet por uma solução para este problema, não consegui encontrar nenhuma. Proponho aqui uma [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. A ideia é colocar as diferentes localizações de que precisamos num dicionário. É aí que as iremos recuperar quando precisarmos de as alterar. O código JavaScript em [rdvmedecins.js] tem a seguinte estrutura:
![]() |
Se removemos as definições de local, que ocupam 200 linhas (linhas 15–215 acima), o código é simples:
- linha 6: define o módulo [rdvmedecins] e as suas dependências;
- linhas 8–10: define o controlador [rdvMedecinsCtrl] da página;
- linha 9: o construtor do controlador recebe dois parâmetros:
- $scope: para criar o modelo de visualização;
- $locale: é a variável que controla a configuração regional do calendário. É esta variável que deve alterar ao mudar de idioma;
- linha 13: a variável do modelo [minDate] é inicializada com a data de hoje;
- linha 15: define o dicionário [locales]. Note que não escrevemos [$scope.locales]. A variável [locales] não faz parte do modelo exposto à vista;
- linhas 15–215: definem um dicionário {'fr':locale-fr-fr, 'en':locale-en-us}. Os valores [locale-fr-fr] e [locale-en-us] são retirados dos ficheiros JS [angular-locale_fr-fr.js] e [angular-locale_en-us.js], respetivamente. A parte mais difícil é não cometer erros com os inúmeros parênteses neste dicionário...
- linha 217: inicializamos a variável $locale com locales['fr'], ou seja, a versão francesa da localização. Não podemos simplesmente escrever [$locale=locales['fr']] porque isso atribuiria o endereço de locales['fr'] a $locale. Temos de realizar uma cópia do valor. Isto pode ser feito utilizando a função predefinida [angular.copy];
- linha 219: a variável [day] no modelo é inicializada com a data de hoje. Isto garante que o calendário será exibido com a data definida para hoje;
- Linhas 223–230: definem o manipulador de eventos que é chamado quando o idioma muda. Observe a sintaxe:
para definir um manipulador de eventos chamado [nome_da_função] que aceita os parâmetros [param1, param2, ...];
Vamos rever o código HTML da lista suspensa:
<!-- languages -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
- linha 8: selecionar o francês aciona a chamada para [setLang('fr')];
- linha 9: selecionar Inglês aciona a chamada para [setLang('en')];
- linha 3: o atributo [is-open] é um valor booleano que controla se a lista suspensa está aberta (true) ou fechada (false). É inicializado com a variável [isopen] do modelo de visualização;
Voltemos ao código em [rdvmedecins.js]:
- linha 225: alteramos o valor da variável [$locale] para o valor apropriado do dicionário [locales];
- linha 227: mencionámos que, quando o modelo M de uma vista V muda, a vista V é automaticamente atualizada com o novo modelo. Na linha 225, alterámos o valor da variável [$locale], que não faz parte do modelo M exibido pela vista V. Precisamos de encontrar uma forma de atualizar este modelo M para que o calendário seja atualizado e utilize a sua nova localização. Aqui, alteramos a variável [day] no modelo do calendário. Inicializamo-la com um novo ponteiro (new) que aponta para uma data idêntica à que está atualmente a ser exibida. [$scope.day.getTime()] é o número de milissegundos decorridos entre 1 de janeiro de 1970 e a data exibida pelo calendário. Utilizando este número, reconstruímos uma nova data. É claro que obteremos a mesma data, e o calendário permanecerá posicionado na data que estava a exibir. Mas o valor de [$scope.day], que é na verdade um ponteiro, terá mudado, e o calendário será atualizado;
- linha 229: definimos o valor da variável [isopen] no modelo como false. Esta variável controla um dos atributos da lista suspensa:
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
...
</div>
Na linha 1 acima, o atributo [is-open] mudará para false, o que fechará a lista suspensa.
3.7.3. Exemplo 3: Internacionalização de texto
Vamos rever a localização do calendário:
![]() |
Em [3], vemos que o calendário está em inglês, mas os textos [Calendar, Languages] não estão. Por predefinição, o Angular não fornece uma ferramenta para internacionalizar mensagens. Aqui, iremos utilizar a biblioteca [angular-translate] (https://github.com/angular-translate/angular-translate).
Vamos desenvolver o seguinte exemplo:
![]() |
- em [1], a versão em francês;
- em [2], a visualização em inglês;
Vejamos a configuração necessária para a internacionalização. O script [rdvmedecins.js] é modificado da seguinte forma:
// --------------------- Angular module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// configuration i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// messages français
$translateProvider.translations("fr", {
'msg_header': 'Cabinet Médical<br/>Les Médecins Associés',
'msg_langues': 'Langues',
'msg_agenda': 'Agenda de {{titre}} {{prenom}} {{nom}}<br/>le {{jour}}',
'msg_calendrier': 'Calendrier',
'msg_jour': 'Jour sélectionné : ',
'msg_meteo': "Aujourd'hui, il va pleuvoir..."
});
// messages anglais
$translateProvider.translations("en", {
'msg_header': 'The Associated Doctors',
'msg_langues': 'Languages',
'msg_agenda': "{{titre}} {{prenom}} {{nom}}'s Diary<br/> on {{jour}}",
'msg_calendrier': 'Calendar',
'msg_jour': 'Selected day: ',
'msg_meteo': 'Today, it will be raining...'
});
// langue par défaut
$translateProvider.preferredLanguage("fr");
}]);
- linha 2: a primeira alteração é a adição de uma nova dependência. A internacionalização da aplicação requer o módulo Angular [pascalprecht.translate];
- linhas 5–26: definem a função [config] do módulo [rdvmedecins]. Quando uma aplicação Angular é iniciada, o framework instancia todos os serviços necessários à aplicação, incluindo os serviços predefinidos do Angular e os serviços definidos pelo utilizador. Por enquanto, não definimos nenhum serviço. A função [config] do módulo de uma aplicação é executada antes de qualquer serviço ser instanciado. Pode ser utilizada para definir informações de configuração para os serviços que serão posteriormente instanciados. Aqui, a função [config] será utilizada para definir as mensagens internacionalizadas da aplicação;
- linha 5: o parâmetro da função [config] é uma matriz ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)], em que Oi é um objeto conhecido fornecido pelo Angular. Aqui, o objeto [$translateProvider] é fornecido pelo módulo [pascalprecht.translate]. [function] é a função executada para configurar a aplicação;
- linhas 7–14: a função [$translateProvider.translations] recebe dois parâmetros:
- o primeiro parâmetro é a chave de um idioma. Pode usar o que quiser. Aqui, utilizámos «fr» para as traduções em francês (linha 7) e «en» para as traduções em inglês (linha 16),
- o segundo é a lista de traduções na forma de um dicionário {'chave1':'mensagem1', 'chave2':'mensagem2', ...};
- linhas 7–14: as mensagens em francês;
- linhas 16–23: as mensagens em inglês;
- linha 25: o método [preferredLanguage] define o idioma padrão. O seu parâmetro é um dos argumentos utilizados como primeiro parâmetro da função [$translateProvider.translations], pelo que aqui é «fr» (linha 7) ou «en» (linha 16);
- Note que existem três tipos de mensagens:
- mensagens sem parâmetros ou elementos HTML (linhas 9, 11, 12, ...),
- mensagens com elementos HTML (linhas 8, 10, ...),
- mensagens com parâmetros (linhas 10, 19);
Agora, duplicamos [app-11.html] para [app-12.html] e fazemos as seguintes alterações:
<div class="container">
<!-- a first text with HTML elements in it -->
<h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
<!-- a second text with parameters -->
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
<!-- a third text translated by the controller -->
<h3 class="alert alert-danger">{{msg2}}</h3>
<pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- the calendar-->
<div class="col-md-4">
<h4>{{'msg_calendrier'|translate}}</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
<!-- languages -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
{{'msg_langues'|translate}}<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
- As traduções ocorrem nas linhas 3, 5, 9, 13 e 23;
- existem três sintaxes distintas:
- a sintaxe [translate={{'msg_key'}}] (linha 3), em que [msg_key] é uma das chaves de um dicionário de tradução. Esta sintaxe é adequada para mensagens com ou sem elementos HTML, mas não para aquelas com parâmetros;
- a sintaxe [translate={{'msg_key'}} translate-values={{dictionary]}}] (linha 5), que é adequada para mensagens com ou sem elementos HTML e com parâmetros;
- a sintaxe [{{'msg_key'|translate}}] (linhas 9, 13, 23) é adequada para mensagens sem parâmetros e sem elementos HTML;
Vejamos as diferentes mensagens nesta vista:
Consultório Médico<br/>The Associated Doctors | The Associated Doctors | |
Calendário | Calendário | |
Idiomas | Idiomas | |
Dia selecionado: | Dia selecionado: |
Vamos agora examinar a linha 5:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Note que [msg.text] e [msg.model] não estão entre aspas simples. Não se trata de cadeias de caracteres, mas sim de elementos do modelo:
- msg.text: define a chave da mensagem configurada a ser utilizada;
- msg.model: é o dicionário que fornece os valores dos parâmetros;
Os nomes dos campos [text, model] podem ser quaisquer. No controlador [rdvMedecinsCtrl] da vista, o objeto [msg] é definido da seguinte forma:

- linha 245: a definição do objeto [msg];
- linha 245: o campo [text] tem o valor [msg_agenda], que está associado a dois valores:
- {{title}} Diário de {{first_name}} {{last_name}}<br/> em {{day}} no dicionário francês;
- {{title}} Diário de {{first_name}} {{last_name}}<br/> em {{day}} no dicionário inglês;
A mensagem a ser exibida tem, portanto, quatro parâmetros [title, first_name, last_name, day];
- Linha 245: O campo [model] é um dicionário que atribui valores a estes quatro parâmetros. Existe um problema com o parâmetro [day]. Pretendemos apresentar o nome completo do dia. Este varia consoante o idioma seja francês ou inglês. Por isso, usamos o filtro [date], que já foi utilizado na vista, na forma {{ day | date:'fullDate'}}. Qualquer filtro pode ser utilizado no código JavaScript na forma $filter('filter')(value, options), onde $filter é um objeto Angular predefinido e 'filter' é o nome do filtro;
- linhas 33–34: o objeto $filter predefinido é passado como parâmetro para o controlador, permitindo que seja utilizado na linha 245;
Voltemos a outra linha na vista apresentada:
<!-- un troisième texte traduit par le contrôleur -->
<h3 class="alert alert-danger">{{msg2}}</h3>
Todas as traduções anteriores foram realizadas na vista utilizando atributos do módulo [pascalprecht.translate]. Também podemos optar por realizar esta tradução no lado do servidor. É isso que se faz aqui. No controlador (linha 247 na captura de ecrã acima), temos o seguinte código:
$scope.msg2 = $filter('translate')('msg_meteo');
Utilizamos a mesma sintaxe que para o filtro «date», porque «translate» também é um filtro. Aqui, solicitamos a mensagem com a chave «msg_meteo».
Vamos examinar o mecanismo para as alterações de idioma. Vimos que a função [config] no módulo [rdvmedecins] tinha definido o francês como idioma predefinido (linha 9 abaixo):
// i18n configuration
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// french messages
$translateProvider.translations("fr", {...});
// english messages
$translateProvider.translations("en", {...});
// default language
$translateProvider.preferredLanguage("fr");
}]);
Note que a localização padrão também era o francês. Na inicialização do controlador [rdvmedecins], escrevemos:
// we put the locale in French
angular.copy(locales['fr'], $locale);
- linha 2: [locales] é um dicionário que criámos;
Não existe qualquer ligação entre a internacionalização de mensagens fornecida pelo módulo [pascalprecht.translate] e a localização de datas que implementámos. Esta última utiliza uma variável $locale que não é utilizada pelo módulo [pascalprecht.translate]. Trata-se de dois processos independentes um do outro.
Agora é hora de ver o que acontece quando o utilizador altera o idioma:

- linha 251: quando o idioma muda, a função [setLang] é chamada com um dos dois parâmetros ['fr','en'];
- linhas 252–257: já foram explicadas — alteram a variável [$locale] do calendário. Isto não tem qualquer efeito no idioma das traduções;
- linha 259: alteramos o idioma de tradução. Utilizamos o objeto [$translate] fornecido pelo módulo [pascalprecht.translate]. Para tal, precisamos de o injetar no controlador:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {
Nas linhas 3 e 4 acima, o objeto $translate é injetado;
- o parâmetro lang da função [$translate.use(lang)] deve ser definido como uma das chaves utilizadas na configuração como primeiro parâmetro da função [$translateProvider.translations], ou seja, «fr» ou «en». É precisamente o que acontece;
- Linha 261: Recalculamos o valor de msg2. Porquê? Na vista, após a alteração de idioma efetuada pela linha 259, todos os atributos [translate] existentes serão reavaliados. Este não será o caso da expressão {{msg2}}, que não possui este atributo. Por conseguinte, o seu novo valor é calculado no controlador. Isto deve ser feito após a alteração de idioma na linha 259, para que o novo idioma seja utilizado no cálculo de [msg2];
Se pararmos por aqui, observamos duas anomalias:
![]() |
- em [1], o dia permanece em francês, enquanto o resto da visualização está em inglês;
- em [2] e [3], a data selecionada é 24 de junho, enquanto em [1] a data permanece definida para 20 de junho;
Vamos tentar explicar estas questões antes de encontrar soluções. A mensagem [1] é construída no controlador com o seguinte código:
$scope.msg = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom': 'PELISSIER', 'jour': $filter('date')($scope.jour, 'fullDate')}};
e exibida na vista com o seguinte código:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
A anomalia [1] (o dia permaneceu em francês enquanto o resto da vista está em inglês) parece indicar que, embora o atributo [translate] seja reavaliado durante uma mudança de idioma, isso não aconteceu com o atributo [translate-values]. Podemos então forçar essa avaliação no controlador:
// ------------------- evts manager
// language change
$scope.setLang = function (lang) {
...
// update msg2
$scope.msg2 = $filter('translate')('msg_meteo');
// and msg day
$scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};
Sempre que o idioma muda, a linha 8 acima recalcula o dia apresentado. Isto resolve efetivamente o primeiro problema, mas não o segundo (o dia apresentado na mensagem não muda quando outro dia é selecionado no calendário). A razão para este comportamento é a seguinte. A mensagem é apresentada na vista com o seguinte código:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
A vista V apresentada só muda se o seu modelo M mudar. No entanto, neste caso, a seleção de um novo dia no calendário desencadeia um evento que não é tratado, o que significa que o modelo [msg] não muda e, por conseguinte, a vista também não muda. Atualizamos a definição do calendário na vista:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>
Acima, especificamos que o clique no calendário deve ser tratado pela função [$scope.calendarClick]. Esta função é a seguinte:

- linha 267: o manipulador de cliques no calendário;
- linha 269: forçamos a atualização do dia exibido utilizando a mensagem [msg];
3.7.4. Exemplo 4: Um serviço de configuração
Vamos rever a arquitetura de uma aplicação AngularJS:
![]() |
Aqui, vamos concentrar-nos no conceito de serviço. Trata-se de um conceito bastante abrangente. Embora a camada [DAO] acima seja claramente um serviço, qualquer objeto Angular pode tornar-se um serviço:
- um serviço segue uma sintaxe específica. Tem um nome, e o Angular identifica-o por esse nome;
- um serviço pode ser injetado pelo Angular em controladores e outros serviços;
Alguns dos serviços que iremos configurar no módulo [rdvmedecins] terão de ser configurados. Uma vez que um serviço pode ser injetado noutro serviço, é tentador realizar a configuração num serviço a que daremos o nome de [config] e injetá-lo nos serviços e controladores a configurar. Iremos agora descrever este processo.
Duplicamos [app-13.html] para [app-14.html] e fazemos as seguintes alterações:
<div class="container">
<!-- waiting msg control -->
<label>
<input type="checkbox" ng-model="waiting.visible">
<span>Voir le message d'attente</span>
</label>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
...
</div>
...
<script type="text/javascript" src="rdvmedecins-02.js"></script>
- Linhas 3–6: Uma caixa de seleção que controla se a mensagem de espera nas linhas 9–15 é exibida. O valor da caixa de seleção é armazenado na variável [waiting.visible] do modelo M da vista V. Este valor é verdadeiro se a caixa de seleção estiver marcada e falso caso contrário. Isto funciona nos dois sentidos. Se definirmos a variável [waiting.visible] como true, a caixa de seleção será marcada. Existe uma associação bidirecional entre a vista V e o seu modelo M;
- linhas 9–15: uma mensagem de espera com um botão para cancelar a espera (linha 11);
- linha 9: a mensagem só é visível se a variável [waiting.visible] tiver o valor true. Assim, quando marcamos a caixa de seleção na linha 4:
- o valor true é atribuído à variável [waiting.visible] (ng-model, linha 4);
- uma vez que houve uma alteração no modelo M, a vista V é automaticamente reavaliada. A mensagem de espera será então tornada visível (ng-show, linha 9);
- o raciocínio é semelhante ao desmarcar a caixa de seleção na linha 4: a mensagem de espera é ocultada;
- linha 10: a mensagem de espera é traduzida (filtro translate);
- linha 11: quando o botão é clicado, o método [waiting.cancel()] é executado (atributo ng-click);
- linha 12: o rótulo do botão é traduzido;
- linha 19: colocamos o código JavaScript da aplicação num novo ficheiro JS [rdvmedecins-02] para não perder o código que já foi escrito e que agora precisa de ser reorganizado;
Isto resulta na seguinte visualização:
![]() |
- em [1], caixa de seleção desmarcada;
- em [2], caixa de seleção marcada;
O script [rdvmedecins-02] é uma reorganização do script [rdvmedecins]:

- linha 6: o módulo [rdvmedecins] da aplicação;
- linhas 9-10: a função de configuração da aplicação;
- linhas 38-39: o serviço [config];
- linhas 283-284: o controlador [rdvMedecinsCtrl];
Anteriormente, tínhamos definido o dicionário locales={'fr':..., 'en': ...} no controlador, que tinha 200 linhas. Este dicionário é claramente um elemento de configuração, pelo que o estamos a mover para o serviço [config] nas linhas 38–39. Este serviço é definido da seguinte forma:

- Linhas 38-39: É criado um serviço utilizando a função [factory] do objeto [angular.module]. A sintaxe desta função é a mesma das anteriores: factory('service_name', ['O1', 'O2', ..., 'On', function (O1, O2, ..., On){...}]), em que os Oi são nomes de objetos conhecidos pelo Angular (pré-definidos ou criados pelo programador) que o Angular injeta como parâmetros na função factory. Uma vez que a função não tem parâmetros aqui, utilizámos uma sintaxe mais curta, mas igualmente válida: factory('nome_do_serviço', function (){...}]);
- linha 40: a função [factory] deve implementar o serviço utilizando um objeto que ela retorna. Este objeto é o serviço. É por isso que a função se chama factory (fábrica de criação de objetos);
Geralmente, o código do serviço assume a forma:
Angular.module('nom_module')
.factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
// service preparation
...
// render the object implementing the service
return {
// fields
...
// methods
...
}
});
- Linha 6: Devolvemos um objeto JavaScript que pode conter tanto campos como métodos. São os métodos que tratam do serviço;
Aqui, o serviço [config] define apenas campos e nenhum método. Colocaremos aqui tudo o que pode ser configurado na aplicação:
- linhas 42–47: as chaves para as mensagens a serem traduzidas;
- linhas 59–62: os URLs da aplicação;
- linhas 64–69: as URLs do serviço web remoto;
- linha 71: uma chamada HTTP para um serviço web que não responde pode demorar muito tempo. Aqui, definimos o tempo máximo de espera pela resposta do serviço web para 1 segundo. Após este tempo, a chamada HTTP falha e é lançada uma exceção JavaScript;
- linha 73: antes de cada chamada ao servidor, simularemos uma espera cuja duração é definida aqui em milissegundos. Uma espera de 0 significa que não há espera. A aplicação será concebida de forma a que o utilizador possa cancelar uma operação que tenha iniciado. Para que seja cancelável, deve durar pelo menos alguns segundos. Utilizaremos esta espera artificial para simular operações de longa duração;
- linha 75: no modo [debug=true], são exibidas informações adicionais na vista atual. Por predefinição, este modo está ativado. Em produção, definiríamos este campo como false;
- linhas 77–278: o dicionário para as duas configurações regionais, 'fr' e 'en'. Anteriormente, este encontrava-se no controlador [rdvMedecinsCtrl];
Com este serviço, o controlador [rdvMedecinsCtrl] evolui da seguinte forma:

- linhas 284-285: o serviço [config] é injetado no controlador;
- linha 290: o dicionário [locales] encontra-se agora no serviço [config] e já não no controlador;
- linha 294: o objeto [waiting] que controla a exibição da mensagem de espera. A chave para a mensagem de espera encontra-se no serviço [config] (campo text). Por predefinição, a mensagem de espera está oculta (campo visible). O campo cancel tem como valor o nome da função na linha 316. Este campo é, portanto, um método ou função;
- linha 316: a função [cancel] é privada (não escrevemos $scope.cancel=function(){}). Vamos rever o código do botão de cancelamento:
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
Quando o utilizador clica no botão Cancelar, o método [$scope.waiting.cancel()] é chamado. Em última análise, é a função privada cancel da linha 316 que é executada. Ela simplesmente oculta a mensagem de espera definindo a variável do modelo [waiting.visible] como false (linha 318);
3.7.5. Exemplo 5: Programação assíncrona
Vamos agora apresentar um novo serviço com um novo conceito: a programação assíncrona.
![]() |
A nossa aplicação terá três serviços:
- [config]: o serviço de configuração que acabámos de apresentar;
- [utils]: um serviço de métodos utilitários. Apresentaremos dois deles;
- [dao]: o serviço para aceder ao serviço web de agendamento de consultas. Iremos apresentá-lo em breve;
Iremos escrever a seguinte aplicação:
![]() |
![]() |
- O objetivo é exibir o banner [2] durante um período definido por [1]. A espera pode ser cancelada por [3].
Duplicamos [app-01.html] para [app-15.html] e modificamos o código da seguinte forma:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
<!-- the form -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="form-group">
<label for="waitingTime">{{waitingTimeText | translate}}</label>
<input type="text" id="waitingTime" ng-model="waiting.time"/>
</div>
<button class="btn btn-primary" ng-click="execute()">Exécuter</button>
</div>
</div>
..
<script type="text/javascript" src="rdvmedecins-03.js"></script>
</body>
</html>
- linha 11: o atributo [ng-cloak] impede que a área seja exibida até que as suas expressões Angular tenham sido avaliadas. Isto evita que a área apareça brevemente antes de o atributo [ng-show] ser avaliado, o que, na verdade, fará com que ela fique oculta;
- linha 22: a entrada do utilizador (tempo de espera) será armazenada no modelo [waiting.time] (atributo ng-model);
- linha 28: a página utiliza um novo script [rdvmedecins-03];
O script [rdvmedecins-03] é o seguinte:

- linha 6: o módulo Angular que gere a aplicação;
- linha 10: a função [config] utilizada para internacionalizar mensagens;
- linha 41: o serviço [config] que descrevemos;
- linha 286: o serviço [utils] que vamos construir;
- linha 315: o controlador [rdvmedecinsCtrl] que iremos construir;
Adicionamos uma nova chave de mensagem à função [config] (linhas 6, 11):
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// french messages
$translateProvider.translations("fr", {
...
'msg_waiting_time_text': "Temps d'attente : "
});
// english messages
$translateProvider.translations("en", {
...
'msg_waiting_time_text': "Waiting time:"
});
// default language
$translateProvider.preferredLanguage("fr");
}]);
Adicionamos uma nova linha (linha 6) ao serviço [config] para esta chave de mensagem:
angular.module("rdvmedecins")
.factory('config', function () {
return {
// messages to be internationalized
...
waitingTimeText: 'msg_waiting_time_text',
O serviço [utils] contém dois métodos (linhas 4, 12):
angular.module("rdvmedecins")
.factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
// display the Json representation of an object
function debug(message, data) {
if (config.debug) {
var text = data ? message + " : " + angular.toJson(data) : message;
console.log(text);
}
}
// waiting
function waitForSomeTime(milliseconds) {
// asynchronous waiting milliseconds milliseconds
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// we return the task
return task;
};
// service authority
return {
debug: debug,
waitForSomeTime: waitForSomeTime
}
}]);
- linha 2: o serviço é chamado [utils] (primeiro parâmetro). Depende de três serviços: dois serviços Angular predefinidos, $timeout e $q, e o serviço config. O serviço [$timeout] permite que uma função seja executada após ter decorrido um determinado período de tempo. O serviço [$q] permite a criação de tarefas assíncronas;
- linha 4: uma função local [debug];
- linha 12: uma função local [waitForSomeTime];
- linhas 23–26: a instância do serviço [utils]. Trata-se de um objeto que expõe dois métodos, os das linhas 4 e 12. Note-se que os campos do objeto podem ter quaisquer nomes. Por uma questão de consistência, atribuímos-lhes os nomes das funções a que fazem referência;
- linhas 4–9: o método [debug] escreve uma mensagem [message] na consola e, se aplicável, a representação JSON de um objeto [data]. Isto permite que objetos de qualquer complexidade sejam exibidos;
- linhas 12–20: o método [waitForSomeTime] cria uma tarefa assíncrona que dura [milliseconds] milissegundos;
- linha 14: criação de uma tarefa utilizando o objeto predefinido [$q] (https://docs.angularjs.org/api/ng/service/$q). Abaixo encontra-se a API para a tarefa denominada [deferred] na documentação do Angular:

- uma tarefa assíncrona [task] é criada pela instrução [$q.defer()];
- é concluída utilizando um de dois métodos:
- [task.resolve(value)]: que conclui a tarefa com sucesso e devolve o valor [value] àqueles que aguardam a conclusão da tarefa;
- [task.reject(value)]: que encerra a tarefa com um erro e devolve o valor [value] àqueles que aguardam a conclusão da tarefa;
A tarefa [task] pode fornecer informações periodicamente àqueles que aguardam a sua conclusão:
- [task.notify(value)]: envia o valor [value] para aqueles que aguardam a conclusão da tarefa. A tarefa continua a ser executada;
Quem quiser esperar que a tarefa termine utiliza o seu campo [promise]:
O objeto [promise] possui a seguinte API (http://www.frangular.com/2012/12/api-promise-angularjs.html):

Para lidar tanto com o sucesso como com o fracasso da tarefa, escrevemos:
- Linha 1: Recuperamos a promessa da tarefa;
- linha 2: definimos as funções a serem executadas em caso de sucesso ou falha. Podemos optar por não incluir uma função de falha. A função [successCallback] só será executada se a [tarefa] for concluída com sucesso [task.resolve()]. A função [errorCallback] só será executada se a [tarefa] falhar [task.reject()].
- Linha 3: Definimos a função a ser executada após uma das duas funções anteriores ter sido executada. Aqui, colocamos o código comum a ambas as funções [successCallback, errorCallback].
Voltemos ao código da função [waitForSomeTime]:
// attente
function waitForSomeTime(milliseconds) {
// attente asynchrone de milliseconds millisecondes
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// on retourne la tâche
return task;
};
- linha 4: é criada uma tarefa;
- linhas 5–7: o objeto [$timeout] permite definir uma função (primeiro parâmetro) que é executada após um determinado atraso expresso em milissegundos (segundo parâmetro). Aqui, o segundo parâmetro da função [$timeout] é o parâmetro do método (linha 1);
- linha 6: após o atraso de [milliseconds], a tarefa é concluída com sucesso;
- linha 9: a tarefa [task] é devolvida. É importante notar aqui que a linha 9 é executada imediatamente após a definição do objeto [$timeout]. Não esperamos que o atraso de [milliseconds] termine. O código nas linhas 2–10 é, portanto, executado em dois momentos diferentes:
- a primeira vez, quando o objeto [$timeout] é definido;
- uma segunda vez quando o tempo limite [milliseconds] tiver decorrido;
Esta é uma função assíncrona: o seu resultado é obtido num momento posterior à sua execução.
O código do controlador que utiliza o serviço [config] é o seguinte:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
function ($scope, utils, config, $filter) {
// ------------------- model initialization
// waiting message
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
// waiting task
var task;
// logs
utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
utils.debug("locales['fr']=", config.locales['fr']);
// execution action
$scope.execute = function () {
// log
utils.debug('début', new Date());
// the waiting msg is displayed
$scope.waiting.visible = true;
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
// end of wait
task.promise.then(function () {
// success
utils.debug('fin', new Date());
}, function () {
// failure
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// end of wait in all cases
$scope.waiting.visible = false;
});
};
// cancel wait
function cancel() {
// complete the task
task.reject();
}
}]);
- linha 3: o controlador utiliza o serviço [config];
- linha 7: adicionámos o campo [time] ao objeto [$scope.waiting]. O objeto [$scope.waiting.time] recebe o valor do tempo de espera definido pelo utilizador;
- linha 8: a chave para a mensagem de espera exibida pela vista é colocada no modelo [$scope.waitingTimeText]. Geralmente, tudo o que é exibido por uma vista V deve ser colocado no objeto [$scope];
- linha 10: uma variável local. Não é exposta à vista V;
- linhas 12-13: utilização do método [debug] do serviço [config]. O seguinte resultado é exibido na consola:
Linha 2: Obtemos a representação JSON do objeto locales['fr'].
- Linha 16: o método executado quando o utilizador clica no botão [Executar];
- linha 18: exibe a hora de início da execução do método;
- linha 22: a tarefa [waitForSomeTime] é iniciada. Não esperamos que ela termine. A execução continua com a linha 24 seguinte;
- linhas 24–30: definem as funções a serem executadas quando a tarefa for concluída com sucesso (linha 26) e em caso de erro (linha 29);
- linha 26: exibe a hora de término da execução do método;
- linha 29: indica que a operação foi cancelada. Isto ocorre apenas quando o utilizador clica no botão [Cancel]. A instrução na linha 41 interrompe então a tarefa assíncrona com um código de falha;
- linhas 31–34: definem a função a ser executada após uma das duas funções anteriores ter sido executada;
É importante compreender a sequência de execução deste código. Se o utilizador definir um atraso de 3 segundos e não cancelar a espera:
- ao clicar no botão [Executar], a função [$scope.execute] é executada. As linhas 16–34 são executadas sem esperar pelos 3 segundos. No final desta execução, a vista V é sincronizada com o modelo M. A mensagem de espera é exibida (ng-show=$scope.waiting.visible=true, linha 20) e o formulário é ocultado (ng-hide=$scope.waiting.visible=true, linha 20);
- a partir deste ponto, o utilizador pode interagir novamente com a vista. Em particular, pode clicar no botão [Cancelar];
- se não o fizer, após 3 segundos, a função [$timeout] (ver linhas 5–7 abaixo) é executada:
// attente
function waitForSomeTime(milliseconds) {
// attente asynchrone de milliseconds millisecondes
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// on retourne la tâche
return task;
};
- Após 3 segundos, o código é executado. Este código conclui a tarefa [task] com um código de sucesso (resolve). Isto irá desencadear a execução de todo o código que estava à espera desta conclusão (linha 4 abaixo):
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
// end of wait
task.promise.then(function () {
// success
utils.debug('fin', new Date());
}, function () {
// failure
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// end of wait in all cases
$scope.waiting.visible = false;
});
- A linha 6 acima (concluída com sucesso) será, portanto, executada. Em seguida, as linhas 11–14 serão executadas. Assim que este código for executado, regressamos à vista V, que será então sincronizada com o seu modelo M. A mensagem de espera é ocultada (ng-show=$scope.waiting.visible=false, linha 13) e o formulário é exibido (ng-hide=$scope.waiting.visible=false, linha 13);
A apresentação no ecrã fica então da seguinte forma:
Como mostrado acima, há um atraso de 3 segundos (06:01–05:58) entre o início e o fim da espera. Por outro lado, se o utilizador cancelar a espera antes de os 3 segundos terminarem, é exibido o seguinte:
Por fim, é importante compreender que, em qualquer momento, existe apenas um segmento de execução, conhecido como segmento da IU (Interface do Utilizador). A conclusão de uma tarefa assíncrona é sinalizada por um evento, tal como um clique num botão. Este evento não é processado imediatamente. É colocado na fila de eventos à espera de execução. Quando chega a sua vez, é processado. Este processamento utiliza a thread da UI, pelo que, durante este período, a interface fica bloqueada. Não responde às entradas do utilizador. Por este motivo, é importante que o processamento de eventos seja rápido. Como cada evento é processado pela thread da UI, nunca há necessidade de resolver problemas de sincronização entre threads em execução simultânea. Em qualquer momento, apenas a thread da UI está em execução.
3.7.6. Exemplo 6: Serviços HTTP
Apresentamos agora o serviço [dao] que comunica com o servidor web:
![]() |
3.7.6.1. A vista V
![]() |
Vamos criar um formulário para solicitar a lista de médicos:

Duplicamos [app-01.html] para [app-16.html], que depois modificamos da seguinte forma:
<div class="container" ng-cloak="">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
<!-- the request -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="form-group">
<label for="waitingTime">{{waitingTimeText | translate}}</label>
<input type="text" id="waitingTime" ng-model="waiting.time"/>
</div>
<div class="form-group">
<label for="urlServer">{{urlServerLabel | translate}}</label>
<input type="text" id="urlServer" ng-model="server.url"/>
</div>
<div class="form-group">
<label for="login">{{loginLabel | translate}}</label>
<input type="text" id="login" ng-model="server.login"/>
</div>
<div class="form-group">
<label for="password">{{passwordLabel | translate}}</label>
<input type="password" id="password" ng-model="server.password"/>
</div>
<button class="btn btn-primary" ng-click="execute()">{{medecins.title|translate:medecins.model}}</button>
</div>
<!-- list of doctors -->
<div class="alert alert-success" ng-show="medecins.show">
{{medecins.title|translate:medecins.model}}
<ul>
<li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
</ul>
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
{{errors.title|translate:errors.model}}
<ul>
<li ng-repeat="message in errors.messages">{{message|translate}}</li>
</ul>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-04.js"></script>
- Linhas 13–31: implementam o formulário. Este formulário não é visível quando a mensagem de espera é exibida (ng-hide="waiting.visible"). Note que os quatro campos de entrada são armazenados em (atributos ng-model) [waiting.time (linha 16), server.url (linha 20), server.login (linha 24), server.password (linha 28)];
- linhas 34–39: exibem a lista de médicos. Esta lista nem sempre está visível (ng-show="medecins.show").
- linha 35: uma alternativa à sintaxe <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}"> já encontrada;
- linha 36: uma lista não ordenada;
- linha 37: a lista de médicos encontra-se no modelo [medecins.data]. A diretiva Angular [ng-repeat] permite iterar através de uma lista. A sintaxe ng-repeat="doctor in medecins.data" instrui a tag <li> a ser repetida para cada elemento da lista [medecins.data]. O elemento atual na lista é chamado [medecin];
- linha 37: para cada <li>, exibimos o título, o nome próprio e o apelido do médico atual designado pela variável [medecin];
- linhas 42–47: exibem a lista de erros. Esta lista nem sempre está visível (ng-show="errors.show"). Esta exibição segue o mesmo padrão que a exibição da lista de médicos. Geralmente, para exibir uma lista de objetos, usamos a diretiva Angular [ng-repeat];
- linha 51: o código JavaScript encontra-se agora no ficheiro [rdvmedecins-04]
3.7.6.2. O controlador C e o modelo M
![]() |
O código JavaScript altera-se da seguinte forma:

- linhas 6–9: o módulo [rdvmedecins] declara uma dependência do módulo [base64] fornecido pela biblioteca [angular-base64], que é uma das dependências do projeto. Este módulo é utilizado para codificar a cadeia [login:password] enviada ao serviço web para autenticação em Base64;
- linhas 12–13: a função de inicialização que contém as nossas mensagens internacionalizadas. Surgem novas mensagens. Não as abordaremos mais detalhadamente;
- linhas 69–70: o serviço [config] que configura a nossa aplicação. Foram adicionadas novas chaves de mensagem aqui. Não as abordaremos mais detalhadamente;
- linhas 318–319: o serviço [utils], que contém métodos utilitários. Foram adicionados novos métodos. Iremos apresentá-los;
- linhas 385–386: o serviço [dao] responsável pela comunicação com o serviço web. É nisto que nos vamos concentrar;
- linhas 467–468: o controlador C para a vista V que acabámos de discutir. Vamos abordá-lo agora porque atua como o orquestrador que responde aos pedidos do utilizador;
3.7.6.3. O controlador C
O código do controlador é o seguinte:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
$scope.server = {url: undefined, login: undefined, password: undefined};
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
$scope.errors = {show: false, model: {}};
$scope.urlServerLabel = config.urlServerLabel;
$scope.loginLabel = config.loginLabel;
$scope.passwordLabel = config.passwordLabel;
// asynchronous task
var task;
// execution action
$scope.execute = function () {
// the UI is updated
$scope.waiting.visible = true;
$scope.medecins.show = false;
$scope.errors.show = false;
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// waiting
promise = promise.then(function () {
// we ask for the list of doctors;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
// analyze the result of the previous call
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// we put the acquired data into the model
$scope.medecins.data = result.data;
// the UI is updated
$scope.medecins.show = true;
$scope.waiting.visible = false;
} else {
// there were errors in obtaining the list of doctors
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
};
// cancel wait
function cancel() {
// complete the task
task.reject();
// the UI is updated
$scope.waiting.visible = false;
$scope.medecins.show = false;
$scope.errors.show = false;
}
}
])
;
- linha 2: o controlador tem uma nova dependência, nomeadamente o serviço [dao];
- linhas 6–13: o modelo M da vista V é inicializado na primeira vez que a vista é exibida;
- linha 8: [$scope.server] será utilizado para recuperar três das quatro informações do formulário V; a quarta está armazenada em [$scope.waiting.time] (linha 6);
- linha 9: [$scope.doctors] irá reunir as informações necessárias para exibir a lista de médicos:
<!-- list of doctors -->
<div class="alert alert-success" ng-show="medecins.show">
{{medecins.title|translate:medecins.model}}
<ul>
<li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
</ul>
</div>
O atributo [medecins.title] será o título do banner. Está definido no serviço [config]. O atributo [medecins.show] controlará se o banner é exibido ou não (atributo ng-show="medecins.show"). O atributo [medecins.model] é um dicionário vazio e permanecerá assim. É utilizado simplesmente para ilustrar a utilização da variante de tradução utilizada na linha 3. Ainda não definido, o atributo [medecins.data] conterá a lista de médicos (linha 5).
- Linha 10: [$scope.errors] irá recolher as informações necessárias para exibir a lista de erros:
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
{{errors.title|translate:errors.model}}
<ul>
<li ng-repeat="message in errors.messages">{{message|translate}}</li>
</ul>
</div>
O atributo [errors.title] será o título do banner. É definido no serviço [config]. O atributo [errors.show] controla se o banner é exibido ou não (atributo ng-show="errors.show"). O atributo [errors.model] é um dicionário vazio e permanecerá assim. É utilizado simplesmente para ilustrar a utilização da variante de tradução utilizada na linha 3. Ainda não definido, o atributo [errors.messages] conterá a lista de mensagens de erro a serem exibidas (linha 5).
- Linha 16: a tarefa assíncrona. O controlador irá lançar sucessivamente duas tarefas assíncronas. As referências a estas tarefas sucessivas serão colocadas na variável [task]. Isto permitirá que sejam canceladas (linha 55);
- Linha 19: O método executado quando o utilizador clica no botão [Lista de Médicos]:
<button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
- Linhas 21–23: A interface do utilizador é atualizada: a mensagem de carregamento é exibida e tudo o resto é ocultado;
- linha 25: a tarefa de espera assíncrona é criada. Um sinal (tarefa concluída) será recebido após o tempo introduzido pelo utilizador no formulário ter decorrido;
- linha 26: recuperamos a promessa da tarefa assíncrona. O programa que inicia a tarefa trabalha com esta promessa. No entanto, precisamos de ter a referência à própria tarefa para poder cancelá-la (linha 55);
- linhas 28–32: definimos o trabalho a realizar assim que a espera estiver concluída;
- linha 30: utilizamos o método [dao.getData] para iniciar uma nova tarefa assíncrona. Passamos-lhe as informações necessárias:
- a URL raiz do serviço web [$scope.server.url], por exemplo [http://localhost:8080];
- o login [$scope.server.login] para autenticação, por exemplo [admin];
- a palavra-passe [$scope.server.password] para autenticação, por exemplo [admin];
- a URL que devolve o serviço solicitado [config.urlSvrMedecins], neste caso [/getAllMedecins]. No total, a URL completa será [http://localhost:8080/getAllMedecins];
O método [dao.getData] retorna um resultado que pode assumir duas formas:
- (continuação)
- {err: 0, data: [med1, med2, ...]} onde [medi] é um objeto que representa um médico (título, nome próprio, apelido),
- {err: n, messages: [msg1, msg2, ...]} onde [msg] é uma mensagem de erro e n não é igual a 0;
- linha 31: devolvemos a promessa da tarefa. Aqui, há algo a compreender. Temos duas promessas:
- promise.then(): retorna uma primeira promessa [promise1];
- return task.promise: retorna uma segunda promessa [promise2];
- Em última análise, promise = promise.then(...; return task.promise) é uma cadeia de duas promessas [promise2.promise1]. [promise1] só será avaliada assim que a promessa [promise2] for resolvida, ou seja, quando a tarefa [dao.getData] estiver concluída. A promessa [promise1] não depende de nenhuma tarefa assíncrona. Será, portanto, resolvida imediatamente;
- linhas 34–50: Pela explicação anterior, conclui-se que estas linhas só serão executadas assim que a tarefa [dao.getData] estiver concluída. O parâmetro [result] passado para a função na linha 34 é construído pelo método [dao.getData] e passado para o código de chamada através da operação [task.resolve(result)], onde [result] tem a seguinte forma:
- {err: 0, data: [med1, med2, ...]} onde [medi] é um objeto que representa um médico (título, nome próprio, apelido),
- {err: n, messages: [msg1, msg2, ...]} onde [msg1] é uma mensagem de erro e n não é igual a 0;
- linha 37: verificamos o código de erro [result.err];
- linhas 38–42: se não houver erro (result.err == 0), então recuperamos a lista de médicos e exibimo-la;
- linhas 44–47: se, por outro lado, houver um erro (result.err != 0), então recuperamos a lista de mensagens de erro e exibimo-la;
- linhas 53–56: a mensagem de carregamento com o seu botão de cancelamento permanece visível até que ambas as operações assíncronas estejam concluídas. Vamos ver o que acontece dependendo de quando ocorre o cancelamento:
- Primeiro, é importante compreender que as linhas 19–50 são executadas todas de uma só vez. Apenas uma tarefa assíncrona foi iniciada neste momento, a da linha 25.
- Após esta execução inicial, a vista V é atualizada e, assim, o banner de espera e o seu botão de cancelamento ficam visíveis. Se o utilizador cancelar a espera antes de a tarefa na linha 25 estar concluída, o método na linha 53 é então executado e a tarefa é cancelada com um erro (linha 55);
- Linhas 56–59: A interface é atualizada: o formulário é exibido novamente e tudo o resto é ocultado,
- Em seguida, volta à vista V e o navegador processa o evento seguinte. Uma vez que a tarefa foi concluída, a promessa para esta tarefa é resolvida, o que desencadeia um evento. Este é então processado;
- as linhas 28–32 são então executadas. Não há nenhuma função definida para o caso de falha, pelo que nenhum código é executado. É obtida uma nova promessa, aquela que é sempre devolvida por [promise.then] e sempre resolvida;
- tendo o evento sido tratado, o controlo regressa à vista V e o navegador passa a tratar do evento seguinte. Uma vez que a [promessa] na linha 28 foi resolvida, a da linha 34 será resolvida, o que desencadeará um novo evento. Este é então tratado;
- as linhas 34–49 serão então executadas por sua vez, uma vez que a promessa utilizada na linha 34 foi cumprida. Mais uma vez, como não há nenhuma função definida para o caso de falha, nenhum código é executado,
- e assim chegamos à linha 50. Já não há nenhuma tarefa em espera e a nova vista V é exibida;
- Suponhamos agora que o cancelamento ocorra enquanto a segunda tarefa assíncrona [dao.getData] está a ser executada. O raciocínio anterior aplica-se novamente. O fim da tarefa irá desencadear a execução das linhas 34–50 com uma falha da tarefa. Em breve descobriremos que o método [dao.getData] faz uma chamada HTTP assíncrona ao serviço web. Esta chamada não será cancelada, mas o seu resultado não será utilizado.
É importante compreender esta constante alternância entre a renderização da vista V e o tratamento de eventos do navegador. Os eventos são acionados pelo utilizador (um clique) ou por operações do sistema, como a conclusão de uma operação assíncrona. O estado de inatividade do navegador é a renderização da vista V. Ele é retirado desse estado de inatividade por um evento que ocorre, o qual é então processado. Assim que o evento é processado, ele retorna ao seu estado de inatividade. A vista V é então atualizada se o evento processado tiver modificado o seu modelo M. O navegador é retirado do seu estado de inatividade pelo evento seguinte.
Tudo acontece numa única thread. Dois eventos nunca são processados simultaneamente. A sua execução é sequencial. O navegador passa para o evento seguinte apenas quando o anterior liberta o controlo, normalmente porque foi totalmente processado.
Há mais um ponto a explicar. Para exibir mensagens de erro, escrevemos:
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
A lista de mensagens é fornecida pelo método [utils.getErrors] definido no serviço [utils]. Este método é o seguinte:
// error analysis in server response JSON
function getErrors(data) {
// data {err:n, messages:[]}, err!=0
// errors
var errors = [];
// error code
var err = data.err;
switch (err) {
case 2 :
// not authorized
errors.push('not_authorized');
break;
case 3 :
// forbidden
errors.push('forbidden');
break;
case 4 :
// local error
errors.push('not_http_error');
break;
case 6 :
// document not found
errors.push('not_found');
break;
default :
// other cases
errors = data.messages;
break;
}
// if no msg, we put one
if (! errors || errors.length == 0) {
errors=['error_unknown'];
}
// return the list of errors
return errors;
}
- linhas 2-3: o parâmetro [data] recebido é um objeto com dois atributos:
- [err]: um código de erro;
- [messages]: uma lista de mensagens;
- linha 5: vamos construir uma matriz de mensagens de erro. Estas mensagens são internacionalizadas. Por esta razão, não são as próprias mensagens que colocamos na matriz, mas sim as suas chaves de internacionalização, exceto na linha 27. Neste caso, usamos o atributo [messages] do parâmetro [data]. Estas mensagens são mensagens reais e não chaves de mensagem. No entanto, a vista V irá tratá-las como chaves de mensagem, que então não serão encontradas. Neste caso, o módulo [translate] exibe a chave de mensagem que não encontrou — neste caso, uma mensagem real. Este é o resultado desejado;
- linhas 32–34: tratam o caso em que [data.messages] na linha 27 é nulo. Isto ocorre com o serviço web escrito. Este cenário deveria ter sido evitado.
3.7.6.4. O serviço [dao]
![]() |
O serviço [dao] lida com as trocas HTTP com o serviço web / JSON. O seu código é o seguinte:
angular.module("rdvmedecins")
.factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
function ($http, $q, config, $base64, utils) {
// logs
utils.debug("[dao] init");
// ----------------------------------méthodes privées
// obtain data from the web service
function getData(serverUrl, username, password, urlAction, info) {
// asynchronous operation
var task = $q.defer();
// url request HTTP
var url = serverUrl + urlAction;
// basic authentication
var basic = "Basic " + $base64.encode(username + ":" + password);
// the answer
var réponse;
// all http requests must be authenticated
var headers = $http.defaults.headers.common;
headers.Authorization = basic;
// query HTTP
var promise;
if (info) {
promise = $http.post(url, info, {timeout: config.timeout});
} else {
promise = $http.get(url, {timeout: config.timeout});
}
promise.then(success, failure);
// the task itself is returned so that it can be cancelled
return task;
// success
function success(response) {
// response.data={status:0, data:[med1, med2, ...]} or {status:x, data:[msg1, msg2, ...]
utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
// answer
var payLoad = response.data;
réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
// we return the answer
task.resolve(réponse);
}
// failure
function failure(response) {
utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
// status analysis
var status = response.status;
var error;
switch (status) {
case 401 :
// unauthorized
error = 2;
break;
case 403:
// forbidden
error = 3;
break;
case 404:
// not found
error = 6;
break;
case 0:
// local error
error = 4;
break;
default:
// something else
error = 5;
}
// we return the answer
task.resolve({err: error, messages: [response.statusText]});
}
}
// --------------------- service instance [dao]
return {
getData: getData
}
}]);
- linhas 77-79: o serviço tem apenas um campo: o método [getData], que recupera informações do serviço web / JSON;
- linha 2: surge uma dependência [$http] que ainda não encontrámos. Trata-se de um serviço Angular predefinido que permite a comunicação HTTP com uma entidade remota;
- linha 6: um registo para ver em que ponto do ciclo de vida da aplicação o código é executado;
- linha 10: o método [getData] aceita cinco parâmetros:
- [serverUrl]: a URL raiz do serviço web (http://localhost:8080);
- [urlAction]: a URL do serviço específico que está a ser solicitado (/getAllMedecins);
- [username]: o nome de utilizador;
- [password]: a palavra-passe do utilizador;
- [info]: um objeto que contém informações adicionais quando a URL do serviço específico solicitado é acedida através de uma operação POST. No caso da URL (/getAllMedecins), este parâmetro não foi passado. Por isso, está [indefinido];
- linha 12: é criada uma tarefa assíncrona;
- linha 14: a URL completa do serviço solicitado (http://localhost:8080/getAllMedecins);
- linha 16: a autenticação é realizada através do envio do seguinte cabeçalho HTTP:
onde [código] é a cadeia codificada em Base64 [nome de utilizador:palavra-passe];
A linha 16 constrói a parte [Código básico] do cabeçalho HTTP;
- linha 18: a resposta do serviço web;
- linha 20: os cabeçalhos HTTP enviados por padrão pelo Angular numa solicitação HTTP são definidos no objeto [$http.defaults.headers.common]. O cabeçalho [Authorization:Basic code] não está incluído;
- linha 21: adicionamo-lo aos cabeçalhos HTTP a serem enviados sistematicamente. No lado esquerdo da atribuição, temos o cabeçalho [Authorization] a ser inicializado e, no lado direito, o valor do cabeçalho — neste caso, o valor definido na linha 16. Portanto, se escrevermos:
O Angular enviará o cabeçalho HTTP:
- linha 23: os métodos do serviço [$http] devolvem promessas. Estas serão armazenadas na variável [promise];
- Linha 27: Como aqui o parâmetro [info] tem o valor [undefined], a linha 27 é executada. A URL (http://localhost:8080/getAllMedecins) é solicitada através de uma solicitação GET. Para evitar uma espera demasiado longa, definimos um tempo limite máximo para receber a resposta do servidor. Por predefinição, este tempo limite é de um segundo;
- linha 29: definimos os dois métodos a serem executados quando a promessa for cumprida:
- [success]: definido na linha 34, é o método a executar quando a promessa for resolvida após a conclusão bem-sucedida da tarefa;
- [failure]: definido na linha 45, é o método a executar quando a promessa é resolvida devido a uma falha na tarefa;
- Ambos os métodos (deveríamos dizer funções) estão definidos dentro da função [getData]. Isto é possível em JavaScript. As variáveis definidas em [getData] são acessíveis dentro das duas funções internas [success] e [failure];
- linha 31: devolvemos a tarefa criada na linha 12. Aqui, devemos recordar o código de chamada:
promise = promise.then(function () {
// we ask for the list of doctors;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
A linha 3 acima recupera uma tarefa.
- Linha 34: A função [success] é executada posteriormente, assim que o pedido HTTP for concluído com sucesso. Esta noção de sucesso está ligada à primeira linha de uma resposta HTTP. Assume a forma:
O código é um número de três dígitos que indica se a solicitação foi bem-sucedida ou não. De modo geral, os códigos 2xx e 3xx são códigos de sucesso, enquanto os demais são códigos de falha. O texto é uma breve mensagem explicativa. Aqui estão duas respostas possíveis, uma para sucesso e outra para falha:
- Linha 36: A resposta do servidor é apresentada na consola. No erro [404 Não encontrado], obtemos algo como:
[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}
Nesta resposta, utilizaremos apenas os campos [data], [status] e [statusText].
- Linha 38: Recuperamos o campo [data] da resposta. Ele assumirá uma das seguintes formas:
- {status: 0, data: [med1, med2, ...]}, em que [med1] é um objeto que representa um médico (título, nome próprio, apelido),
- {status: n, data: [msg1, msg2, ...]} onde [msg1] é uma mensagem de erro e n não é igual a 0;
![]() |

- Linha 39: Construímos a resposta {0,data} ou {n,mensagens}. A primeira resposta contém os médicos no campo [data]. A segunda indica um erro que ocorreu no lado do servidor. O servidor tratou este erro, gerou um código de erro em [err] e uma lista de mensagens de erro em [data]. Em ambos os casos, devolve um código de estado HTTP 200, indicando que o pedido HTTP foi totalmente processado. É por isso que ambos os casos são tratados na mesma função [success];
- linha 41: a tarefa é concluída [task.resolve] e uma das duas respostas é devolvida:
- {err: 0, data: [med1, med2, ...]} onde [medi] é um objeto que representa um médico (título, nome, apelido),
- {err: n, messages: [msg1, msg2, ...]} onde [msgi] é uma mensagem de erro e n não é igual a 0;
Este código deve estar associado à forma como esta resposta é recuperada no código de chamada do controlador:
// analyze the result of the previous call
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
...
}
A resposta de [task.resolve(response)] é armazenada na variável [result] acima.
- linha 45: a função [failure] quando a tarefa assíncrona termina em falha. Existem dois casos possíveis:
- o servidor sinaliza esta falha devolvendo um código de estado que não é nem 2xx nem 3xx,
- o Angular cancela o pedido HTTP. Neste caso, não é feito qualquer pedido. Ocorre uma exceção do Angular, mas o servidor não devolve qualquer código de erro HTTP. Isto acontece, por exemplo, se for fornecida uma URL inválida à qual não é possível aceder;
- linha 46: exibimos a resposta na consola;
- linha 48: recordamos que a resposta do servidor tem o seguinte formato:
{"data":"...","status":404,"config":{...},"statusText":"Not Found"}
Linha 48: Recuperamos o atributo [status] acima;
- Linhas 50–70: Com base no código de erro HTTP, geramos um novo código de erro para ocultar a natureza HTTP do método [dao.getData] do código de chamada. Podemos verificar que, no controlador que utiliza este método, nada sugere que exista uma chamada HTTP dentro do método;
- linha 51: o erro [401] corresponde a uma autenticação falhada (palavra-passe incorreta, por exemplo),
- linha 55: o erro [403] corresponde a um pedido não autorizado. O utilizador autenticou-se corretamente, mas não possui permissões suficientes para aceder ao URL solicitado. Isto ocorrerá com o utilizador [user / user]. Este utilizador existe na base de dados, mas não tem permissão para utilizar a aplicação. Apenas o utilizador [admin / admin] possui essa permissão;
- Linha 59: O erro [404] indica que a URL não foi encontrada. O erro pode ter várias causas:
- o utilizador cometeu um erro de digitação na URL do serviço;
- o serviço web não foi iniciado;
- o serviço web não respondeu com rapidez suficiente (tempo limite padrão de um segundo);
- linha 63: O código de erro HTTP 0 não existe. Isto ocorre quando o Angular não efetuou a chamada HTTP solicitada porque o URL introduzido pelo utilizador é inválido e não pode ser acedido. Encontraremos outros casos mais adiante em que o Angular não executa a chamada HTTP solicitada;
- linha 72: concluímos a tarefa com sucesso (task.resolve) devolvendo uma resposta do tipo {err, messages}, em que a matriz [messages] consiste exclusivamente na mensagem [response.statusText]. Se o Angular não efetuou a chamada HTTP solicitada, teremos uma string vazia;
Agora que temos uma visão geral e detalhada da aplicação, podemos começar a testar.
3.7.6.5. Testes da aplicação - 1
Vamos começar com entradas válidas:

![]() |
- em [1], introduzimos 0 para evitar qualquer atraso;
- em [2], recebemos uma mensagem de erro, mesmo que as entradas estejam corretas. Ainda não abordámos as diferentes mensagens de erro. A mensagem apresentada em [2] é uma mensagem genérica associada ao erro 0, que corresponde a uma exceção do Angular. O Angular encontrou um problema que o impediu de efetuar um pedido HTTP. Nesses casos, é necessário verificar os registos da consola JavaScript. Existem duas formas de o fazer:
- pressionar [F12] no navegador Chrome;
- utilizar a consola do WebStorm;
No console do WebStorm, encontramos várias mensagens, incluindo esta:
- Linha 1: O Angular reporta um erro, ao qual voltaremos mais tarde;
- linha 2: o registo do método [dao.getData]. Há aqui alguns detalhes interessantes:
- [status] é 0, indicando que não foi feita nenhuma solicitação HTTP. Consequentemente, [statusText] está vazio,
- [url] é equivalente a [http://localhost:8080/getAllMedecins], o que está correto;
- o cabeçalho de autenticação HTTP [Authorization":"Basic YWRtaW46YWRtaW4=] também está correto;
Então, por que não funcionou? A frase-chave nos registos é [Não existe nenhum cabeçalho 'Access-Control-Allow-Origin']. Para compreender isto, é necessária uma explicação pormenorizada. Comecemos por rever a arquitetura geral da aplicação cliente/servidor:

- as páginas HTML/CSS/JS da aplicação Angular vêm do servidor [1];
- em [2], o serviço [dao] faz uma solicitação a outro servidor, o servidor [2]. Bem, isto é bloqueado pelo navegador que executa a aplicação Angular, porque se trata de uma vulnerabilidade de segurança. A aplicação só pode consultar o servidor de onde veio, ou seja, o servidor [1];
Na verdade, não é correto dizer que o navegador impede a aplicação Angular de consultar o servidor [2]. Na realidade, ele consulta-o para perguntar se este permite que um cliente que não seja originário do seu próprio domínio o consulte. Esta técnica de partilha é chamada CORS (Cross-Origin Resource Sharing). O servidor [2] concede permissão enviando cabeçalhos HTTP específicos. Foi porque o nosso servidor [2] não os enviou que o navegador recusou efetuar o pedido HTTP solicitado pela aplicação.
Agora vamos entrar em detalhes. Vamos examinar o tráfego de rede que ocorreu durante a solicitação HTTP. Para isso, no navegador Chrome, pressionamos [F12] para abrir as ferramentas de desenvolvedor e selecionamos a guia [Rede] para visualizar o tráfego de rede:
![]() |
- Em [1], selecionamos o separador [Rede];
- em [2], solicitamos a lista de médicos;
Obtemos as seguintes informações no separador [Rede]:
![]() |
- em [1], as informações enviadas ao servidor;
- em [2], a resposta do servidor;
Podemos ver em [1] que o navegador enviou um pedido HTTP [OPTIONS] para o URL solicitado. [OPTIONS] é um dos métodos HTTP, a par dos mais conhecidos [GET] e [POST]. Permite solicitar informações a um servidor, nomeadamente sobre as opções HTTP que este suporta, daí o nome do método. O servidor responde em [2]. Para indicar que aceita pedidos de clientes fora do seu domínio, deve devolver um cabeçalho específico chamado [Access-Control-Allow-Origin]. E como não devolveu este cabeçalho, o Angular não executou a chamada HTTP solicitada e devolveu o erro:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.
Devemos, portanto, modificar o nosso servidor para que envie o cabeçalho HTTP esperado.
3.7.6.6. Modificar o servidor Web/JSON
Voltamos ao Eclipse. Para preservar o nosso progresso, duplicamos a versão atual do servidor Web/JSON [rdvmedecins-webapi-v2] para [rdvmedecins-webapi-v3] [1]:
![]() |
Fazemos uma modificação inicial em [ApplicationModel], que é um dos elementos de configuração do serviço Web:
package rdvmedecins.web.models;
...
@Component
public class ApplicationModel implements IMetier {
// the [business] layer
@Autowired
private IMetier métier;
// data from the [business] layer
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// configuration data
private boolean CORSneeded = true;
...
public boolean isCORSneeded() {
return CORSneeded;
}
}
- linha 17: criamos uma variável booleana que indica se os clientes de fora do domínio do servidor são aceites ou não;
- linhas 21–23: o método para aceder a esta informação;
Em seguida, criamos um novo controlador Spring MVC [3]:
![]() |
A classe [RdvMedecinsCorsController] é a seguinte:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
}
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
}
- Linhas 28–31: definem um controlador para a URL [/getAllMedecins] quando esta é solicitada com o método HTTP [OPTIONS];
- linha 29: o método [getAllMedecins] recebe o objeto [HttpServletResponse] como parâmetro, que será enviado ao cliente que efetuou a solicitação. Este objeto é injetado pelo Spring;
- linha 30: o tratamento da solicitação é delegado ao método privado nas linhas 19–25;
- linhas 15–16: o objeto [ApplicationModel] é injetado;
- linhas 20–23: se o servidor estiver configurado para aceitar clientes de fora do seu domínio, então o cabeçalho HTTP é enviado:
Access-Control-Allow-Origin: *
o que significa que o servidor aceita clientes de qualquer domínio (*).
Estamos agora prontos para realizar mais testes. Lançamos a nova versão do serviço web e verificamos que o problema permanece inalterado. Nada mudou. Se adicionarmos uma saída de consola na linha 30 acima, esta nunca é apresentada, indicando que o método [getAllMedecins] na linha 29 nunca é chamado.
Após alguma pesquisa, descobrimos que o Spring MVC lida com os pedidos HTTP [OPTIONS] por si próprio, com um processamento por predefinição. Por conseguinte, é sempre o Spring que responde, e nunca o método [getAllMedecins] na linha 29. Este comportamento por predefinição do Spring MVC pode ser alterado. Introduzimos uma nova classe de configuração para definir o novo comportamento:
![]() |
A nova classe de configuração [WebConfig] é a seguinte:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
// dispatcherservlet configuration for CORS headers
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
- linha 8: a classe é uma classe de configuração Spring. Ela declara beans que serão colocados no contexto Spring;
- linha 12: o bean [dispatcherServlet] é utilizado para definir o servlet que trata dos pedidos dos clientes. É do tipo [DispatcherServlet]. Este servlet é normalmente criado por predefinição. Se o criarmos nós próprios, podemos então configurá-lo;
- linha 14: criamos uma instância do tipo [DispatcherServlet];
- linha 15: instruímos o servlet a reencaminhar comandos HTTP [OPTIONS] para a aplicação;
- linha 16: devolvemos o servlet configurado desta forma;
Ainda precisamos de modificar a classe [AppConfig]:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- Linha 11: A nova classe de configuração [WebConfig] é importada;
3.7.6.7. Teste da aplicação - 2
Lançamos a nova versão do serviço web / JSON e tentamos recuperar a lista de médicos utilizando o nosso cliente Angular. Analisamos o tráfego de rede no separador [Rede]:
![]() |
- Em [1], podemos ver que o cabeçalho HTTP [Access-Control-Allow-Origin: *] está agora presente na resposta do servidor. No entanto, continua a não funcionar. Analisamos os registos da consola em [2]. Aí encontramos o seguinte registo:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Request header field Authorization is not allowed by Access-Control-Allow-Headers
Podemos ver que o navegador está à espera de um novo cabeçalho HTTP [Access-Control-Allow-Headers] que lhe indique que temos permissão para enviar o cabeçalho de autenticação:
Isto pode ser um bom sinal. O Angular pode ter tentado enviar o pedido HTTP GET. No entanto, uma vez que este pedido inclui um cabeçalho de autenticação, está a verificar se o servidor o aceita.
Modificamos o nosso servidor web / JSON para enviar este cabeçalho. A classe [RdvMedecinsCorsController] é alterada da seguinte forma:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// we authorize the header [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
- As linhas 6–7 adicionam o cabeçalho em falta.
Reiniciamos o servidor e solicitamos novamente a lista de médicos utilizando o cliente Angular:
![]() |
Desta vez, funcionou. Os registos da consola mostram a resposta recebida pelo método [dao.getData]:
[dao] getData[/getAllMedecins] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
Podemos ver que:
- o servidor devolveu um código de erro [status=200] com a mensagem [statusText=OK]. É por isso que estamos na função [success];
- o servidor devolveu um objeto [data] com dois campos:
- [status]: (não confundir com o código de erro HTTP [status]). Aqui, [status=0] indica que a URL [/getAllMedecins] foi processada sem erros;
- [data]: que contém a lista JSON de médicos;
Vamos agora analisar alguns outros casos interessantes:
Introduzimos credenciais incorretas [login, password]:
![]() |
Iniciamos sessão como [user / user], que não tem acesso à aplicação (apenas [admin] tem acesso):
![]() |
Desta vez, o erro já não é [Erro de autenticação], mas sim [Acesso negado].
3.7.7. Exemplo 7: Lista de clientes
Vamos utilizar a aplicação anterior para apresentar a lista de clientes num menu suspenso do tipo [Bootstrap select] (ver secção 3.6.6).
3.7.7.1. Vista V
A vista inicial será a seguinte:
![]() |
Para obter a vista V, duplicamos o código de [app-16.html] para [app-17.html] e modificamo-lo da seguinte forma:
<div class="container" >
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible" >
...
</div>
<!-- the request -->
<div class="alert alert-info" ng-hide="waiting.visible" >
...
<button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
</div>
<!-- customer list -->
<div class="row" style="margin-top: 20px" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
- linhas 5-7: o banner de carregamento não muda;
- linhas 10-13: o formulário não muda, exceto o rótulo do botão (linha 12);
- linhas 28-30: o banner de erro não muda;
- linhas 16-25: os clientes são apresentados numa lista suspensa com o estilo do componente [Bootstrap-selectpicker] (atributos data-style e class, linha 19);
- linha 20: a diretiva [ng-repeat] é utilizada para gerar as várias opções na lista suspensa. Note-se que o rótulo de uma opção é do tipo [Mme Julienne Tatou] e que o valor da opção é do tipo [100], sendo que 100 é o ID do cliente apresentado;
- linha 34: o código JavaScript é movido para um novo ficheiro [rdvmedecins-05];
3.7.7.2. O controlador C e o modelo M
O código JavaScript no ficheiro [rdvmedecins-05] é copiado do ficheiro [rdvmedecins-04]:

Quase nada mudou, exceto no controlador, que agora está adaptado para fornecer a lista de clientes:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
$scope.server = {url: undefined, login: undefined, password: undefined};
$scope.clients = {title: config.listClients, show: false, model: {}};
$scope.errors = {show: false, model: {}};
$scope.urlServerLabel = config.urlServerLabel;
$scope.loginLabel = config.loginLabel;
$scope.passwordLabel = config.passwordLabel;
// asynchronous task
var task;
// execution action
$scope.execute = function () {
// the UI is updated
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// waiting
promise = promise.then(function () {
// we ask for the customer list;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
return task.promise;
});
// analyze the result of the previous call
promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// we put the acquired data into the model
$scope.clients.data = result.data;
// the UI is updated
$scope.clients.show = true;
$scope.waiting.visible = false;
// style the drop-down list
$('.selectpicker').selectpicker();
} else {
// there were errors in obtaining the customer list
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
};
// cancel wait
function cancel() {
// complete the task
task.reject();
// the UI is updated
$scope.waiting.visible = false;
$scope.clients.show = false;
$scope.errors.show = false;
}
}
])
;
- Muito pouco mudou no controlador. Anteriormente, fornecia uma lista de médicos. Agora, fornece uma lista de clientes;
- linha 9: [$scope.clients] será o modelo para o banner do cliente na vista V;
- linha 30: a URL [/getAllClients] é agora utilizada;
- linhas 35–36: os dois formatos de resposta devolvidos pelo método [dao.getData]. Agora temos clientes em vez de médicos;
- linha 44: uma instrução bastante rara no código Angular. Estamos a manipular diretamente o DOM (Document Object Model). Aqui, queremos aplicar o método [selectpicker] (parte do [bootstrap-select.min.js]) aos elementos DOM que têm a classe [selectpicker] [$('.selectpicker)']. Existe apenas um: a lista suspensa:
<select data-style="btn-primary" class="selectpicker" select-enable="">
....
</select>
Na secção 3.6.6, vimos que isto formatava a lista suspensa da seguinte forma:
![]() | ![]() |
Tal como foi feito para os médicos, também precisamos de modificar o serviço web.
3.7.7.3. Modificar o serviço web - 1
![]() |
A classe [RdvMedecinsController] foi melhorada com um novo método:
package rdvmedecins.web.controllers;
...
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// we authorize the header [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
public void getAllClients(HttpServletResponse response) {
sendOptions(response);
}
}
- Linhas 29–32: O método [getAllClients] irá tratar a solicitação HTTP [OPTIONS] enviada pelo navegador;
3.7.7.4. Teste da aplicação – 1
Estamos agora prontos para testar. Iniciamos o servidor web e, em seguida, introduzimos valores válidos no formulário Angular. Obtemos a seguinte resposta:

Esta mensagem de erro é apresentada quando o Angular não conseguiu efetuar a solicitação HTTP pedida. Temos então de procurar as causas nos registos da consola. Aí encontramos a seguinte mensagem:
XMLHttpRequest cannot load http://localhost:8080/getAllClients. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.
Um problema que pensávamos estar resolvido. Vejamos agora o tráfego de rede que ocorreu:

Vemos que a operação [getAllClients] utilizando o método HTTP [OPTIONS] foi bem-sucedida, mas a operação [getAllClients] utilizando o método HTTP [GET] foi cancelada. A resposta ao pedido [OPTIONS] foi a seguinte:

Os cabeçalhos HTTP CORS estão, de facto, presentes. Vamos agora examinar as trocas HTTP durante a solicitação GET:

A solicitação HTTP parece estar correta. Em particular, podemos ver o cabeçalho de autenticação.
Para além da mensagem de erro anterior, a seguinte mensagem aparece nos registos da consola:
[dao] getData[/getAllClients] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}
Este é o registo que o método [dao.getData] gera sistematicamente ao receber a resposta à sua solicitação HTTP. Dois aspetos destacam-se:
- [status=0]: isto significa que o Angular cancelou a solicitação HTTP;
- [method=GET]: e foi a solicitação GET que foi cancelada;
Quando combinado com a primeira mensagem, isto significa que o Angular também está à espera de cabeçalhos CORS para a solicitação GET. No entanto, atualmente, o nosso serviço web só os envia para a solicitação HTTP [OPTIONS]. É muito estranho encontrar este erro agora e não na lista de médicos. Não tenho nenhuma explicação.
Por isso, precisamos de modificar o serviço web novamente.
3.7.7.5. Modificar o serviço web – 2
![]() |
Os métodos [GET] e [POST] são tratados na classe [RdvMedecinsController]. Precisamos de a modificar para que estes métodos enviem os cabeçalhos CORS. Fazemos isso da seguinte forma:
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients(HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.getAllClients(response);
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// customer list
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
...
- linha 8: queremos reutilizar o código que colocámos no controlador [RdvMedecinsCorsController]. Por isso, injetamo-lo aqui;
- linha 14: o método que trata a solicitação [GET /getAllClients]. Fazemos duas alterações:
- linha 14: injetamos o objeto [HttpServletResponse] nos parâmetros do método,
- linha 16: usamos os métodos da classe [RdvMedecinsCorsController] para definir os cabeçalhos CORS neste objeto;
3.7.7.6. Teste da Aplicação – 2
Lançamos a nova versão do serviço web e solicitamos novamente a lista de clientes. Obtemos a seguinte resposta:
![]() |
- em [1], obtemos uma resposta, mas está vazia [2];
- em [3]: as trocas de dados na rede ocorreram sem problemas;
Nos registos da consola, o método [dao.getData] apresentou a resposta que recebeu:
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
Portanto, o método recebeu efetivamente a lista de clientes. Depois de verificado o código, começamos a suspeitar da seguinte instrução, que não compreendemos totalmente:
// on style la liste déroulante
$('.selectpicker').selectpicker();
Comentamos a linha 2 e tentamos novamente. Obtemos então a seguinte resposta:
![]() |
Identificámos, assim, o problema. É a aplicação do método [selectpicker] à lista suspensa que está a causar o problema. Quando analisamos o código-fonte da página com o erro, vemos o seguinte:
![]() |
- descobrimos que em [1], a lista suspensa está de facto presente com os seus elementos, mas não é apresentada [style='display:none'];
- Em [2], o botão [bootstrap select] é exibido. Os itens da lista suspensa deveriam aparecer na lista <ul role='menu'>. Como não estão lá, temos uma lista vazia. Parece que, quando o método [selectpicker] foi aplicado à lista suspensa, o seu conteúdo estava vazio naquele momento;
Ao pesquisar na web por uma solução, encontrámos esta. Substituímos o código:
// on style la liste déroulante
$('.selectpicker').selectpicker();
com o seguinte:
// on style la liste déroulante
$timeout(function(){
$('.selectpicker').selectpicker();
});
O estilo [bootstrap-select] é aplicado através de uma função [$timeout]. Já nos deparámos com esta função, que permite que uma função seja executada após um determinado atraso. Aqui, a ausência de um atraso significa um atraso de zero. As linhas anteriores colocam um evento na fila de eventos do navegador. Quando o evento atual (clique no botão [Lista de Clientes]) terminar de ser processado, a vista V será exibida. Imediatamente a seguir, o navegador verificará a sua lista de eventos. Devido ao seu atraso zero, o evento [$timeout] estará no topo da lista e será processado. O estilo [bootstrap-select] é então aplicado a uma lista suspensa preenchida. Vejamos o resultado:
![]() |
Se olharmos novamente para o código-fonte da página exibida, vemos o seguinte:
![]() |
O botão [bootstrap-select], que anteriormente estava vazio, contém agora a lista de clientes.
3.7.7.7. Utilizando uma diretiva
No controlador C da vista V, deparámo-nos com o seguinte código:
// on style la liste déroulante
$('.selectpicker').selectpicker();
Estamos a manipular um objeto DOM. Muitos programadores Angular têm aversão a manipular o DOM dentro do código do controlador. Para eles, isto deve ser feito numa diretiva. Uma diretiva Angular pode ser vista como uma extensão da linguagem HTML. Isto permite criar novos elementos ou atributos HTML. Vejamos um primeiro exemplo:
Criamos o seguinte ficheiro JS [selectEnable]:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
var selectpicker = $('.selectpicker');
selectpicker.selectpicker();
});
}
};
}]);
- A diretiva segue a sintaxe do controlador com a qual já estamos familiarizados:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)
A diretiva pertence ao módulo [rvmedecins]. É uma função que aceita dois parâmetros:
- (continuação)
- o primeiro é o nome da diretiva [selectEnable];
- o segundo é uma matriz ['obj1','obj2',..., function(obj1, obj2,...)] em que os [obj] são os objetos a serem injetados na função. Aqui, o único objeto injetado é o objeto predefinido [$timeout];
- a função [directive] devolve um objeto que pode ter vários atributos. Aqui, o único atributo é o atributo [link] (linha 3). O seu valor aqui é uma função que recebe três parâmetros:
- scope: o modelo da vista na qual a diretiva é utilizada;
- element: o elemento da vista, o alvo da diretiva;
- attrs: os atributos deste elemento;
Vejamos um exemplo. A diretiva [selectEnable] poderia ser utilizada no seguinte contexto:
No exemplo acima, o atributo [select-enable] aplica a diretiva [selectEnable] ao elemento HTML <div>. Uma diretiva [doSomething] pode ser aplicada a qualquer elemento HTML adicionando-lhe o atributo [do-something]. Repare na diferença ortográfica entre o nome da diretiva e o seu atributo associado. Passamos de [camelCase] para [camel-case].
A diretiva [selectEnable] também pode ser utilizada da seguinte forma:
Aqui, a diretiva [doSomething] é aplicada na forma de uma tag HTML <do-something>.
Voltemos à sintaxe
e aos três parâmetros da função [link] da diretiva, [scope, element, attrs]:
- scope: é o modelo da vista na qual o <div> está localizado;
- element: é o próprio <div>;
- attrs: é a matriz de atributos para o <div>. Estes podem ser utilizados para passar informações para a diretiva. No exemplo acima, escrevemos attrs['selectEnable'] para recuperar a informação [data]. Repare na alteração na notação [selectEnable] para se referir ao atributo [select-enable];
Voltemos ao código da diretiva:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
$('.selectpicker').selectpicker();
});
}
};
}]);
- Linhas 14–16: Aqui vemos o código que colocámos anteriormente no controlador. Este código é executado quando a diretiva [select-enable] (como elemento ou atributo) é encontrada durante a renderização da vista V.
Para implementar esta diretiva, copiamos o ficheiro [app-17.html] para [app-17B.html] e modificamo-lo da seguinte forma:
<select data-style="btn-primary" class="selectpicker" select-enable="">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
- linha 1: aplicamos a diretiva [selectEnable] ao elemento HTML [select]. Como não há informações a passar para a diretiva, escrevemos simplesmente [select-enable=""] ;
Também modificamos o controlador duplicando o ficheiro JS [rdvmedecins-05.js] para [rdvmedecins-05B.js] e referenciamos o novo ficheiro JS no ficheiro [app-17B.html] e no ficheiro da diretiva [selectEnable.js]. Não se esqueça deste último ponto. Se o ficheiro da diretiva estiver em falta, o atributo [select-enable=""] não será processado, mas o Angular não reportará quaisquer erros.
<script type="text/javascript" src="rdvmedecins-05B.js"></script>
<script type="text/javascript" src="selectEnable.js"></script>
No ficheiro JS [rdvmedecins-05B.js], removemos as seguintes linhas do controlador:
// on style la liste déroulante
$timeout(function(){
$('.selectpicker').selectpicker();
});
Esta operação é agora tratada pela diretiva.
3.7.7.8. Teste da aplicação – 3
Ao testar a nova aplicação [app-17B.html], obtém-se o seguinte resultado:
![]() |
- Em [1], obtemos uma lista vazia.
Os registos da consola apresentam o seguinte:
- linha 1: inicialização do serviço [dao];
- linha 2: aquando da exibição inicial da vista V, a diretiva [selectEnable] é executada;
- linha 3: esta linha aparece quando o utilizador clica no botão [Lista de Clientes]. Podemos ver que a diretiva [selectEnable] não é executada uma segunda vez. Em última análise, foi executada quando a lista de clientes estava vazia, pelo que temos uma lista suspensa vazia;
Por outras palavras, é a operação:
$('.selectpicker').selectpicker();
não ocorreu no momento certo. Podemos tentar resolver o problema de várias maneiras. Após inúmeros testes sem sucesso, percebemos que a operação acima deve ocorrer apenas uma vez e somente quando a lista suspensa tiver sido preenchida. Para alcançar este resultado, reescrevemos a tag <select> da seguinte forma:
<select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
Linha 1: A tag <select> só é gerada se [clients.data] existir. Este não é o caso quando a vista V é inicialmente apresentada. Por conseguinte, a tag <select> não será gerada e a diretiva [selectEnable] não será avaliada. Quando o utilizador clicar no botão [Lista de Clientes], [clients.data] terá um novo valor no modelo M. Como o modelo M mudou, a tag <select> será reavaliada e gerada aqui. A diretiva [selectEnable] será, portanto, avaliada também. Quando é avaliada, as linhas 2–4 da tag <select> ainda não foram avaliadas. Temos, portanto, uma lista vazia de clientes. Se escrevermos a diretiva [selectEnable] da seguinte forma:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable");
$('.selectpicker').selectpicker();
}
}
}]);
A linha 5 será executada com uma lista vazia e, em seguida, veremos uma lista suspensa vazia no ecrã. Por isso, devemos escrever:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable");
$timeout(function () {
$('.selectpicker').selectpicker();
})
}
}
}]);
para obter o resultado esperado. Devido ao [$timeout] na linha 5, a linha 6 só será executada depois de a vista V ter sido totalmente renderizada, ou seja, num momento em que a tag <select> tenha todos os seus elementos.
3.7.8. Exemplo 8: Agenda de um médico
Apresentamos agora uma aplicação que exibe a agenda de um médico.
3.7.8.1. A vista V da aplicação
Apresentaremos o seguinte formulário:
![]() |
- em [1], solicitamos a agenda da Sra. PELISSIER [2] para 25 de junho de 2014 [3];
O resultado seguinte [4] é obtido:
![]() |
Vamos examinar as duas perspetivas separadamente.
3.7.8.2. O formulário
Duplicamos o ficheiro [app-17.html] para [app-18.html] e, em seguida, modificamos o código da seguinte forma:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the request -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="row" style="margin-bottom: 20px">
<div class="col-md-3">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
<div class="col-md-3">
<h2 translate="{{calendar.title}}"></h2>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="calendar.jour" min-date="calendar.minDate" show-weeks="true"
class="well well-sm"></datepicker>
</div>
</div>
</div>
<button class="btn btn-primary" ng-click="execute()">{{agenda.title|translate}}</button>
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- the diary -->
<div id="agenda" ng-show="agenda.show">
...
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
- linhas 5-7: a mensagem de carregamento não muda;
- linhas 12-19: a lista de médicos utilizando o componente [bootstrap select];
- linhas 20-26: o calendário [ui-bootstrap] que já apresentámos. Note-se que o dia selecionado é colocado no modelo [calendar.day] (atributo ng-model);
- linha 28: o botão que solicita o calendário;
- linhas 32–34: a lista de erros permanece inalterada;
- linhas 37–39: o calendário, que apresentaremos mais tarde;
- linha 42: o código JS é transferido para o ficheiro [rdvmedecins-06.js] através da cópia do ficheiro [rdvmedecins-05.js];
3.7.8.3. O controlador C
O código JS da aplicação passa a ser o seguinte:

Apenas o serviço [utils] e o controlador [rdvMedecinsCtrl] serão afetados pelas alterações.
O controlador [rdvMedecinsCtrl] passa a ser o seguinte:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
$scope.errors = {show: false, model: {}};
$scope.medecins = {
data: [
{id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
{id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
{id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
{id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
],
title: config.listMedecins};
$scope.agenda = {title: config.getAgendaTitle, data: undefined, show: false};
$scope.calendar = {title: config.getCalendarTitle, minDate: new Date(), jour: new Date()};
// style the drop-down list
$timeout(function () {
$('.selectpicker').selectpicker();
});
// for the French local calendar
angular.copy(config.locales['fr'], $locale);
...
}
])
;
- linha 7: definimos um tempo limite de 3 segundos antes de efetuar o pedido HTTP;
- linha 8: os elementos necessários para a ligação HTTP estão codificados de forma rígida;
- linhas 10–17: a lista de médicos está codificada de forma rígida;
- linha 18: o modelo [agenda] configura a exibição do calendário na vista;
- linha 19: o modelo [calendar] configura a exibição do calendário na vista. Definimos a data mínima [minDate] para hoje e a data atual também para hoje;
- linhas 21–23: a lista suspensa é estilizada utilizando o método visto anteriormente;
- linha 25: definimos a localização da aplicação como «fr». Por predefinição, é «en»;
O método executado quando o calendário é solicitado é o seguinte:
// exécution action
$scope.execute = function () {
// les infos du formulaire
var idMedecin = $('.selectpicker').selectpicker('val');
// vérification
utils.debug("[homeCtrl] idMedecin", idMedecin);
utils.debug("[homeCtrl] jour", $scope.calendar.jour);
// on met le jour au format yyyy-MM-dd
var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
// mise à jour de la vue
$scope.waiting.visible = true;
$scope.errors.show = false;
$scope.agenda.show = false;
...
};
- Linha 4: Recuperamos o atributo [value] do médico selecionado. Aqui, voltamos a utilizar o método [selectpicker] do ficheiro [bootstrap-select.min.js]. Lembre-se do formato das opções da lista suspensa:
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
O valor (atributo value) da opção é, portanto, o [id] do médico.
- linha 11: formatamos a data selecionada pelo utilizador como [aaaa-mm-dd], que é o formato de data esperado pelo servidor web;
- linhas 13-15: quando o método [execute] terminar, o banner de carregamento será exibido e todo o resto ficará oculto;
O código continua da seguinte forma:
// simulated waiting
var task = utils.waitForSomeTime($scope.waiting.time);
// we ask for the doctor's diary
var promise = task.promise.then(function () {
// the URL service path
var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
// we ask for the agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// we return the promise of task completion
return task.promise;
});
// we analyze the result of the call to service [dao]
promise.then(function (result) {
// end of wait
$scope.waiting.visible = false;
// mistake?
if (result.err == 0) {
// we prepare the agenda model
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// timetable display formatting
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
// we create an evt to style the table after the view is displayed
$timeout(function () {
$("#creneaux").footable();
});
} else {
// mistakes were made in obtaining the agenda
$scope.errors = {
title: config.getAgendaErrors,
messages: utils.getErrors(result),
show: true
};
}
- linha 2: a tarefa assíncrona que aguarda 3 segundos;
- linhas 5–10: o código que será executado quando esta espera terminar;
- linha 6: a URL solicitada é construída [/getAgendaMedecinJour/1/2014-06-25];
- linha 8: a URL é consultada. Inicia-se uma tarefa assíncrona;
- linha 10: tornamos esta tarefa assíncrona;
- linhas 14–38: o código que será executado assim que o pedido HTTP tiver devolvido a sua resposta;
- linha 13: [result] é a resposta enviada pelo método [dao.getData]. Aqui, devemos ter em conta o formato da resposta do servidor web:
![]() |
O parâmetro [result.data] na linha 19 é o atributo [data] [1] mencionado acima. Este atributo, por sua vez, contém o atributo [creneauxMedecin] [2] mencionado acima. Trata-se de uma matriz de intervalos de tempo, cada um contendo duas informações:
- [rv]: a representação JSON de uma consulta, ou [null] se não tiver sido agendada nenhuma consulta para esse intervalo;
- [hDeb, mDeb, hFin, mFin]: as informações de tempo para o intervalo;
Voltemos ao código do controlador:
- linha 15: a espera acabou;
- linha 19: preenchemos o modelo [$scope.agenda], que controla a exibição do calendário;
- linha 20: o calendário é tornado visível;
- linhas 22–24: percorremos cada elemento C na matriz [creneauxMedecin] que acabámos de discutir;
- linha 23: cada elemento C tem um atributo [slot] que representa o intervalo de tempo. Este é complementado com um atributo [text] que será a representação textual do intervalo de tempo no formato [10:20–10:40];
- linhas 26–28: tornamos a tabela HTML utilizada para apresentar os intervalos do calendário responsiva. Abordámos este conceito na secção 3.6.7;
![]() |
- linha 27: para tornar a tabela responsiva, temos de lhe aplicar o método [footable]. Aqui deparamo-nos com a mesma dificuldade que com o componente [bootstrap-select]. Se escrevermos simplesmente a linha 17, vemos que a tabela não é responsiva. Resolvemos este problema da mesma forma, utilizando a função [$timeout] (linha 26);
- linhas 31–34: o caso em que a solicitação HTTP falhou. São então exibidas mensagens de erro;
3.7.8.4. Exibição do calendário
Voltamos agora ao código do calendário no ficheiro [app-18.html]. É o seguinte:
<!-- the diary -->
<div id="agenda" ng-show="agenda.show">
<!-- case of a doctor without consultation slots -->
<h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
translate="agenda_medecinsanscreneaux"></h4>
<!-- doctor's diary -->
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table creneaux-table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span translate="agenda_creneauhoraire"></span>
</th>
<th>
<span translate="agenda_client">Client</span>
</th>
<th data-hide="phone">
<span translate="agenda_action">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
<td>
<span
ng-class="! creneauMedecin.rv ? 'status-metro status-active' : 'status-metro status-suspended'">
{{creneauMedecin.creneau.text}}
</span>
</td>
<td>
<span>{{creneauMedecin.rv.client.titre}} {{creneauMedecin.rv.client.prenom}} {{creneauMedecin.rv.client.nom}}</span>
</td>
<td>
<a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active">
</a>
<a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended">
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
- Linhas 4-5: Recorde-se que [agenda.data] é o calendário e que [agenda.data.creneauxMedecin] é uma matriz de objetos do tipo [creneauMedecin]. Cada elemento deste tipo possui um atributo [creneauMedecin.creneau], que é um intervalo de tempo. Cada intervalo de tempo possui dois elementos que nos interessam:
- [doctorSlot.slot.appointment], que é a consulta (se houver; appointment ≠ null) agendada para o intervalo;
- [doctorSlot.slot.text], que é o texto [início:fim] do intervalo de tempo;
- linha 4: exibe uma mensagem especial se o médico não tiver intervalos de tempo. Isto é improvável, mas acontece que a nossa base de dados está incompleta e este cenário ocorre. O facto de a mensagem ser ou não renderizada em HTML é controlado pela diretiva [ng-if];

A diretiva [ng-if] é diferente das diretivas [ng-show, ng-hide]. Estas últimas simplesmente ocultam uma área presente no documento. Se [ng-if='false'], então a área é removida do documento. Utilizámo-la aqui para fins ilustrativos;
- Linha 9: O atributo [id='creneaux'] é importante. É utilizado na seguinte instrução:
$("#creneaux").footable();
- linhas 10–22: exibem os cabeçalhos da tabela [1];
- linhas 23–45: exibem o conteúdo da tabela [2];
![]() |
- linha 24: percorremos o array [agenda.data.creneauxMedecin];
- linhas 26–29: o texto é renderizado [3]. A diretiva [ng-class] é utilizada para gerar o atributo [class] do elemento. Aqui, se [creneauMedecin.rv == null], isso significa que o horário está disponível, e o texto recebe um fundo verde. Caso contrário, recebe um fundo vermelho;
- linha 32: escrevemos o nome do cliente para quem a consulta foi marcada [4]. Se [rv==null], esta informação não existe, mas o Angular lida com este caso corretamente e não gera um erro;
- Linhas 34–39: Exibe um de dois botões: [Marcar] ou [Apagar]. A existência ou não de uma consulta determina qual o botão selecionado;
3.7.8.5. Modificação do servidor web
Tal como nos exemplos anteriores, o servidor web deve ser modificado para que a URL [/getAgendaMedecinJour] envie os cabeçalhos CORS:
![]() |
Na classe [RdvMedecinsCorsController], adicione um novo método:
// doctor's diary
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getAgendaMedecinJour(HttpServletResponse response) {
sendOptions(response);
}
Este método enviará os cabeçalhos CORS para a solicitação HTTP [OPTIONS]. Temos de fazer o mesmo para a solicitação HTTP [GET] na classe [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}
3.7.8.6. Utilização de diretivas
Tal como fizemos anteriormente, vamos transferir a manipulação do DOM para diretivas. Temos duas manipulações do DOM:
- quando a vista é exibida inicialmente:
// on style la liste déroulante
$timeout(function () {
$('.selectpicker').selectpicker();
});
- Quando o calendário é exibido:
// we create an evt to style the table after the view is displayed
$timeout(function () {
$("#creneaux").footable();
});
No primeiro caso, utilizaremos a diretiva [selectEnable] já apresentada. No segundo caso, criamos a diretiva [ footable] no seguinte ficheiro JS [footable.js]:
angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive footable");
$timeout(function () {
$("#creneaux").footable();
})
}
}
}]);
Por isso, utilizamos a mesma técnica que para a diretiva [selectEnable].
O código HTML [app-18.html] é duplicado em [app-18B.html]. Em seguida, modificamo-lo da seguinte forma:
<select data-style="btn-primary" class="selectpicker" select-enable="">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
- Linha 1: Aplique a diretiva [selectEnable] (através do atributo [select-enable]) à tag <select> para médicos;
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table" footable="">
<thead>
<tr>
- linha 3: a diretiva [footable] (através do atributo [footable]) é aplicada à tabela HTML do calendário;
<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- directives -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- linhas 3-4: referenciam os ficheiros JS para ambas as diretivas;
- linha 1: o código JS de [app-18B.html] é o código JS de [app-18.html] duplicado no ficheiro [rdvmedecins-06B.js];
O ficheiro [rdvmedecins-06B.js] é idêntico ao ficheiro [rdvmedecins-06.js], exceto por dois detalhes. As linhas que manipulam o DOM foram removidas:
// on style la liste déroulante
$timeout(function () {
$('.selectpicker').selectpicker();
});
// we create an evt to style the table after the view is displayed
$timeout(function () {
$("#creneaux").footable();
});
Depois de fazer isto, a execução da aplicação [app-18B.html] produz os mesmos resultados que a execução de [app-18.html].
3.7.9. Exemplo 9: Criar e cancelar reservas
Apresentamos agora uma aplicação que permite criar e cancelar reservas.
3.7.9.1. Vista V da aplicação
Apresentaremos o seguinte formulário:
![]() |
- Em [1], pode efetuar uma reserva. A reserva será efetuada para um cliente aleatório;
- Em [2], pode eliminar as reservas que efetuou;
Duplicamos o ficheiro [app-18.html] como [app-19.html] e, em seguida, modificamos o código da seguinte forma:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- the diary -->
<div id="agenda" ng-show="agenda.show">
..
<!-- doctor's diary -->
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table" footable="">
...
<tbody>
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
...
<td>
<a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active" ng-click="reserver(creneauMedecin.creneau.id)">
</a>
<a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended" ng-click="supprimer(creneauMedecin.rv.id)">
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-07.js"></script>
<script type="text/javascript" src="footable.js"></script>
- linhas 5-7: a mensagem de carregamento é a mesma da versão anterior;
- linhas 10-12: a mensagem de erro é a mesma da versão anterior;
- linhas 15-36: o calendário é o mesmo da versão anterior, com duas exceções:
- linha 26: clicar no botão [book] (atributo ng-click) é tratado pelo método [reserve] do modelo M na vista V. É-lhe passado o número do intervalo de tempo da reserva;
- linha 26: clicar no botão [delete] é tratado pelo método [reserve] do modelo M na vista V. É-lhe passado o número da consulta a eliminar;
- linha 39: o código JavaScript que gere a aplicação encontra-se no ficheiro [rdvmedecins-07.js];
- linha 40: o código JS para a diretiva [footable] aplicada na linha 20;
3.7.9.2. O controlador C
O código JavaScript para [rdvmedecins-07.js] é primeiro criado através da cópia do ficheiro [rdvmedecins-06.js]. Em seguida, é modificado. Os grandes blocos de código habituais permanecem. As alterações são feitas principalmente no controlador:

Descreveremos o controlador C para a vista V em várias etapas.
3.7.9.3. Inicialização do controlador C
O código de inicialização do controlador é o seguinte:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
$scope.errors = {show: false, model: {}};
$scope.medecins = {
data: [
{id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
{id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
{id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
{id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
],
title: config.listMedecins
};
var médecin = $scope.medecins.data[0];
var clients = [
{id: 1, version: 1, titre: "Mr", nom: "MARTIN", prenom: "Jules"},
{id: 2, version: 1, titre: "Mme", nom: "GERMAN", prenom: "Christine"},
{id: 3, version: 1, titre: "Mr", nom: "JACQUARD", prenom: "Maurice"},
{id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
];
// for the date
angular.copy(config.locales['fr'], $locale);
var today = new Date();
var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
var fullDay = $filter('date')(today, 'fullDate');
$scope.agenda = {title: config.agendaTitle, data: undefined, show: false, model: {titre: médecin.titre, prenom: médecin.prenom, nom: médecin.nom, jour: fullDay}};
// ---------------------------------------------------------------- agenda initial
// the global asynchronous task
var task;
// we ask for the agenda
getAgenda();
// ------------------------------------------------------------------ réservation
$scope.reserver = function (creneauId) {
....
};
// ------------------------------------------------------------ suppression RV
$scope.supprimer = function (idRv) {
...
};
// obtaining the agenda
function getAgenda() {
...
}
// cancel wait
function cancel() {
...
}
} ]);
- linha 6: configuração da mensagem de espera. Por predefinição, esperaremos 3 segundos antes de efetuar um pedido HTTP;
- linha 7: informações necessárias para as solicitações HTTP;
- linha 8: configuração da mensagem de erro;
- linhas 9–17: médicos predefinidos;
- linha 18: um médico específico. Serão feitas reservas para os horários deste médico;
- linhas 19–24: clientes codificados;
- linha 26: queremos lidar com datas em formato francês;
- linha 27: as consultas serão agendadas para a data de hoje;
- linha 28: o serviço de reservas online espera datas no formato «aaaa-mm-dd»;
- linha 29: data de hoje no formato [quinta-feira, 26 de junho de 2014];
- linha 30: configuração do calendário. O atributo [model] contém os parâmetros da mensagem internacionalizada a ser exibida:
agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
- linha 35: a variável global [task] representa a tarefa assíncrona atualmente em execução num determinado momento;
- linha 37: o calendário inicial é solicitado;
Isso é tudo o que é feito durante o carregamento inicial da página. Se tudo correr bem, a visualização exibe o calendário da Sra. PELISSIER para o dia.

3.7.9.4. Recuperação do calendário
O calendário é recuperado utilizando o seguinte método [getAgenda]:
// obtaining the agenda
function getAgenda() {
// the URL service path
var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
// we ask for the agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// waiting msg
$scope.waiting.visible = true;
// we analyze the result of the call to service [dao]
task.promise.then(function (result) {
// end of wait
$scope.waiting.visible = false;
// mistake?
if (result.err == 0) {
// we prepare the agenda model
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// timetable display formatting
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
} else {
// mistakes were made in obtaining the agenda
$scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
}
});
}
Este código é idêntico ao estudado na aplicação anterior. Existem duas alterações:
- não há espera simulada antes da chamada HTTP;
- linha 4: usamos o médico criado durante a inicialização do controlador, bem como o dia formatado que foi construído;
Este código foi isolado numa função porque também é utilizado pelas funções [reserve] e [delete].
3.7.9.5. Reservar um horário
![]() | ![]() |
Lembre-se de que os clientes são escolhidos aleatoriamente.
O código de reserva é o seguinte:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// we create a RV with a random customer in the slot identified by [id]
var idClient = clients[Math.floor(Math.random() * clients.length)].id;
utils.debug("réservation du créneau pour le client", idClient);
// simulated waiting
$scope.waiting.visible = true;
var task = utils.waitForSomeTime($scope.waiting.time);
// we add the
var promise = task.promise.then(function () {
// the URL service path
var path = config.urlSvrResaAdd;
// data to be sent to the service
var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
// start the asynchronous task
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// we return the promise of task completion
return task.promise;
});
// task result analysis
promise = promise.then(function (result) {
if (result.err != 0) {
// there were errors in validating the appointment
$scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
} else {
// we ask for the new agenda
getAgenda();
}
});
};
- linha 1: note que o parâmetro da função [reserve] é o número do slot (atributo id);
- linha 4: É selecionado aleatoriamente um cliente da lista de clientes codificada no código de inicialização. Guardamos o seu identificador [id];
- linhas 7–8: a espera de 3 segundos;
- linhas 11–18: estas linhas são executadas apenas após os 3 segundos terem decorrido;
- linha 12: o URL do serviço de reservas [/ajouterRv]. Este URL é diferente dos que encontrámos até agora. Está definido da seguinte forma no serviço web:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
- (continuação)
- linha 1: a URL não tem parâmetros e é solicitada via POST;
- linha 2: os parâmetros enviados estão na forma de um objeto JSON. Este será deserializado para o parâmetro [post] (@RequestBody);
Vimos um exemplo deste POST (secção 2.12.2):
![]() |
- em [0], a URL do serviço web;
- em [1], é utilizado o método POST;
- em [2], o texto JSON das informações enviadas ao serviço web no formato {dia, clientId, slotId};
- em [3], o cliente informa ao serviço web que está a enviar dados JSON;
Voltemos ao código JS da função [reserve]:
- linha 14: criamos o valor a ser enviado na forma de um objeto JS. O Angular irá serializá-lo em JSON quando for enviado;
- linha 16: a solicitação HTTP é feita. O valor a ser enviado é o último parâmetro da função [dao.getData]. Quando este parâmetro está presente, a função [dao.getData] executa um POST em vez de um GET (ver o código na secção 3.7.6.4);
- Linha 18: A promessa da chamada HTTP é devolvida;
- linhas 23–29: são executadas apenas quando a chamada HTTP tiver devolvido a sua resposta;
- linha 23: o parâmetro [result] tem o formato [err,data] ou [err,messages], em que [err] é um código de erro;
- linhas 23–26: se houver erros, a mensagem de erro é exibida;
- linha 28: se a reserva foi bem-sucedida, o novo calendário é exibido novamente;
3.7.9.6. Modificação do servidor
![]() |
Na classe [RdvMedecinsCorsController], adicionamos o seguinte método:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// we authorize the header [authorization]
response.addHeader("Access-Control-Allow-Headers", "authorization");
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
public void ajouterRv(HttpServletResponse response) {
sendOptions(response);
}
A adição é feita nas linhas 10–13. Os cabeçalhos nas linhas 2–8 serão enviados para o URL [/addAppt] (linha 10) e o método HTTP [OPTIONS] (linha 10).
A classe [RdvMedecinsController] é modificada da seguinte forma:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.ajouterRv(response);
...
Para o método [POST] (linha 1) e a URL [/addAppointment] (linha 1), o método que acabámos de adicionar ao [RdvMedecinsCorsController] é chamado (linha 4), devolvendo assim os mesmos cabeçalhos HTTP que para o método HTTP [OPTIONS].
3.7.9.7. Testes
Vamos realizar um teste inicial em que marcamos qualquer horário disponível:
![]() |
Como sempre nestes casos, precisamos de verificar os registos da consola:
[dao] getData[/ajouterRv] error réponse : {"data":"","status":0,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/ajouterRv","data":{"jour":"2014-06-30","idCreneau":1,"idClient":4},"headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4=","Content-Type":"application/json;charset=utf-8"}},"statusText":""}
O método [dao.getData] falhou com [status=0], o que significa que o Angular cancelou o pedido. A causa do erro encontra-se nos registos:
XMLHttpRequest cannot load http://localhost:8080/ajouterRv. Request header field Content-Type is not allowed by Access-Control-Allow-Headers.
Se analisarmos o tráfego de rede, vemos o seguinte:
![]() |
- em [1] e [2]: houve apenas uma solicitação HTTP, a solicitação [OPTIONS];
- em [3], o cliente Angular solicita duas permissões:
- permissão para enviar os cabeçalhos HTTP [accept, authorization, content-type];
- permissão para enviar uma solicitação POST;
- em [4]: o servidor autoriza o cabeçalho [authorization]. Lembre-se de que, no lado do servidor, somos nós que enviamos esta autorização;
A novidade é que, para uma operação POST, o cliente Angular solicita autorizações adicionais ao servidor. Devemos, portanto, modificar o servidor para concedê-las:
![]() |
Na classe [RdvMedecinsCorsController], modificamos o método privado que gera os cabeçalhos HTTP enviados para os pedidos OPTIONS, GET e POST:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// the POST is authorized
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
- linha 7: adicionámos uma autorização para os cabeçalhos HTTP [accept, content-type];
- linha 9: adicionámos uma autorização para o método POST;
Executamos novamente o teste após reiniciar o servidor:
![]() |
Desta vez, a reserva foi bem-sucedida.
3.7.9.8. Eliminar um compromisso
![]() | ![]() |
O código para a função [delete] é o seguinte:
$scope.supprimer = function (idRv) {
utils.debug("suppression rv n°", idRv);
// simulated waiting
$scope.waiting.visible = true;
task = utils.waitForSomeTime($scope.waiting.time);
// we add the
var promise = task.promise.then(function () {
// the URL service path
var path = config.urlSvrResaRemove;
// data to be sent to the service
var post = {idRv: idRv};
// start the asynchronous task
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// we return the promise of task completion
return task.promise;
});
// task result analysis
promise = promise.then(function (result) {
if (result.err != 0) {
// there have been errors deleting the rv
$scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
// the UI is updated
$scope.waiting.visible = false;
} else {
// we ask for the new agenda
getAgenda();
}
});
};
- linha 1: lembre-se de que o parâmetro da função é o ID do compromisso a ser eliminado. Este código é muito semelhante ao código de marcação. Iremos apenas comentar as diferenças;
- linha 9: o URL do serviço aqui é [/deleteAppointment] e, tal como anteriormente, é acedido através de um pedido POST:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
O parâmetro enviado é novamente transmitido no formato JSON. Na secção 2.12.17, demonstrámos a natureza do POST realizado manualmente:
![]() |
- em [1], a URL do serviço web;
- em [2], o método POST é utilizado;
- Em [3], o texto JSON da informação enviada ao serviço web na forma {idRv};
- em [4], o cliente informa ao serviço web que está a enviar dados JSON;
Voltemos ao código JS da função [delete]:
- linha 11: criamos o objeto postado. O Angular irá serializá-lo automaticamente para JSON;
O resto do código é semelhante ao da reserva.
3.7.9.9. Alterações no lado do servidor
No lado do servidor, fazemos as seguintes alterações:
![]() |
Na classe [RdvMedecinsCorsController], adicionamos o seguinte método:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// the POST is authorized
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
...
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(HttpServletResponse response) {
sendOptions(response);
}
A adição é feita nas linhas 13–16. Os cabeçalhos das linhas 2–10 serão enviados para a URL [/deleteAppointment] (linha 13) e o método HTTP [OPTIONS] (linha 13).
A classe [RdvMedecinsController] é modificada da seguinte forma:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.supprimerRv(response);
...
Para o método [POST] (linha 1) e o URL [/deleteAppointment] (linha 1), o método que acabámos de adicionar ao [RdvMedecinsCorsController] é chamado (linha 4), devolvendo assim os mesmos cabeçalhos HTTP que para o método HTTP [OPTIONS].
3.7.10. Exemplo 10: Criação e cancelamento de consultas - 2
Apresentamos agora a mesma aplicação de antes, mas em vez de marcar uma consulta para um cliente aleatório, o cliente será selecionado a partir de uma lista suspensa.
3.7.10.1. A vista V da aplicação
Apresentaremos o seguinte formulário:
![]() |
Os clientes serão selecionados em [1].
O código é semelhante ao da aplicação anterior, pelo que apenas apresentaremos as principais diferenças.
Duplicamos o ficheiro [app-19.html] para [app-20.html] e, em seguida, criamos o código para a lista suspensa de clientes [1]:
<!-- customer list -->
<div class="alert alert-info">
<h3>{{agenda.title|translate:agenda.model}}</h3>
<div class="row" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
- linhas 8–12: a lista suspensa será implementada utilizando o componente [bootstrap-select];
- linha 1: a diretiva [selectEnable] é aplicada através do atributo [select-enable];
- linha 1: a tag <select> é gerada apenas se [clients.data] existir (# null, undefined). Este ponto é importante e foi explicado na secção 3.7.7.8;
Além disso, importamos novos ficheiros JS:
<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- directives -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- Linha 1: O ficheiro [rdvmedecins-08.js] é criado através da cópia do ficheiro [rdvmedecins-0.js];
- Linhas 3-4: Os ficheiros para ambas as diretivas são importados;
3.7.10.2. Controlador C
O código para o controlador C evolui da seguinte forma:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- model initialization
...
// our customers
$scope.clients = {title: config.listClients, show: false, model: {}};
//------------------------------------------- initilisation vue
// the global asynchronous task
var task;
// we ask for the customers, then the agenda
getClients().then(function () {
getAgenda();
});
...
// execution action
function getClients() {
....
};
} ]);
- linha 8: o objeto [$scope.clients] configura a lista suspensa de clientes na vista V;
- linhas 14–16: de forma assíncrona, primeiro solicitamos a lista de clientes e, depois de obtida, solicitamos a agenda da Sra. PELISSIER para hoje. A sintaxe aqui utilizada funciona apenas porque a função [getClients] devolve uma promessa;
O método [getClients] recupera a lista de clientes:
function getClients() {
// the UI is updated
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// we ask for the customer list;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analyze the result of the previous call
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// we put the acquired data into the model
$scope.clients.data = result.data;
// the UI is updated
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// there were errors in obtaining the customer list
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
// we return the promise
return promise;
};
Este é um código que já vimos e discutimos. A parte importante a destacar é a linha 31:
- linha 27: devolvemos a promessa da linha 10, ou seja, a última promessa obtida no código. Esta promessa só será cumprida assim que o pedido HTTP tiver devolvido a sua resposta;
O método [reserve] sofre uma ligeira alteração:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// on crée un RV pour le client sélectionné
var idClient = $(".selectpicker").selectpicker('val');
...
});
- Linha 4: Já não fazemos reservas para um cliente aleatório, mas sim para o cliente selecionado da lista de clientes.
3.7.11. Exemplo 11: uma diretiva [selectEnable2]
Este exemplo retoma as diretivas.
3.7.11.1. A vista V
A aplicação apresenta a seguinte vista:
![]() |
3.7.11.2. O código HTML da vista
O código HTML para a vista [app-21.html] é o seguinte:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- customer list -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- list of doctors -->
<div class="alert alert-info">
<div class="row" ng-show="medecins.show">
<div class="col-md-4">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
</div>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-09.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="selectEnable2.js"></script>
- linhas 19–23: a lista suspensa do cliente;
- linha 19: a diretiva [selectEnable2] (atributo [select-enable2]) é aplicada;
- linha 19: apenas se [clients.data] não estiver vazio;
- linha 19: a lista suspensa é identificada pelo atributo [id="selectpickerClients"];
- linhas 33-37: a lista suspensa de médicos;
- linha 33: a diretiva [selectEnable2] é aplicada (atributo [select-enable2]);
- linha 33: apenas se [doctors.data] não estiver vazio;
- linha 33: a lista suspensa é identificada pelo atributo [id="selectpickerMedecins"];
- linha 43: um novo ficheiro JS [rdvmedecins-09.js] é importado;
- linha 45: o ficheiro JS para a nova diretiva é importado;
3.7.11.3. A diretiva [selectEnable2]
O código para a diretiva [selectEnable2] é o seguinte:
angular.module("rdvmedecins").directive('selectEnable2', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable2 attrs", attrs);
$timeout(function () {
$('#' + attrs['id']).selectpicker();
})
}
}
}]);
- linha 4: exibimos o valor do parâmetro [attrs] para ajudar a compreender como o código funciona. Veremos que attrs['id']='selectpickerClients' para a lista de clientes;
- linha 6: para localizar um elemento com [id='x'] no DOM, escrevemos [$('#x')]. Portanto, devemos escrever [$('#selectpickerClients')] para localizar a lista de clientes. Isto é conseguido utilizando a sintaxe [$('#' + attrs['id'])];
A diretiva [selectEnable2] utiliza, portanto, a informação contida num dos atributos do elemento HTML ao qual é aplicada.
3.7.11.4. Controlador C
O controlador C encontra-se no ficheiro JS [rdvmedecins-09.js] e tem a seguinte estrutura:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- model initialization
// the waiting msg
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
// login information
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
// errors
$scope.errors = {show: false, model: {}};
// the doctors
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
// our customers
$scope.clients = {title: config.listClients, show: false, model: {}};
// the global asynchronous task
var task;
// ---------------------------------------------------- initialisation vue
// the UI is updated
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.medecins.show = false;
$scope.errors.show = false;
// we ask for customers, then doctors
getClients().then(function () {
getMedecins();
});
// customer list
function getClients() {
...
}
// list of doctors
function getMedecins() {
...
}
// cancel wait
function cancel() {
...
}
} ]);
- linhas 26–28: primeiro consultamos os clientes, depois os médicos;
3.7.11.5. Testes
Teste esta nova versão.
3.7.12. Exemplo 12: Uma diretiva [list]
Vamos usar o mesmo exemplo de antes, mas queremos simplificar o código HTML utilizando uma diretiva. Atualmente, temos o seguinte código HTML:
<!-- customer list -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- list of doctors -->
<div class="alert alert-info">
<div class="row" ng-show="medecins.show">
<div class="col-md-4">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
</div>
</div>
As linhas 14–26 são idênticas às linhas 1–13. Aplicam-se aos médicos em vez de aos clientes. Gostaríamos de poder escrever o seguinte:
<!-- la liste des clients -->
<list model="clients" ng-if="clients.show"></list>
<!-- la liste des médecins -->
<list model="medecins" ng-if="medecins.show"></list>
Este código utiliza uma nova diretiva [list], que iremos criar agora.
3.7.12.1. A diretiva [list]
A diretiva [list] é colocada no ficheiro JS [list.js]. O seu código é o seguinte:
angular.module("rdvmedecins")
.directive("list", ['utils', '$timeout', function (utils, $timeout) {
// instance de la directive retournée
return {
// élément HTML
restrict: "E",
// url du fragment
templateUrl: "list.html",
// scope unique à chaque instance de la directive
scope: true,
// fonction lien avec le document
link: function (scope, element, attrs) {
utils.debug("directive list attrs", attrs);
scope.model = scope[attrs['model']];
utils.debug("directive list model", scope.model);
$timeout(function () {
$('#' + scope.model.id).selectpicker();
})
}
}
}]);
- linha 2: define uma diretiva chamada «list»;
- linha 6: o atributo [restrict] especifica como a diretiva pode ser utilizada. [restrict: "E"] significa que a diretiva [list] pode ser utilizada como um elemento HTML <list ...>...</list>. [restrict: "A"] significa que a diretiva [list] pode ser utilizada como um atributo, por exemplo <div ... list='...'>. [restrict: "AE"] significa que a diretiva [list] pode ser utilizada tanto como um atributo como um elemento;
- linha 8: o atributo [templateUrl] especifica o nome do fragmento HTML a ser utilizado quando a tag for encontrada. Este fragmento será o corpo da tag;
- linha 10: o atributo [scope] define o âmbito do modelo da diretiva. [scope: true] significa que dois elementos <list> terão, cada um, o seu próprio modelo. Por predefinição (scope não inicializado), partilham os seus modelos;
- linha 12: a função [link], que já utilizámos várias vezes;
Para compreender o código acima, é necessário lembrar-se de como a diretiva será utilizada:
<!-- la liste des clients -->
<list model="clients" ng-if="clients.show"></list>
<!-- la liste des médecins -->
<list model="medecins" ng-if="medecins.show"></list>
A diretiva [list] é utilizada como um elemento HTML <list>. Este elemento possui dois atributos:
- [model]: que terá como valor o elemento do modelo M da vista V na qual a diretiva [list] está localizada. Este elemento irá preencher o modelo da diretiva;
- [ng-if]: que garante que o código HTML da diretiva não seja gerado se não houver nada para exibir;
Voltemos ao código da função [link] da diretiva:
link: function (scope, element, attrs) {
utils.debug("directive list attrs", attrs);
scope.model = scope[attrs['model']];
utils.debug("directive list model", scope.model);
$timeout(function () {
$('#' + scope.model.id).selectpicker();
})
}
Vamos combinar este código JS com o código HTML que utiliza a diretiva:
<list model="clients" ng-if="clients.show"></list>
- linha 3: attrs['model'] tem aqui o valor 'clients';
- linha 3: scope[attrs['model']] tem o valor scope['clients'] e, portanto, representa [$scope.clients], ou seja, o campo [clients] do modelo de visualização. Este campo terá o valor {id:'...', data:[client1, client2, ...], show:..., title:'...'};
- linha 3: adicionamos um campo [model] ao modelo da diretiva. Este campo herda do modelo da vista em que se encontra. Devemos, portanto, evitar conflitos com qualquer campo [model] que a vista também possa ter. Aqui, não haverá conflito;
- linha 4: exibimos [scope.model] para compreender melhor o código;
- linhas 5-7: vemos código que já encontrámos anteriormente. A diferença é que o ID do componente era anteriormente recuperado a partir de um atributo attrs['id']. Aqui, será recuperado a partir de [scope.model.id];
Agora, vamos analisar o código HTML gerado pela diretiva. Devido ao atributo [templateUrl: "list.html"] da diretiva, precisamos de procurá-lo no ficheiro [list.html]:
<!-- a list of customers or doctors -->
<div class="alert alert-info" ng-show="model.show">
<div class="row">
<div class="col-md-4">
<h2 translate="{{model.title}}"></h2>
<select data-style="btn-primary" id="{{model.id}}" ng-if="model.data">
<option ng-repeat="element in model.data" value="{{element.id}}">
{{element.titre}} {{element.prenom}} {{element.nom}}
</option>
</select>
</div>
</div>
</div>
- A primeira coisa a ter em conta ao ler este código é que a diretiva criou um objeto [scope.model] com o formato [{id:'...', data:[client1, client2, ...], show:..., title:'...'}]. Este objeto [model] (scope está implícito no código HTML) é utilizado pelo código HTML da diretiva;
- linha 2: utilização de [model.show] para mostrar/ocultar a vista gerada pela diretiva;
- linha 5: utilização de [model.title] para definir um título;
- linha 6: utilização de [model.id] para atribuir um ID à tag <select>. Este ID é utilizado pelo código JavaScript da diretiva;
- linha 6: utilização de [model.data] para gerar o <select> apenas se houver dados para apresentar;
- linhas 7–9: utilização de [model.data] para gerar os itens da lista suspensa;
3.7.12.2. O código HTML
O código HTML para a aplicação [app-22.html] é o seguinte:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- customer list -->
<list model="clients" ng-if="clients.show"></list>
<!-- list of doctors -->
<list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="list.js"></script>
- linha 22: não se esqueça de incluir o código JS para a diretiva;
3.7.12.3. O controlador C
O controlador C sofre poucas alterações:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- model initialization
...
// the doctors
$scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
// our customers
$scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
- Linhas 7 e 9: Adicionamos o atributo [id] aos modelos médicos e clientes;
3.7.12.4. Os testes
Os testes produzem os mesmos resultados que no exemplo anterior.
3.7.13. Exemplo 13: Atualização do modelo de uma diretiva
Continuamos o nosso estudo das diretivas e mantemos o exemplo da lista suspensa. Aqui, queremos examinar o comportamento da diretiva [list] quando o conteúdo da lista suspensa muda.
3.7.13.1. As vistas V
As diferentes visualizações são as seguintes:
![]() |
- em [1], solicitamos a lista de clientes pela primeira vez;
![]() |
- em [2], solicitamos a lista de clientes pela segunda vez. Esta segunda lista é então combinada com a primeira [3]. É a atualização do componente [Bootstrap select] que queremos examinar neste exemplo.
3.7.13.2. A página HTML
A página HTML [app-23.html] é criada através da cópia de [app-22.html] e, em seguida, modificada da seguinte forma:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- le message d'attente -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la liste d'erreurs -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- directives -->
<script type="text/javascript" src="list2.js"></script>
As alterações em relação à aplicação anterior são as seguintes:
- linhas 15–17: adição de um botão;
- linha 20: utilização de uma nova diretiva [list2];
- linha 23: utilização de um novo ficheiro JS;
- Linha 25: Importação do ficheiro JS a partir da diretiva [list2];
3.7.13.3. A diretiva [list2]
A diretiva [list2] em [list2.js] é a seguinte:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instance de la directive retournée
return {
// élément HTML
restrict: "E",
// url du fragment
templateUrl: "list.html",
// scope unique à chaque instance de la directive
scope: true,
// fonction lien avec le document
link: function (scope, element, attrs) {
utils.debug('directive list2');
scope.model = scope[attrs['model']];
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
}
}
}]);
A única diferença em relação à diretiva [list] está na linha 16: com o método [selectpicker('refresh')], dizemos ao componente [Bootstrap-select] para atualizar. A ideia por trás disso é que, sempre que o utilizador solicitar uma nova lista de clientes, a lista suspensa será atualizada. Não vai funcionar, mas essa é a ideia básica.
3.7.13.4. O controlador C
O controlador encontra-se no ficheiro [rdvmedecins-11.js], criado através da cópia do ficheiro [rdvmedecins-10.js]:
// our customers
$scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
// customer list
$scope.getClients = function getClients() {
// the UI is updated
$scope.waiting.visible = true;
$scope.errors.show = false;
// we ask for the customer list;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analyze the result of the previous call
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// put the acquired data into a new model to force the view to refresh
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
// the UI is updated
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// there were errors in obtaining the customer list
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
}
- linha 1: para permitir que as matrizes sejam concatenadas em [clients.data], este objeto é inicializado com uma matriz vazia;
- linha 18: concatenamos a nova lista de clientes com as já presentes na matriz [clients.data];
Anteriormente, tínhamos escrito:
Agora escrevemos:
Para compreender este código, é necessário recordar como o modelo M é utilizado na vista V no caso da diretiva [list2]:
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
O modelo utilizado pela diretiva [list2] é [clients]. Só será reavaliado na vista V se [clients] sofrer alterações no modelo M da vista. A primeira ideia que vem à mente para a modificação é escrever:
para ter em conta o facto de que a nova lista de clientes deve ser anexada às anteriores. Ao fazê-lo, modifica-se [clients.data], mas não [clients]. Não estou familiarizado com as complexidades do JavaScript, mas não seria surpreendente se [clients] fosse um ponteiro, tal como [clients.data]. O ponteiro [clients] não muda quando alteramos o ponteiro [clients.data]. A diretiva [list2] não é, portanto, reavaliada. É precisamente isto que observamos ao depurar a aplicação (F12 no Chrome).
Ao escrever:
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
Asseguramos que [$scope.clients] recebe efetivamente um novo valor. O ponteiro [$scope.clients] aponta para um novo objeto. A diretiva [list2] deve então ser reavaliada. No entanto, não obtemos o resultado desejado. Vamos examinar as capturas de ecrã quando solicitamos a lista de clientes duas vezes:
![]() |
- em [1], temos apenas quatro elementos em vez de oito;
- em [2], estes quatro elementos estão num elemento [select], mas estão ocultos (style='display: none');
![]() |
- em [3], encontramos os quatro clientes num layout HTML diferente, e é isto que o utilizador vê quando clica na lista suspensa;
Por fim, os registos da consola mostram o seguinte:
- linha 1: o serviço [dao] é instanciado;
- linha 2: o serviço [dao] recupera uma lista inicial de clientes;
- linha 3: a diretiva [list2] é executada;
- linha 4: o serviço [dao] recupera uma segunda lista de clientes;
A saída na linha 2 provém do seguinte código na diretiva:
link: function (scope, element, attrs) {
utils.debug('directive list2');
...
}
Vamos examinar o ciclo de vida da diretiva [list2]:
- Entre as linhas 1 e 2, ela não é ativada, mesmo que a vista tenha sido exibida pela primeira vez. Isto deve-se ao seu atributo [ng-if="clients.show"] na vista V:
<list2 model="clients" ng-if="clients.show"></list2>
- linha 3: após recuperar a primeira lista de médicos, [clients.show] torna-se verdadeiro e a diretiva é ativada;
- após recuperar a segunda lista de clientes, vemos que o código da diretiva [list2] não é chamado. É por isso que não vemos a segunda lista;
Para resolver este problema, modificamos a diretiva [list2] da seguinte forma:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instance de la directive retournée
return {
// élément HTML
restrict: "E",
// url du fragment
templateUrl: "list.html",
// scope unique à chaque instance de la directive
scope: true,
// fonction lien avec le document
link: function (scope, element, attrs) {
// à chaque fois que attrs["model"] change, le modèle de la directive doit changer également
scope.$watch(attrs["model"], function (newValue) {
utils.debug("directive list2 newValue", newValue);
// on met à jour le modèle de la directive
scope.model = newValue;
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
});
}
}
}]);
- Linha 14: A função [scope.$watch] permite-lhe observar um valor no modelo. A sua sintaxe é [scope.$watch('var'), f], em que [var] é o identificador de uma variável no modelo e f é a função a executar quando essa variável muda de valor. Aqui, queremos observar a variável [clients]. Por isso, temos de escrever [scope.$watch('clients')]. Como temos attrs['model']='clients', escrevemos [scope.$watch(attrs["model"], function (newValue)];
- linha 14: o segundo parâmetro da função [scope.$watch] é a função a ser executada quando a variável observada muda de valor. O parâmetro [newValue] é o novo valor da variável, ou seja, para nós, o novo valor da variável [clients] no modelo;
- linha 17: este novo valor é atribuído ao campo [model] do modelo da diretiva;
Com esta alteração efetuada, os registos mudam:
![]() |
Acima, vemos que, após obter a segunda lista de clientes, a diretiva [list2] é de facto executada novamente, conforme confirmado pelo resultado [2].
3.7.14. Exemplo 14: as diretivas [waiting] e [errors]
Voltemos ao código HTML da aplicação anterior:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- le message d'attente -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la liste d'erreurs -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
- linhas 5-7: a mensagem de carregamento;
- linhas 10-12: a mensagem de erro;
Decidimos colocar o código HTML destas duas mensagens dentro de diretivas.
3.7.14.1. O novo código HTML
O novo código HTML [app-24.html] é o seguinte:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- le message d'attente -->
<waiting model="waiting"></waiting>
<!-- la liste d'erreurs -->
<errors model="errors"></errors>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- directives -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
- linha 5: a diretiva para a mensagem de espera;
- linha 8: a diretiva para a mensagem de erro;
- linha 19: o novo ficheiro JS associado à aplicação;
- linhas 21–23: os ficheiros JS para as três diretivas;
3.7.14.2. A diretiva [waiting]
O código JS para a diretiva [waiting] encontra-se no seguinte ficheiro [waiting.js]:
angular.module("rdvmedecins")
.directive("waiting", ['utils', function (utils) {
// returned directive instance
return {
// element HTML
restrict: "E",
// fragment url
templateUrl: "waiting.html",
// scope unique to each directive instance
scope: true,
// function link to document
link: function (scope, element, attrs) {
// each time attr["model"] changes, the page model must also change
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[waiting] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Este código segue a mesma lógica da diretiva [list2] já discutida.
Na linha 8, fazemos referência ao seguinte ficheiro [waiting.html]:
<div class="alert alert-warning" ng-show="model.show">
<h1>{{ model.title.text | translate:model.title.values}}
<button class="btn btn-primary pull-right" ng-click="model.cancel()">{{'cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
No código JS da aplicação, o modelo [$scope.waiting] para este código HTML será definido da seguinte forma:
// the waiting msg
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time: 3000};
3.7.14.3. A diretiva [errors]
O código JS para a diretiva [errors] encontra-se no seguinte ficheiro [errors.js]:
angular.module("rdvmedecins")
.directive("errors", ['utils', function (utils) {
// returned directive instance
return {
// element HTML
restrict: "E",
// fragment url
templateUrl: "errors.html",
// scope unique to each directive instance
scope: true,
// function link to document
link: function (scope, element, attrs) {
// each time attr["model"] changes, the page model must also change
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[errors] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Este código segue a mesma lógica da diretiva [list2] já discutida.
Na linha 8, fazemos referência ao seguinte ficheiro [errors.html]:
<div class="alert alert-danger" ng-show="model.show">
{{model.title.text|translate:model.title.values}}
<ul>
<li ng-repeat="message in model.messages">{{message|translate}}</li>
</ul>
</div>
No código JS da aplicação, o modelo [$scope.errors] para este código HTML será definido da seguinte forma:
// there were errors in obtaining the customer list
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};
3.7.15. Exemplo 15: Navegação
Até agora, temos utilizado aplicações de página única. Neste exemplo, abordaremos aplicações com várias páginas e a navegação entre elas.
3.7.15.1. As vistas V da aplicação
![]() |
- em [1], o URL da vista n.º 1;
- em [2], o seu conteúdo;
- em [3], vamos para a página 2;
- em [4], visualização n.º 2;
- em [5], vamos para a página 3;
![]() |
- em [6], visualização n.º 3;
- em [7], vamos para a página 1;
- em [8], voltamos à vista n.º 1;
3.7.15.2. Organização do código
Estamos a iniciar uma nova organização do código:
![]() |
- as vistas da aplicação serão colocadas na pasta [views];
- o módulo da aplicação será colocado na pasta [modules];
- Os controladores da aplicação serão colocados na pasta [controllers];
Da mesma forma, na versão final:
- os serviços serão colocados na pasta [services];
- as diretivas serão colocadas na pasta [directives];
3.7.15.3. O contêiner de visualizações
As visualizações na pasta [views] serão apresentadas no seguinte contentor [app-25.html]:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container" ng-controller="mainCtrl">
<!-- the navigation bar -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- the current view -->
<ng-view></ng-view>
</div>
...
<!-- the module -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- controllers -->
<script type="text/javascript" src="controllers/mainController.js"></script>
<script type="text/javascript" src="controllers/page1Controller.js"></script>
<script type="text/javascript" src="controllers/page2Controller.js"></script>
<script type="text/javascript" src="controllers/page3Controller.js"></script>
</body>
</html>
- linha 7: o corpo do contentor é controlado por [mainCtrl];
- linha 9: a diretiva [ng-include] permite incluir um ficheiro HTML externo, neste caso uma barra de navegação;
- linha 12: as diferentes vistas apresentadas pelo contentor são renderizadas dentro da diretiva [ng-view]. Em última análise, temos um contentor que apresenta:
- sempre a mesma barra de navegação (linha 9);
- diferentes vistas na linha 12;
- linhas 16–22: importamos os ficheiros JS do módulo da aplicação [rdvmedecins-13.js] e dos seus controladores;
3.7.15.4. O módulo da aplicação
O ficheiro [rdvmedecins-13.js] define o módulo da aplicação e o encaminhamento entre vistas:
// --------------------- Angular module
angular.module("rdvmedecins", [ 'ngRoute' ]);
angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ routage
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
$routeProvider.when("/page2",
{
templateUrl: "views/page2.html",
controller: 'page2Ctrl'
});
$routeProvider.when("/page3",
{
templateUrl: "views/page3.html",
controller: 'page3Ctrl'
});
$routeProvider.otherwise(
{
redirectTo: "/page1"
});
}]);
- linha 1: define o módulo [rdvmedecins]. Este depende do módulo [ngRoute] fornecido pela biblioteca [angular-route.min.js]. Este módulo permite o encaminhamento definido nas linhas 6–24;
- linha 4: define a função [config] do módulo [rdvmedecins]. Note-se que esta função é executada antes de qualquer serviço ser instanciado. Trata-se de uma função de configuração do módulo. Aqui, o seu encaminhamento é configurado. Isto é feito utilizando o objeto [$routeProvider] fornecido pelo módulo [ngRoute];
- Linhas 6–10: definem a vista a apresentar quando o utilizador solicita o URL [/page1]. Trata-se de um encaminhamento interno dentro da aplicação. A URL é, na verdade, [/rdvmedecins-angular-v1/app-21.html#/page1]. Podemos ver que continua a ser utilizada a URL do contentor [/rdvmedecins-angular-v1/app-21.html], mas com informações adicionais a seguir ao caractere #. É esta informação adicional que o encaminhamento do Angular trata;
- linha 8: especifica o fragmento HTML a ser inserido na diretiva [ng-view] do contêiner:
- linha 9: especifica o nome do controlador para este fragmento;
- linhas 11–15: definem a vista a ser exibida quando o utilizador solicita a URL [/page2];
- linhas 16–20: definem a vista a ser exibida quando o utilizador solicita o URL [/page3];
- linhas 21–24: definem o roteamento a ser executado quando a URL solicitada não for uma das três anteriores (caso contrário, linha 21);
- linha 23: redireciona para a URL [/page1] e, assim, para a vista definida nas linhas 6–10;
3.7.15.5. O controlador do contentor de vistas
Vimos que o contêiner de visualização declarou um controlador:
<div class="container" ng-controller="mainCtrl">
O controlador [mainCtrl] está definido no ficheiro [mainController.js]:
// controller
angular.module("rdvmedecins")
.controller('mainCtrl', ['$scope', '$location',
function ($scope, $location) {
// page templates
$scope.page1 = {};
$scope.page2 = {};
$scope.page3 = {};
// global model
var main = $scope.main = {};
main.text = "[Modèle global]";
// methods exposed to view
main.showPage1 = function () {
$location.path("/page1");
};
main.showPage2 = function () {
$location.path("/page2");
};
main.showPage3 = function () {
$location.path("/page3");
}
}]);
- linha 3: o controlador [mainCtrl] necessita do objeto [$location] fornecido pelo módulo de roteamento [ngRoute]. Este objeto permite-lhe alterar as vistas (linhas 16, 19, 22);
Voltemos ao código do contêiner:
<div class="container" ng-controller="mainCtrl">
<!-- the navigation bar -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- the current view -->
<ng-view></ng-view>
</div>
- O controlador [mainCtrl] constrói o modelo para a área 1-7;
- a vista incluída na linha 6 também tem um controlador. Por exemplo, a vista [page1] tem o controlador [page1Ctrl]. Este controlador constrói o modelo para a área apresentada na linha 6. Temos, então, dois modelos nesta área:
- o modelo construído pelo controlador [mainCtrl];
- o modelo construído pelo controlador [page1Ctrl];
Existe uma convenção de nomenclatura para modelos. Na vista apresentada na linha 6, os modelos para os controladores [mainCtrl] e [pagexCtrl] estão ambos visíveis. Se duas variáveis nestes modelos tiverem o mesmo nome, uma substituirá a outra. Para evitar este conflito de nomenclatura, criamos quatro modelos com quatro nomes diferentes:
recipiente | mainCtrl | principal | 11 |
página1 | página1Ctrl | página1 | 7 |
página 2 | página2Ctrl | página2 | 8 |
página 3 | página3Ctrl | página3 | 9 |
- linha 12: define um elemento [text] no modelo [main];
As linhas 7–11 têm um efeito muito específico: definem o [$scope] do controlador [mainCtrl] e, dentro dele, criam quatro variáveis [main, page1, page2, page3]. Estas quatro variáveis serão utilizadas como os respetivos modelos para o contentor e as três vistas que este irá conter, por sua vez.
3.7.15.6. A barra de navegação
A barra de navegação é definida da seguinte forma no contentor:
<div class="container" ng-controller="mainCtrl">
<!-- the navigation bar -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- the current view -->
<ng-view></ng-view>
</div>
A barra de navegação é definida na linha 3. Isto significa que ela só reconhece o modelo [main]. O seu código é o seguinte:
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span ng-click="main.showPage1()">Page 1</span>
</a>
</li>
<li class="active">
<a href="">
<span ng-click="main.showPage2()">Page 2</span>
</a>
</li>
<li class="active">
<a href="">
<span ng-click="main.showPage3()">Page 3</span>
</a>
</li>
</ul>
</div>
</div>
</div>
- Nas linhas 16, 21 e 26, são utilizados métodos do modelo [main];
- linha 16: clicar no link [Page1] irá acionar a execução do método [$scope.main.showPage1]. Este está definido no controlador [mainCtrl] da seguinte forma:
// global model
var main = $scope.main = {};
main.text = "[Modèle global]";
// methods exposed to view
main.showPage1 = function () {
$location.path("/page1");
};
- Linha 6: A partir do código acima, podemos ver que o método [main.showPage1] é, na verdade, o método [$scope.main.showPage1]. Portanto, é este que será executado;
- linha 7: alteramos o URL da aplicação para [/page1]. Voltemos ao roteamento definido no módulo principal:
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
Podemos ver que o fragmento [views/page1.html] será inserido no contentor e que o seu controlador é [page1Ctrl].
3.7.15.7. A vista [/page1] e o seu controlador
O fragmento [views/page1.html] é o seguinte:
<h1>Page 1</h1>
<div class="alert alert-info">
<ul>
<li>Modèle global : {{main.text}}</li>
<li>Modèle local : {{page1.text}}</li>
</ul>
</div>
Lembre-se de que, na vista inserida no contentor, o modelo [main] está visível. É isso que queremos verificar na linha 4. Além disso, o controlador [page1Ctrl] para o fragmento [views/page1.html] define um modelo [page1]. É este que é utilizado na linha 5.
O código para o controlador [page1Ctrl] é o seguinte:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope',
function ($scope) {
// page 1 template
var page1=$scope.page1;
page1.text="[Modèle local dans page 1]";
}]);
- Linha 2: O [$scope] injetado aqui não está vazio. Uma vez que o controlador [page1Ctrl] controla uma área inserida num contentor controlado pelo [mainCtrl], o [$scope] na linha 2 contém os elementos do [$scope] definido pelo controlador [mainCtrl]. É importante compreender isto. O [$scope] definido pelo controlador [mainCtrl] contém os seguintes elementos: [main, page1, page2, page3]. Isto significa que temos acesso aos modelos de todas as vistas. Isto não é necessariamente desejável, mas é o que acontece aqui. Na versão final do cliente Angular, utilizaremos esta funcionalidade para armazenar informações que precisam de ser partilhadas entre vistas no modelo [main]. Isto será análogo ao conceito de «sessão» do lado do servidor;
- linha 6: recuperamos o modelo [page1] para a página 1 a partir do [$scope] e, em seguida, trabalhamos com ele (linha 7). Obtemos então a seguinte visualização:
![]() |
As vistas [/page2] e [/page3] são construídas com base no mesmo modelo que a vista [/page1] (ver as capturas de ecrã na página 240).
3.7.15.8. Controlo de navegação
Queremos agora controlar a navegação da seguinte forma [página1 --> página2 --> página3 --> página1]. Assim, se o utilizador estiver na página 1 [/página1] e introduzir o URL [/página3] no seu navegador, esta navegação não deve ser aceite e o utilizador deve permanecer na página 1.
Para conseguir isso, modificamos os controladores de página da seguinte forma:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope', '$location',
function ($scope, $location) {
// authorized navigation?
var main = $scope.main;
if (main.lastUrl && main.lastUrl != '/page3') {
// we return to the last URL
$location.path(main.lastUrl);
return;
}
// we store the URL of the page
main.lastUrl = '/page1';
// page template
var page1 = $scope.page1;
page1.text = "[Modèle local dans page 1]";
}]);
- linha 12: quando uma página é exibida, armazenamos a sua URL no modelo [main.lastUrl]. Aqui estamos a utilizar o conceito que discutimos anteriormente: usar o modelo [main] para armazenar informações partilhadas por todas as vistas. Neste caso, é a última URL visitada;
- O código nas linhas 4–12 é duplicado e adaptado para as três vistas. Aqui estamos na vista [/page1];
- linha 5: recuperamos o modelo [main];
- linha 6: se o modelo [main.lastUrl] existir e for diferente de [/page3], então a navegação é proibida (a última URL visitada existe e não é /page3);
- linha 8: voltamos então para a última URL visitada;
Vamos experimentar:
![]() |
- em [1], estamos na página 1 e digitamos o URL da página 3 em [2];
- em [3], a navegação não ocorreu e voltámos para o URL da página 1;
3.7.16. Conclusão
Abordámos todos os casos de utilização que iremos encontrar na versão final do cliente Angular. Quando o apresentarmos, iremos concentrar-nos mais nas funcionalidades da aplicação do que nos detalhes da sua implementação. Quanto a estes últimos, iremos simplesmente remeter para o exemplo que ilustra o caso de utilização em questão.
3.8. O Cliente Angular Final
3.8.1. Estrutura do projeto
O projeto final tem o seguinte aspeto:
![]() |
![]() |
- em [1], todo o projeto. [app.html] é a página principal da aplicação;
- em [2], os controladores;
- em [3], as diretivas;
- em [4], os serviços e o módulo Angular [main.js] da aplicação;
- em [5], as várias vistas que são inseridas na página principal [app.html];
3.8.2. Dependências do projeto
As dependências do projeto são as seguintes:
![]() |
A função destes vários elementos foi explicada na Secção 3.4, página 134.
3.8.3. A página mestre [app.html]
A página mestre é a seguinte:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tahé">
<!-- on CSS -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
<link href="assets/css/footable.core.min.css" rel="stylesheet"/>
</head>
<!-- controller [appCtrl], model [app] -->
<body ng-controller="appCtrl">
<div class="container">
...
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- modules -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- services -->
<script type="text/javascript" src="services/config.js"></script>
<script type="text/javascript" src="services/dao.js"></script>
<script type="text/javascript" src="services/utils.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="directives/waiting.js"></script>
<script type="text/javascript" src="directives/errors.js"></script>
<script type="text/javascript" src="directives/footable.js"></script>
<script type="text/javascript" src="directives/debug.js"></script>
<script type="text/javascript" src="directives/list.js"></script>
<!-- controllers -->
<script type="text/javascript" src="controllers/appController.js"></script>
<script type="text/javascript" src="controllers/loginController.js"></script>
<script type="text/javascript" src="controllers/homeController.js"></script>
<script type="text/javascript" src="controllers/agendaController.js"></script>
<script type="text/javascript" src="controllers/resaController.js"></script>
</body>
</html>
- linha 18: note que [appCtrl] é o controlador da página mestre;
- linhas 19–21: o conteúdo da página mestre;
Este conteúdo é o seguinte:
<div class="container">
<!-- navigation bars -->
<ng-include src="'views/navbar-start.html'" ng-show="app.navbarstart.show"></ng-include>
<ng-include src="'views/navbar-run.html'" ng-show="app.navbarrun.show"></ng-include>
<!-- the jumbotron -->
<ng-include src="'views/jumbotron.html'"></ng-include>
<!-- page title -->
<div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
translate-values="{{app.titre.model}}"></div>
<!-- page errors -->
<errors model="app.errors" ng-show="app.errors.show"></errors>
<!-- the waiting message -->
<waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
<!-- the current view -->
<ng-view></ng-view>
<!-- debug -->
<debug model="app" ng-show="app.debug.on"></debug>
</div>
Independentemente da vista apresentada, esta terá sempre os seguintes elementos:
- linhas 3-4: uma barra de comandos. As duas barras nas linhas 3 e 4 são mutuamente exclusivas;
![]()
![]()
- linha 6: um logótipo/texto da aplicação:

- linha 8: um título

- linha 11: uma mensagem de erro:

- linha 13: uma mensagem de carregamento:

- linha 17: informações de depuração:

Todos os elementos acima são controlados por uma diretiva [ng-show / ng-hide], o que significa que, mesmo que estejam presentes, não são necessariamente visíveis.
3.8.4. As vistas da aplicação
No código da página principal, temos:
<div class="container">
...
<!-- the current view -->
<ng-view></ng-view>
...
</div>
A linha 4 recebe as várias vistas da aplicação. Estas são definidas no módulo [main.js]:

A função de configurar as diferentes rotas foi explicada na secção 3.7.15.4, página 242.
A vista [login.html] está vazia, o que significa que não adiciona quaisquer elementos aos que já estão presentes na página mestre.
A vista [home.html] adiciona o seguinte elemento à página mestre:

A vista [agenda.html] adiciona o seguinte elemento à página mestre:

A vista [resa.html] adiciona o seguinte elemento à página mestre:

3.8.5. Funcionalidades da aplicação
As vistas do cliente Angular já foram apresentadas na Secção 1.3.3, na página 7. Para facilitar a leitura deste novo capítulo, repetimo-las aqui. A primeira vista é a seguinte:
![]() |
- [6], a página de início de sessão da aplicação. Trata-se de uma aplicação de agendamento de consultas médicas;
- em [7], uma caixa de seleção que permite ao utilizador ativar ou desativar o modo [debug]. Este modo caracteriza-se pela presença do painel [8], que exibe o modelo da vista atual;
- em [9], um tempo de espera artificial em milissegundos. O valor padrão é 0 (sem espera). Se N for o valor deste tempo de espera, qualquer ação do utilizador será executada após um tempo de espera de N milissegundos. Isto permite-lhe ver a gestão de espera implementada pela aplicação;
- em [10], o URL do servidor Spring 4. Com base no que precedeu, este é [http://localhost:8080];
- em [11] e [12], o nome de utilizador e a palavra-passe do utilizador que pretende utilizar a aplicação. Existem dois utilizadores: admin/admin (login/palavra-passe) com uma função (ADMIN) e user/user com uma função (USER). Apenas a função ADMIN tem permissão para utilizar a aplicação. A função USER é incluída exclusivamente para demonstrar a resposta do servidor neste caso de utilização;
- em [13], o botão que permite ligar-se ao servidor;
- em [14], o idioma da aplicação. Existem dois: francês (padrão) e inglês.
![]() |
- em [1], faz o login;
![]() |
- depois de iniciar sessão, pode escolher o médico com quem deseja marcar uma consulta [2] e a data da consulta [3];
- Em [4], solicita a visualização da agenda do médico selecionado para o dia escolhido;
![]() |
- Assim que a agenda do médico for apresentada, pode reservar um horário [5];
![]() |
- Em [6], selecione o paciente para a consulta e confirme a sua seleção em [7];
![]() |
Assim que a consulta for confirmada, será automaticamente redirecionado para a agenda, onde a nova consulta já estará listada. Esta consulta pode ser eliminada posteriormente [7].
As principais funcionalidades já foram descritas. São simples. As que não foram descritas são funções de navegação para regressar a uma visualização anterior. Vamos concluir com as definições de idioma:
![]() |
- Em [1], o idioma muda de francês para inglês;
![]() |
- em [2], a visualização muda para inglês, incluindo o calendário;
3.8.6. O módulo [main.js]
O módulo [main.js] define o módulo Angular que controlará a aplicação:
![]() |
- linha 4: o módulo chama-se [rdvmedecins];
- linha 5: o módulo [ngRoute] é utilizado para o encaminhamento de URLs;
- linha 6: o módulo [translate] é utilizado para a internacionalização de texto;
- linha 7: o módulo [base64] é utilizado para codificar a cadeia de caracteres «login:password» em Base64;
- linha 8: o módulo [ngLocale] é utilizado para internacionalizar o calendário;
- linha 9: o módulo [ui.bootstrap] é utilizado para o calendário;
- linha 12: configuração da rota;
- linha 40: internacionalização de mensagens;
3.8.7. O controlador da página principal
Vamos rever o código HTML da página principal [app.html]:
<body ng-controller="appCtrl">
<div class="container">
...
Linha 1: Todo o corpo da página mestre é controlado pelo controlador [appCtrl]. Devido à sua posição, isto torna-o o controlador geral e principal da aplicação. Conforme explicado na Secção 3.7.15, o modelo criado por este controlador é herdado por todas as vistas que serão inseridas na página mestre.
O seu código é o seguinte:
angular.module("rdvmedecins")
.controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
function ($scope, config, utils, $location, $locale) {
// debug
utils.debug("[app] init");
// ----------------------------------------initialisation page
// templates for # pages
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// current page template
var app = $scope.app;
...
// ---------------------------------- méthodes
// cancel current job
app.cancel = function () {
...
};
// disconnect
app.deconnecter = function () {
...
};
// this code must remain here as it refers to the preceding [cancel] function
app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
}])
;
As linhas 10–14 definem os cinco modelos utilizados na aplicação:
app.html | appCtrl | |
login.html | loginCtrl | |
home.html | homeCtrl | |
reserva.html | resaCtrl | |
agenda.html | agendaCtrl |
É importante compreender que o objeto [$scope], sendo o modelo do controlador da página principal, é herdado por todas as visualizações e controladores. Assim, o controlador [loginCtrl] tem acesso aos elementos [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda]. Por outras palavras, um controlador tem acesso aos âmbitos de outros controladores. A aplicação em questão evita cuidadosamente utilizar esta capacidade. Assim, por exemplo, o controlador [loginCtrl] trabalha apenas com dois escopos:
- o seu próprio [$scope.login];
- e o do controlador pai [$scope.app];
O mesmo se aplica a todos os outros controladores. O modelo [$scope.app] será utilizado como memória partilhada entre os diferentes controladores. Quando um controlador C1 precisa de passar informação para o controlador C2, segue-se o seguinte procedimento:
Em [C1]:
Em [C2]:
Em ambos os casos, $scope é herdado do controlador [appCtrl] e é, portanto, idêntico (é um ponteiro) em [C1] e [C2]. O objeto [$scope.app], que serve como memória partilhada entre controladores, é frequentemente referido como uma «sessão» nos comentários, imitando a sessão utilizada em aplicações web tradicionais, que se refere à memória partilhada entre pedidos HTTP sucessivos.
Voltemos ao código do controlador [appCtrl]:
// templates for # pages
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// current page template
var app = $scope.app;
// [app.debug] and [utils.verbose] must always be synchronized
app.debug = utils.verbose;
app.debug.on = config.debug;
// no page title for the moment
app.titre = {show: false};
// no navigation bars
app.navbarrun = {show: false};
app.navbarstart = {show: false};
// no errors
app.errors = {show: false};
// local default
angular.copy(config.locales['fr'], $locale);
// the current view
app.view = {url: undefined, model: {}, done: false};
// the current task
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
- linha 8: [$scope.app] será o modelo para a página principal. Também servirá como memória partilhada entre os vários controladores. Em vez de escrever [$scope.app.field=value] em todo o lado, o ponteiro [$scope.app] é atribuído à variável [app], pelo que escrevemos [app.field=value]. Basta lembrar que [app] é o modelo exposto à página principal;
- linha 11: [app.debug.on] é um booleano que controla o modo de depuração da aplicação. Por predefinição, está definido como true. O seu valor está ligado à caixa de seleção [debug] nas barras de navegação;
- linha 15: [app.navbarrun.show] controla a exibição da seguinte barra de navegação:
![]()
- linha 16: [app.navbarstart.show] controla a exibição da seguinte barra de navegação:
![]()
- linha 18: [app.errors] é o modelo para o banner de erro;

- Linha 22: [app.view] conterá informações sobre a vista atual — aquela atualmente exibida pela tag [ng-view] na página mestre. Incluiremos as seguintes informações nesse local:
- [url]: o URL da vista atual, por exemplo [/agenda];
- [model]: o modelo da vista atual, por exemplo [$scope.agenda];
- [done]: quando verdadeiro, indica que a vista atual terminou o seu trabalho e que estamos a mudar para outra vista;
Estas informações são utilizadas para controlar a navegação.
- linha 24: inicia uma tarefa assíncrona, uma espera simulada. A tarefa assíncrona é referenciada por dois ponteiros [app.view.model.task.action] e [app.task];
Dois métodos foram incorporados no controlador [appCtrl]:
// cancel current job
app.cancel = function () {
...
};
// disconnect
app.deconnecter = function () {
...
};
- linha 2: a função [app.cancel] é utilizada para cancelar a tarefa atual para a qual está a ser exibida uma mensagem de carregamento. Todas as vistas exibem esta mensagem, pelo que a tarefa será cancelada aqui;
- linha 7: a função [app.logout] redireciona o utilizador para a página de início de sessão. Todas as vistas, exceto a vista [/login], oferecem esta opção;
A função [app.deconnecter] é a seguinte:
// disconnect
app.deconnecter = function () {
// we return to the login page
$location.path(config.urlLogin);
};
- linha 4: regressar à página de início de sessão na URL [/login];
3.8.8. Gestão de tarefas assíncronas
Na nossa aplicação, em qualquer momento, apenas uma tarefa assíncrona estará em execução. É possível ter várias tarefas em execução. Por exemplo, quando a aplicação inicia, solicita a lista de médicos ao serviço web e, em seguida, a lista de clientes com duas solicitações HTTP sucessivas. Poderíamos fazer o mesmo com duas solicitações HTTP simultâneas. O Angular fornece as ferramentas para isso. Aqui, não optámos por essa abordagem.
A tarefa atualmente em execução é cancelada com o seguinte código no controlador [appCtrl]:
// cancel current job
app.cancel = function () {
utils.debug("[app] cancel task");
// cancel the current view's asynchronous task
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
...
};
- linha 5: a tarefa é recuperada de [app.view.model.task]. Portanto, todos os controladores garantirão que as suas tarefas assíncronas sejam referenciadas por este objeto;
- linha 6: para indicar que a tarefa está concluída;
- linha 7: para encerrar a tarefa com uma falha. Esta notação difere da utilizada nos exemplos do Angular estudados:
- nos exemplos, o objeto [task] era um objeto [$q.defer()] que podia ser encerrado;
- na versão final, o objeto [task] é um objeto com os campos [action, isFinished], onde [action] é o objeto [$q.defer()] que pode ser encerrado e [isFinished] é um booleano que indica que a ação está concluída;
Vamos examinar o ciclo de vida do objeto [task] usando um exemplo. Na inicialização, após o controlador [appCtrl], o controlador [loginCtrl] assume o controlo para exibir a vista [views/login.html]. O seu código de inicialização é o seguinte:
// retrieve the parent model
var login = $scope.login;
var app = $scope.app;
// current view
app.view = {url: config.urlLogin, model: login, done: false};
Na linha 5, temos [model=login]. Isto significa que, quando modificamos o objeto [login], modificamos o objeto [app.view.model], ou seja, [$scope.app.view.model]. Quando queremos simular uma espera no controlador [loginCtrl], escrevemos:
// simulated waiting
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
Ao adicionar o campo [task] ao objeto [login], este é, portanto, adicionado ao objeto [$scope.app.view.model]. Se o utilizador cancelar a espera, o código em [appCtrl.cancel]:
// current page template
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
concluirá com sucesso a espera simulada (linhas 4–6).
3.8.9. Controlo de navegação
As regras de navegação utilizadas na aplicação são as seguintes:
qualquer | sim | |
/login | sim, se o controlador [loginCtrl] tiver indicado que concluiu o seu trabalho | |
/home | sim | |
/calendário | sim | |
/casa | sim, se o controlador [homeCtrl] tiver indicado que concluiu o seu trabalho | |
/reset | sim | |
/agenda | sim | |
/calendário | sim, se o controlador [homeCtrl] tiver indicado que concluiu o seu trabalho | |
/reiniciar | sim |
Isto é implementado com o seguinte código:
Para [agendaCtrl]:

- linhas 11–20: implementação da regra de navegação;
- linha 26: nova vista atual;
Para [resaCtrl]:

- linhas 12–20: implementação da regra de navegação:
- linha 27: nova vista atual;
Para [loginCtrl]:

- Não há controlo de navegação aqui, uma vez que a regra estabelece que o URL [/login] pode ser acedido a partir de qualquer lugar. Portanto, se o utilizador digitar este URL no seu navegador, ele funcionará independentemente da vista atual;
- linha 16: a nova vista atual;
O código para o controlador [homeCtrl] foi fornecido na secção 3.8.7.
Por fim, para uma regra como:
/home | sim, se o controlador [homeCtrl] tiver indicado que concluiu o seu trabalho |
Aqui está um exemplo de código que navega da URL [/home] para a URL [/agenda]:
![]() |
Acima, estamos no método [displayCalendar] do controlador [homeCtrl]. O utilizador solicitou o calendário de um médico.
- linha 107: a promessa da tarefa HTTP;
- linha 109: a variável [app] foi inicializada com [$scope.app]. Como vimos, este objeto é usado como modelo para a vista [app.html]. Este modelo [$scope.app] também é usado para armazenar informações que precisam de ser partilhadas entre vistas;
- linha 111: o código de erro devolvido pela tarefa é analisado;
- linha 113: o resultado [result.data] é colocado no modelo [app];
- linha 116: o controlador [homeCtrl] irá passar o controlo para o controlador [agendaCtrl]. Indica que concluiu o seu trabalho com o código na linha 115. Este código será utilizado pelo controlador [agendaCtrl] da seguinte forma:

- linha 11: o objeto [$scope.app.view] é recuperado;
- linha 15: processamento do campo [$scope.app.view.done] inicializado por [homeCtrl];
3.8.10. Serviços
![]() |
Os serviços [config, utils, dao] são aqueles já descritos na visão geral do Angular:
- o serviço [config] foi apresentado na secção 3.7.4;
- o serviço [utils] foi apresentado na secção 3.7.5;
- o serviço [dao] foi apresentado na secção 3.7.6;
A título de lembrete, eis a estrutura destes serviços:
Serviço [config]
![]() |
- em [1]: vemos que o código tem cerca de 250 linhas. A maior parte deste código envolve a externalização das chaves para mensagens internacionalizadas [2]. Evitamos codificar estas chaves diretamente no código;
serviço [utils]
![]() |
- Linha 8: Ainda não nos deparámos com a variável [verbose]. Ela controla a função [debug] da seguinte forma:
![]() |
- Linhas 22–25: A função [utils.debug] não faz nada se [verbose.on] for avaliada como false. Esta variável está ligada a uma variável no controlador [appCtrl]:
![]() |
- linha 21: [app.debug] assume o valor do ponteiro [utils.verbose]. Portanto, qualquer alteração feita em [app.debug] será também aplicada a [utils.verbose];
- linha 22: o valor inicial de [app.debug.on] é obtido do ficheiro de configuração. Por predefinição, está definido como true. Este valor pode mudar ao longo do tempo. O utilizador pode alterá-lo através das barras de navegação:
![]() |
- linha 45: uma caixa de seleção (type=checkbox) permite alterar o valor de [app.debug.on] (atributo ng-model);
Serviço [dao]
![]() |
3.8.11. Diretivas
![]() |
As diretivas [errors, footable, list, waiting] são as já descritas na visão geral do Angular:
- a diretiva [footable] foi apresentada na secção 3.7.8.6;
- a diretiva [list] foi apresentada na secção 3.7.12;
- as diretivas [errors] e [waiting] foram apresentadas na secção 3.7.14;
Ainda não tínhamos encontrado a diretiva [debug]. É a seguinte:
![]() |
O ficheiro [debug.html] referido na linha 11 é o seguinte:
- linha 2: a diretiva [debug] apresenta o seu modelo em formato JSON num banner Bootstrap (linha 1);
Esta diretiva é utilizada apenas na página principal [app.html]:
![]() |
- A diretiva [debug] é utilizada na linha 35. Assim, apresenta a representação JSON do modelo [$scope.app] quando em modo de depuração (atributo ng-show). Isto produz um resultado semelhante ao seguinte:
![]() |
Isto requer uma boa compreensão do código para ser interpretado, mas, uma vez adquirida essa compreensão, a informação acima torna-se útil para a depuração. Aqui, destacámos os elementos do modelo [$scope.app] apresentado. Recorde-se que [$scope.app] é a memória partilhada entre controladores;
- [waitingBeforeTask]: o tempo de espera simulado antes de qualquer pedido HTTP;
- [debug]: modo de depuração — é necessariamente verdadeiro se este banner for exibido;
- [navbarrun]: um valor booleano que controla a exibição da barra de navegação seguinte:
![]()
- [navbarstart]: um valor booleano que controla a exibição da seguinte barra de navegação:
![]()
- [errors]: modelo para a diretiva [errors];
- [view]: encapsula informações sobre a vista atualmente exibida;
- [waiting]: modelo para a diretiva [waiting];
- [serverUrl, username, password]: credenciais de login para o serviço web;
- [doctors]: modelo para a diretiva [list] aplicada aos médicos;
- [clients]: o mesmo que acima para clientes;
- [menu]: controla as opções do menu exibido. Estas são definidas em [navbar-run.html]:

As opções do menu encontram-se nas linhas 16, 23, 29 e 36.
- [formattedDay]: o dia selecionado no calendário no formato «aaaa-mm-dd»;
- [agenda]: a agenda do médico. Contém horários disponíveis (rv==null) e horários reservados. Para estes últimos, inclui o nome do cliente que efetuou a reserva;
- [selectedCreneau]: o intervalo de tempo selecionado para efetuar uma reserva;
3.8.12. O controlador [loginCtrl]
![]() |
O controlador [loginCtrl] está associado à vista [views/login.html], que, quando combinada com a página principal, gera a seguinte página:

O controlador [loginCtrl] é o seguinte:

- linha 13: [login] será o modelo para a vista atual;
- linha 14: [app] é a memória partilhada entre controladores;
- linha 16: [app.view] é preenchido com informações da vista atual;
Este código de inicialização estará presente em todos os controladores. Para o controlador C1 de uma vista V1 com o modelo M1, teremos o seguinte código de inicialização:
- linha 18: deve lembrar-se que [appCtrl] iniciou uma espera simulada referenciada pelo objeto [app.task.action]. Utilizamos a [promise] desta tarefa para aguardar a sua conclusão;
- linha 39: o método [login.setLang] lida com a mudança de idioma;
- linha 47: o método [login.authenticate] lida com a autenticação do utilizador;
Vejamos os principais passos do método de autenticação:

- linhas 50–51: [app.waiting] é o modelo para o banner de carregamento;
- linha 53: [app.errors] é o modelo para o banner de erro;
- linha 55: é iniciada uma espera simulada. O objeto [action, isFinished] é referenciado por [login.task] e, portanto, uma vez que [app.view.model=login], por [app.view.model.task]. Recorde-se que esta é a condição para que a tarefa seja cancelada;
- linha 57: após o término da espera simulada, os médicos são carregados;
- linha 62: assim que os médicos forem recuperados, o seu pedido é analisado. Se os médicos tiverem sido obtidos, os clientes são então solicitados;
- linha 83: a resposta é analisada e a visualização final é apresentada. Isto é feito com o seguinte código:

- Linha 87: O booleano [task.isFinished] é definido como verdadeiro nos seguintes casos:
- o utilizador cancelou a espera;
- o pedido dos médicos terminou com um erro;
- linhas 91–98: o caso em que temos os clientes;
- linha 93: [app.clients] é o modelo para a diretiva [list] que irá apresentar os clientes numa lista suspensa;
- linhas 97–98: preparamo-nos para alterar as vistas (linha 98), mas primeiro indicamos que o controlador terminou o seu trabalho (linha 97). Recorde-se que [$scope.app.view.done] é utilizado para o controlo da navegação;
O ponto importante a notar aqui é que os médicos e os clientes foram armazenados em cache no navegador. Eles já não serão solicitados ao serviço web.
3.8.13. O controlador [homeCtrl]
![]() |
O controlador [homeCtrl] está associado à vista [views/home.html], que, quando combinada com a página mestre, produz a seguinte página:

A estrutura do controlador [homeCtrl] é a seguinte:

- linhas 12–20: este é o controlo de navegação. Todos os controladores têm isto, exceto o [loginCtrl], porque a página [/login.html] é acessível sem condições;

- linhas 25–28: aqui encontramos linhas semelhantes às do controlador [loginCtrl]. [home] é, portanto, o modelo de visualização associado ao controlador;
- linha 33: um atributo que ainda não encontrámos. Este é o modelo para a barra de cabeçalho da vista:
![]()
- linha 36: [home.datepicker] é o modelo para o calendário;
- linha 38: [app.menu] é o modelo para o menu da barra de navegação. Aqui, a opção [Schedule] estará presente. É isto que permite solicitar a agenda de um médico;
Por fim, o controlador tem dois métodos:

A exibição da agenda (linha 51) foi abordada na secção 3.7.8.
3.8.14. O controlador [agendaCtrl]
![]() |
O controlador [agendaCtrl] está associado à vista [views/agenda.html], que, quando combinada com a página mestre, produz a seguinte página:

A estrutura do controlador [agendaCtrl] é a seguinte:

- as linhas 10–20 tratam do controlo de navegação;

- linhas 23–26: [agenda] será o modelo de visualização associado ao controlador [agendaCtrl];
- linhas 36–44: [app.title] é o modelo para a seguinte barra de título:

- linha 46: o menu terá a opção [Home]:
![]()
Os métodos do controlador são os seguintes:

- linha 95: o método [agenda.delete] foi abordado na secção 3.7.9;
O método [agenda.home] é um método de navegação puro:

O método [agenda.reserve] é o seguinte:

- linha 73: o parâmetro da função [reserve] é o número do intervalo (id);
- linhas 77–86: procuram encontrar o intervalo de tempo com este identificador;
- linha 82: o intervalo de tempo encontrado é colocado na memória partilhada [app]. O controlador [resaCtrl], que assumirá o controlo (linha 90), utilizará esta informação para apresentar a sua barra de título;
- linhas 89-90: navega para [/resa.html];
3.8.15. O controlador [resaCtrl]
![]() |
O controlador [resaCtrl] está associado à vista [views/resa.html], que, quando combinada com a página mestre, produz a seguinte página:

A estrutura do controlador [resaCtrl] é a seguinte:

- linhas 12–20: controlo de navegação;

- linhas 24–27: [resa] será o modelo para a vista atual;
- linhas 38–45: [app.titre] é o modelo para a barra de título seguinte:

- linha 47: são apresentadas duas opções de menu:
![]()
Os métodos do controlador são os seguintes:

O método [resa.valider] foi abordado na secção 3.7.9.
3.8.16. Gestão de idiomas
Todos os controladores disponibilizam o seguinte método [setLang]:

Poderia ter sido incorporado no controlador [appCtrl].




























































































































