3. O cliente Angular JS
3.1. Referências do framework Angular JS
No início deste documento, foram fornecidas duas referências para o framework Angular JS. Repetimo-las aqui:
- [ref1]: o livro «Pro AngularJS», escrito por Adam Freeman e publicado pela editora Apress. É um excelente livro. Os códigos-fonte dos exemplos deste livro estão disponíveis 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 Angular JS merece um livro só para si. O livro de Adam Freeman tem mais de 600 páginas e nenhuma delas é desperdiçada. Vamos descrever uma aplicação Angular e, ao longo dessa descrição, iremos abordar os fundamentos deste framework. No entanto, limitar-nos-emos apenas às explicações necessárias para a compreensão da solução proposta. O Angular é um framework extremamente rico e existem inúmeras soluções para se chegar ao mesmo resultado. Isto constitui uma dificuldade, pois, quando se está a dar os primeiros passos, não se sabe se se está a utilizar uma solução pior ou melhor do que outra. É o caso da solução aqui proposta. Ela poderia ser escrita de forma diferente e, talvez, seguindo melhores práticas.
3.2. Arquitetura do cliente Angular
A arquitetura do cliente Angular assemelha-se à de uma aplicação web clássica MVC, com algumas diferenças. Uma aplicação web Spring MVC, por exemplo, tem a seguinte arquitetura:
![]() |
O processamento de um pedido de um cliente decorre da seguinte forma:
- pedido — os URL solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... A [Dispatcher Servlet] é a classe do Spring que processa os URL recebidos. Esta «encaminha» o URL para a ação que deve processá-lo. Estas ações são métodos de classes específicas denominadas [Contrôleurs]. O C de MVC é, neste caso, a cadeia [Dispatcher Servlet, Contrôleur, Action]. Se nenhuma ação tiver sido configurada para processar o URL recebido, o servlet [Dispatcher Servlet] responderá que o URL solicitado não foi encontrado (erro 404 NOT FOUND);
- processamento
- a ação selecionada pode utilizar os parâmetros parami que a servlet [Dispatcher Servlet] lhe transmitiu. Estes podem provir de várias fontes:
- do caminho [/param1/param2/...] do URL,
- dos parâmetros [p1=v1&p2=v2] do URL,
- dos parâmetros enviados pelo navegador com o seu pedido;
- no processamento do pedido do utilizador, a ação pode necessitar da camada [metier] [2b]. Uma vez processado o pedido do cliente, este pode gerar várias respostas. Um exemplo clássico é:
- uma página de erro, caso a solicitação não tenha podido ser processada corretamente
- uma página de confirmação, caso contrário
- a ação solicita que uma determinada vista seja apresentada: [3]. Esta vista irá apresentar dados a que se chama o modelo da vista. É o M de MVC. A ação irá criar este modelo M [2c] e solicitar que uma vista V seja apresentada [3];
- resposta — a vista V selecionada utiliza o modelo M criado 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 uma terminologia ligeiramente diferente. Em primeiro lugar, as aplicações Angular são geralmente aplicações web de página única (APU) ou Single Page Application (SPA):

- o utilizador solicita a URL inicial da aplicação na forma: http://machine:port/contexte. O navegador irá consultar um servidor web para obter o documento solicitado. Trata-se de uma página HTML com estilo definido por CSS e dinamizada por JavaScript;
- em seguida, o utilizador irá interagir com as vistas que lhe são apresentadas. É possível distinguir vários tipos de interações:
- aquelas que não requerem qualquer interação com o exterior, por exemplo, ocultar/mostrar elementos da vista. Estas são tratadas pelo JavaScript incorporado;
- aquelas que requerem dados provenientes de um serviço web remoto. Estes serão recuperados através de uma chamada AJAX (Asynchronous JavaScript and XML), será construído um modelo e exibida uma vista;
- aquelas que requerem uma vista diferente da vista inicial. Esta será solicitada através de uma chamada Ajax ao servidor que forneceu a página inicial. Em seguida, o processo anterior repetir-se-á. A página obtida será armazenada em cache no navegador. Na próxima chamada, não será solicitada ao servidor remoto HTML;
No final, o navegador efetua apenas uma única chamada HTTP, aquela que obtém a página inicial. As chamadas HTTP seguintes, para o servidor de páginas HTML ou para serviços web remotos, são efetuadas pelo JavaScript incorporado nas páginas.
Apresentamos agora a arquitetura da aplicação no navegador. Ignoramos o servidor HTML que fornece as páginas HTML da aplicação. Para efeitos de explicação, podemos considerar que todas elas estão presentes na cache do navegador.
![]() |
Em primeiro lugar, é necessário contextualizar esta arquitetura:
- em [1], estamos num navegador;
- em [2], um utilizador interage com as vistas apresentadas pelo navegador;
- em [3], os dados são obtidos na rede, frequentemente a partir de serviços web;
O utilizador interage com as vistas: preenche formulários e valida-os. Vamos explicar este processo com a vista V1 acima. Vamos supor que esta é a vista inicial da aplicação. Foi obtida da seguinte forma:
- o utilizador solicita a vista inicial URL da aplicação sob a forma: http://machine:port/contexte;
- o navegador solicitou o documento associado a essa URL. Recebeu a página HTML / CSS / JS a partir da vista V1;
- o JavaScript incorporado na página assumiu então o controlo e passou-o ao controlador C1 [5];
- este construiu o modelo M1 [8] [9] da vista V1. A criaçã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, valida-o:
- em [4], o utilizador valida o formulário;
- em [5], este evento será tratado por um dos métodos do controlador C1;
Se o evento implicar apenas uma simples alteração na vista V1 (ocultar/mostrar campos), o controlador C1 irá modificar o modelo M1 da vista V1 e, em seguida, voltar a apresentar a vista V1. Para tal, poderá necessitar de um dos serviços da camada [services] [6].
Se o evento requerer dados externos:
- na [6], o controlador C1 solicitará à camada [DAO] que os obtenha;
- em [7], esta irá efetuar uma ou mais chamadas AJAX para os obter;
- em [8] e [9], o modelo M1 será alterado e a vista V1 será apresentada;
Se o evento provocar uma mudança de vista, nos dois casos anteriores, em vez de apresentar a vista V1, o controlador C1 irá solicitar uma nova vista URL [10]. Trata-se de uma URL interna ao navegador. Não se traduz imediatamente numa chamada HTTP ao servidor de páginas HTML. Esta alteração de URL é tratada por um router configurado de forma a que, a cada URL interna, corresponda uma vista V e o seu controlador C. O router faz então com que a nova vista Vn seja apresentada. Antes da exibição, o seu controlador Cn assume o controlo, constrói o modelo Mn e, em seguida, faz com que a vista Vn [11] seja exibida. Se a página HTML da vista Vn não estiver em cache no navegador, será então solicitada ao servidor de páginas HTML.
A camada [Présentation] desta arquitetura é semelhante à arquitetura JSF (Java Server Faces):
- a vista V corresponde à vista do tipo Facelet de JSF;
- o controlador C corresponde ao bean JSF, uma classe Java que contém tanto o modelo M da vista V como os gestores de eventos desta;
A camada [Services] é diferente das camadas [Services] a que estamos habituados. No desenvolvimento web do lado do servidor, temos, na maioria das vezes, a seguinte arquitetura em camadas:
![]() |
Na figura acima, a camada [web] comunica com a camada [DAO] apenas através da camada [métier]. Nada nos impediria de inserir na camada [web] uma referência à camada [DAO] que permitisse essa comunicação. Mas evitamos fazê-lo.
Com o Angular, não nos impedimos de o fazer. A arquitetura passa então a ser a seguinte:
![]() |
- em [1], a camada [présentation] pode comunicar diretamente com qualquer serviço;
- em [2], os serviços reconhecem-se entre si. Um serviço pode utilizar um ou vários outros.
3.3. As vistas do cliente Angular
As vistas do cliente Angular já foram apresentadas no parágrafo 1.3.3. Para facilitar a leitura deste novo capítulo, voltamos a apresentá-las aqui. A primeira vista é a seguinte:
![]() |
- em [6], a página inicial da aplicação. Trata-se de uma aplicação para marcação de consultas médicas;
- em [7], uma caixa de seleção que permite ativar ou desativar o modo [debug]. Este último caracteriza-se pela presença do quadro [8], que exibe o modelo da vista atual;
- em [9], um tempo de espera artificial em milissegundos. O valor predefinido é 0 (sem espera). Se N for o valor desse tempo de espera, qualquer ação do utilizador será executada após um tempo de espera de N milissegundos. Isto permite observar a gestão da espera implementada pela aplicação;
- em [10], o URL do servidor Spring 4. Seguindo o que foi referido anteriormente, trata-se de [http://localhost:8080];
- em [11] e [12], o identificador e a palavra-passe de quem pretende utilizar a aplicação. Existem dois utilizadores: admin/admin (login/password) 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 existe apenas para mostrar 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: o francês, por predefinição, e o inglês.
![]() |
- em [1], efetua-se a ligação;
![]() |
- uma vez conectado, pode-se escolher o médico com quem se deseja marcar uma consulta [2] e o dia da mesma [3];
- em [4], solicita-se a visualização da agenda do médico escolhido para o dia selecionado;
![]() |
- assim que obtiver a agenda do médico, pode-se reservar um horário [5];
![]() |
- em [6], seleciona-se o doente para a consulta e confirma-se essa escolha em [7];
![]() |
Assim que a consulta for validada, é-se redirecionado automaticamente para a agenda, onde a nova consulta já se encontra registada. Esta consulta poderá ser posteriormente eliminada em [7].
As principais funcionalidades foram descritas. São simples. As que não foram descritas são funções de navegação para regressar a uma vista anterior. Terminemos com a gestão do idioma:
![]() |
- em [1], muda-se do francês para o inglês;
2

- para [2], a vista passa para inglês, incluindo o calendário;
3.4. Configuração do projeto Angular
Vamos construir o nosso cliente Angular de forma progressiva. Utilizamos o IDE Webstorm.
Vamos criar uma pasta vazia [rdvmedecins-angular-v1] e, em seguida, abri-la com o Webstorm:
![]() |
- em [1], abrimos uma pasta;
- em [2], selecionamos a pasta que criámos;
- em [3], obtemos um projeto WebStorm vazio;
![]() |
- em [4], a configuração do projeto é feita através da opção [File / Settings];
- Nos ficheiros [5] e [6], configura-se a propriedade [Spelling], que gere a verificação ortográfica. Por predefinição, esta está ativada. Como o software descarregado está em inglês, os nossos comentários em francês sobre os programas serão assinalados como possíveis erros ortográficos. Por isso, desativamos esta verificação ortográfica [7];
![]() |
- em [8], criamos um novo ficheiro;
- em [9], optamos por criar o ficheiro [package.json] que descreve a aplicação com uma sintaxe JSON;
- em [10], o ficheiro gerado é modificado conforme mostrado em [11];
- em [12], guarda-se este ficheiro tanto em [package.json] como em [bower.json];
![]() |
- em [13], volta-se a configurar o projeto;
![]() |
- no [14], configura-se a propriedade [Javascript / Bower], que nos permitirá declarar as bibliotecas JavaScript de que necessitamos;
- em [15], indicar o ficheiro [bower.json] que acabámos de criar;
![]() |
- em [16], adicionemos uma biblioteca JavaScript;
- em [17] são apresentadas todas as bibliotecas JavaScript disponíveis para download;
- no [18], podemos definir um critério para filtrar a lista [17]. Aqui, indicamos que queremos a biblioteca [Angular JS];
- em [19], aparecem as características da biblioteca. Vemos aqui que a versão 1.2.18 do Angular vai ser descarregada;
- em [20], procede-se ao download;
![]() |
- em [21], verifica-se que foi descarregada;
- em [22], vemos a versão descarregada. Trata-se, portanto, na realidade, da versão 1.2.19;
- em [23], vemos a última versão disponível;
![]() |
- em [24], seguindo o mesmo procedimento anterior, descarregam-se as seguintes bibliotecas:
para codificar a cadeia «user:password» em Base64; | ||
para internacionalizar o calendário | ||
para encaminhar os URL internos da aplicação para o controlador e a vista corretos; | ||
permite a internacionalização das vistas. Trata-se de um projeto independente do Angular. Aqui, serão utilizados dois idiomas: o francês e o inglês; | ||
fornece componentes visuais compatíveis com o Bootstrap. Aqui, utilizaremos o seu calendário; | ||
o framework CSS Bootstrap. Será utilizado para construir as vistas; | ||
fornece um componente visual do tipo «tabela». É «responsivo», no sentido em que se adapta ao tamanho do ecrã; | ||
fornece um componente do tipo «lista suspensa»; |
![]() |
- em [25], as bibliotecas descarregadas foram instaladas na pasta [bower_components];
- em [26], verifica-se que a biblioteca JQuery foi descarregada. Isto deve-se ao facto de o Bootstrap a utilizar. O sistema de instalação das dependências JavaScript de um projeto é semelhante ao do Maven no mundo Java: se uma biblioteca descarregada tiver, por sua vez, dependências, estas são descarregadas automaticamente;
O ficheiro [bower.json] foi atualizado:
Todas as dependências descarregadas foram registadas no ficheiro.
3.5. A página inicial do cliente Angular
Criamos uma primeira versão da página inicial do cliente Angular:
![]() |
- em [1] e [2], criamos um ficheiro HTML denominado [app-01], [3] e [4];
O ficheiro [app-01.html] será a nossa página principal durante algum tempo. Vamos configurar nele a importação dos ficheiros CSS e JS de que a aplicação necessita:
<!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é">
<!-- o 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>
<!-- Núcleo do Bootstrap 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>
<!-- AngularJS -->
<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 dos componentes Bootstrap;
- linha 21: os componentes Bootstrap são alimentados pelo 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 do Angular e dos projetos a ele associados;
- linha 26: o ficheiro JS do Angular. Deve ser carregado após o JQuery, caso esta biblioteca seja utilizada;
- linha 27: o ficheiro JS do projeto [angular-ui-bootstrap];
- linha 28: o ficheiro JS do router [angular-route];
- linha 29: o ficheiro JS do módulo de internacionalização das aplicações Angular;
- linha 30: o ficheiro JS do módulo [angular-base64];
A validade do ficheiro [app-01.html] pode ser verificada:
![]() |
- no [1], solicita-se a verificação do código;
- no [2], o resultado quando tudo corre bem;
Recomenda-se esta verificação sistemática do código antes da sua execução. Neste caso, esta verificação permite detetar qualquer erro de referência nos ficheiros CSS e JS. Se um caminho estiver incorreto, o verificador de código irá assinalá-lo.
- Em [3], a página pode ser carregada num navegador através de um depurador. Obtém-se o seguinte resultado no navegador:
![]() |
- em [4], a página [app-01.html] foi servida por um servidor interno do WebStorm a operar aqui na porta 63342;
- em [5], a consola do depurador. Se tivessem ocorrido erros, estes teriam aparecido aqui. É também aqui que são exibidas as saídas de ecrã produzidas pela instrução [console.log(expression)] do JavaScript. Iremos utilizar amplamente esta funcionalidade;
O modo de depuração permite alterar a página no WebStorm e ver os resultados dessas alterações no navegador sem ter de recarregar a página. Assim, se adicionarmos a linha 3 abaixo:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2>Version 1</h2>
</div>
e voltarmos ao navegador, verificamos que a página mudou:
![]() |
3.6. Introdução ao Bootstrap
Vamos agora ilustrar algumas das características do Bootstrap utilizadas na aplicação. Tenho apenas um conhecimento limitado deste framework, adquirido através de copiar e colar códigos encontrados na Internet. Explicarei o papel das classes CSS que creio compreender. Abster-me-ei de comentar as restantes.
3.6.1. Exemplo 1
No Angular, as operações que vão buscar informação a partir do exterior são assíncronas. Isto significa que a operação é iniciada e que há um regresso imediato à vista, com a qual o utilizador pode continuar a interagir. A aplicação é notificada do fim da operação através de um evento. Este evento é tratado por uma função JS, que pode então enriquecer a vista atual ou alterá-la. Se a operação for suscetível de demorar, é útil oferecer ao utilizador a possibilidade de a cancelar. Ofereceremos essa possibilidade sistematicamente. Para tal, utilizaremos um banner Bootstrap:

Para obter este resultado, duplicamos [app-01.html] em [app-02.html] e alteramos 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 visualização no interior do navegador;
- linha 3: a classe CSS [alert] apresenta uma área colorida. A classe [alert-warning] utiliza uma cor predefinida;
- linha 5: a classe [btn] aplica um estilo a um botão. A classe [btn-primary] atribui-lhe uma determinada cor. A classe [pull-right] posiciona-o à direita da faixa de alerta;
- linha 6: uma imagem animada de espera;
3.6.2. Exemplo 2
As diferentes vistas da aplicação terão um título comum:

Para obter este resultado, duplicamos [app-01.html] em [app-03.html] e alteramos 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 é obtida com a classe [jumbotron] da 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 na linha;
- linha 7: nestas duas colunas insere-se uma imagem;
- linhas 9-11: nas outras 10 colunas, insere-se o texto;
3.6.3. Exemplo 3
As vistas terão uma barra de comandos na parte superior. Nela encontrar-se-ão opções de comando, ligações ou botões. Encontrar-se-ão também elementos de formulário. Por exemplo:
![]() |
Para obter este resultado, duplicamos o [app-01.html] no [app-04.html] e alteramos 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">
<!-- modo de depuração -->
<label style="width: 100px">
<input type="checkbox">
<span style="color: white">Debug</span>
</label>
<!-- formulário de identificação -->
<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] irá definir o estilo da barra de navegação. A classe [navbar-inverse] atribui-lhe um fundo preto. A classe [navbar-fixed-top] irá garantir que, ao «deslizar» a página exibida pelo navegador, a barra de navegação permaneça na parte superior do ecrã;
- linhas 6-14: definem a área [1]. Trata-se, tipicamente, 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 comandos. Num smartphone, esta área desaparece numa área de menu;
- linha 16: a classe [navbar-form] define o estilo de um formulário da barra de comandos. A classe [navbar-right] posiciona-o à direita desta;
- linhas 23-32: as quatro áreas de preenchimento do formulário da linha 17, [3]. Estão dentro de uma classe [form-group] que define os elementos de um formulário e cada uma delas tem a classe [form-control];
- linha 33: a classe [btn] que já vimos anteriormente, enriquecida com a classe [btn-success], que lhe confere a cor verde;
3.6.4. Exemplo 4
A barra de comandos permitirá mudar de idioma através de uma lista suspensa:

Para obter este resultado, duplicamos a classe [app-01.html] na classe [app-05.html] e adicionamos as seguintes linhas à barra de comando:
<button class="btn btn-success">
Connexion
</button>
<!-- idiomas -->
<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 a 21.
- linha 5: a classe [btn-group] define o estilo de um grupo de botões. Existem dois nas linhas 6 e 9;
- linhas 6-8: o primeiro botão define o texto da lista suspensa. A classe [btn-danger] atribui-lhe a cor vermelha;
- linhas 9-12: o segundo botão é o da lista suspensa. Está colado ao primeiro, o que dá a impressão de ser 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 elementos da lista suspensa são os elementos de uma lista não ordenada;
3.6.5. Exemplo 5
Para validar um formulário ou para navegar, o utilizador terá à sua disposição, na barra de comandos, opções ou botões como os que se seguem:
![]() |
Foram implementadas opções de menu em [1]. Para obter este resultado, duplicamos [app-01.html] em [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>
<!-- opções do menu -->
<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>
<!-- botões à direita -->
<form class="navbar-form navbar-right" role="form">
...
</form>
</div>
</div>
</div>
</div>
- As opções do menu são obtidas através das linhas 8-29. Trata-se, mais uma vez, de elementos de uma lista <ul>. A classe [active] faz com que o texto fique destacado, indicando assim que é possível clicar na opção.
3.6.6. Exemplo 6
Apresentaremos os médicos e os clientes em listas suspensas, como se segue:
![]() |
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 [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>
<!-- Núcleo do Bootstrap JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- script local -->
<script>
$('.selectpicker').selectpicker();
</script>
</body>
</html>
- linha 5: é necessário importar a folha de estilo de [bootstrap-select];
- linha 13: o atributo [data-style] é utilizado pelo [bootstrap-select]. Serve para aplicar um estilo à lista suspensa. Aqui, atribuímos-lhe a forma de um botão azul [btn-primary];
- linha 13: o atributo [class] é utilizado na linha 23. Pode ser qualquer um;
- linhas 14-17: os elementos da lista suspensa. Aqui temos as balizas clássicas HTML;
- linha 22: é necessário importar o JS a partir do [bootstrap-select];
- linhas 24-26: um script JS executado no final do carregamento da página;
- linha 25: uma instrução JQuery. Aplica-se o método [selectpicker] (selectpicker()) a todos os elementos com a classe [selectpicker] ($('.selectpicker')). Existe apenas um, a tag <select> da 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, vamos utilizar uma tabela «responsiva» fornecida pela biblioteca JS [footable]:
![]() |
- em [1]: a tabela com uma apresentação normal;
- em [2]: a tabela quando se reduz o tamanho da janela do navegador. A coluna [Action] passa automaticamente para a linha seguinte. É a isto que se chama um componente «responsivo» ou, simplesmente, adaptável.
Duplicamos o [app-01.html] para o [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]. Trata-se dos ficheiros CSS e JS fornecidos pela biblioteca [footable];
- a linha 3 faz referência ao seguinte ficheiro CSS:
@CHARSET "UTF-8";
#intervalos th {
text-align: center;
}
#intervalos 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] encontrado no site da biblioteca.
- linha 8: insere a tabela numa linha [row] e numa moldura 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] indica a coluna que contém o símbolo [+/-] que expande/retrai a linha;
- linha 19: o atributo [data-hide='phone'] indica que a coluna deve ser ocultada se o ecrã tiver o tamanho de um ecrã de telemóvel. Também é possível utilizar o valor «tablet»;
3.6.8. Exemplo 8
Para ajudar o utilizador, vamos criar balões de ajuda (tooltip) em torno dos principais componentes das vistas:
![]() |
Para obter este resultado, duplicamos o [app-01.html] no [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>
<!-- opções do menu -->
<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>
<!-- Núcleo do Bootstrap JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- script local -->
<script>
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>
As bolhas de ajuda são fornecidas pela biblioteca [angular-ui-bootstrap], que, por sua vez, se baseia na biblioteca [angular]. A linha 50 importa a biblioteca [angular-ui-bootstrap]. Para implementar os componentes da biblioteca [angular-ui-bootstrap], é necessário criar um módulo Angular. Isto é feito nas linhas 52 a 55. Estas linhas definem um módulo Angular denominado [rdvmedecins] (1.º parâmetro). Um módulo Angular pode utilizar outros módulos Angular. É a isso que se chamam dependências do módulo. Estas são fornecidas numa tabela 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 que nos irá fornecer as bolhas de ajuda.
A linha 54 define um módulo Angular. Por predefinição, isto não tem qualquer efeito na página. Indica-se que a página deve ser gerida pelo Angular, associando-a a um módulo Angular. É isso que se faz 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 processados pelo módulo [ui.bootstrap].
A sintaxe da bolha de ajuda é a seguinte:
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
No exemplo acima, adiciona-se uma bolha de ajuda ao texto [Home]:
- [tooltip]: define o texto da bolha de ajuda;
- [tooltip-placement]: define a sua posição (bottom, top, left, right);
O Angular JS permite adicionar novas balizas ou novos atributos aos que já existem na linguagem HTML. Esta extensão da linguagem HTML é feita através de diretivas do Angular. Aqui, os atributos [tooltip] e [tooltip-placement] são atributos criados pelo [angular-ui-bootstrap].
3.6.9. Exemplo 9
Para ajudar o utilizador a escolher o dia de um compromisso, vamos apresentar-lhe um calendário:

Tal como acontece com as bolhas de ajuda, este calendário é fornecido pela biblioteca [angular-ui-bootstrap]. Para obter este resultado, duplicamos o [app-01.html] no [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>
...
<!-- script local -->
<script>
// --------------------- módulo 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 baliza <datepicker> da linha 16, definida pela biblioteca [angular-ui-bootstrap]:
- [show-weeks='true']: para apresentar os números das semanas;
- [class='well']: para envolver o calendário numa área cinzenta com cantos arredondados;
- [ng-model='jour']: os atributos [ng-*] são atributos do Angular. O atributo [ng-model] designa um dado que será colocado no modelo da vista. Quando o utilizador clicar numa data, esta será colocada na variável [jour] do modelo. Esta variável é utilizada na linha 10. A sintaxe {{expressão}} permite avaliar uma expressão composta por elementos do modelo. Aqui, {{dia}} irá apresentar o valor da variável [jour] do modelo. Uma característica importante do Angular é que a vista acompanha automaticamente as alterações na variável [jour]. Assim, quando o utilizador alterar as datas, essas alterações serão imediatamente apresentadas na linha 10. De um modo geral, o funcionamento é o seguinte:
- uma vista V está associada a um modelo M;
- o Angular observa o modelo M e atualiza automaticamente a vista V sempre que há uma alteração no seu modelo M;
A sintaxe {{dia|data}} é designada por filtro. Não é o valor de [jour] que é apresentado, mas sim o valor de [jour] filtrado por um filtro denominado [date]. Este filtro está predefinido no Angular. Serve para formatar datas. Aceita parâmetros que especificam o formato pretendido. Assim, a expressão {{dia | data:'fullDate'}} indica que se pretende o formato completo da data, neste caso [Friday, June 20, 2014], porque o calendário está, por predefinição, em inglês. Abordaremos a sua internacionalização em breve.
3.6.10. Conclusão
Apresentámos os elementos do framework CSS Bootstrap que iremos utilizar. Tratava-se de componentes passivos: os seus eventos não eram geridos. Assim, clicar nos botões ou nos links não provocava qualquer ação. Estes eventos serão geridos em JavaScript. É possível utilizar esta linguagem sem o auxílio de frameworks, mas, tal como aconteceu no lado do servidor, alguns frameworks tornam-se indispensáveis no lado do cliente. É o caso do framework Angular JS, que traz consigo uma nova forma de abordar o desenvolvimento de aplicações JavaScript executadas por um navegador. Vamos apresentá-lo agora.
3.7. Descoberta do Angular JS
Vamos agora ilustrar algumas das características do framework Angular JS utilizadas na aplicação. Já nos deparámos com algumas delas:
- uma página HTML é alimentada pelo Angular JS se lhe for associado um módulo:
<html ng-app="rdvmedecins">
- O Angular permite criar novas tags e novos atributos HTML através de diretivas:
- O Angular permite criar filtros:
- Uma vista V apresenta um modelo M. O Angular observa o modelo M e atualiza automaticamente a vista V sempre que ocorre uma alteração no seu modelo M. O valor de uma variável do modelo M é apresentado na vista V através de:
Vamos começar por aprofundar a implementação do Padrão de Design Modelo – Vista – Controlador no Angular. Recorde-se as relações que existem entre eles do ponto de vista da arquitetura:
![]() |
- a vista V1 apresenta o modelo M1 construído pelo controlador C1. Este último contém não só o modelo M1, mas também os gestores de eventos da vista V1. Estamos no ciclo 5, 8, 9:
- [5]: ocorre um evento na vista V1. Este é tratado pelo controlador C1;
- este 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 já referimos, esta última etapa é automática. Ao contrário do que acontece noutros frameworks, não há, no caso do MVC, um «push» explícito (o C1 envia o modelo M1 para o V1) nem um «pull» explícito (a vista V1 vai buscar o modelo M1 em C1). Existe um «push» 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 retomar o exemplo do calendário. Já vimos a diretiva que o gera:
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
Esta diretiva admite outros atributos além dos apresentados acima, entre os quais o atributo [min-date], que define a data mínima que se pode selecionar no calendário. Isto será útil para nós. Quando o utilizador seleciona uma data para um compromisso, esta deve ser igual ou posterior à data do dia atual. Escreveremos, então:
<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>
onde [dateMin] será uma variável do modelo da página cujo valor corresponderá à data de hoje. Isto resultará na seguinte página:
![]() |
- em [1], estamos a 19 de junho de 2014. O cursor indica que é possível selecionar o dia 19 de junho;
- em [2], o cursor indica que não é possível selecionar o dia 18 de junho;
Duplicamos o [app-10.html] para o [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>
<!-- Núcleo do Bootstrap JavaScript ================================================== -->
...
<!-- script local -->
<script>
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// data mínima
$scope.minDate = new Date();
}]);
</script>
</body>
</html>
Analisemos primeiro o script local das 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. Aqui não haverá nenhum gestor de eventos;
- linhas 30-31: o controlador [rdvMedecinsCtrl] pertence ao módulo [rdvmedecins]. É possível adicionar quantos controladores se 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] é um array com o formato ['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 Angular JS irá fornecer à função.
Voltemos à arquitetura de uma aplicação Angular:
![]() |
Acima, o controlador C1 contém o conjunto de gestores de eventos da vista V1, bem como o modelo M1 desta última. Os manipuladores de eventos podem necessitar de um ou mais serviços [6] para realizarem as suas tarefas. Passa-se todos esses serviços como parâmetros da função de construção do controlador:
Os serviços Si são singletons. O Angular cria-os numa única instância. 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. Neste processo de minificação, a tabela acima passa a ser:
Os parâmetros perdem os seus nomes. No entanto, trata-se dos nomes dos serviços. Por isso, é importante manter esses nomes. É por isso que são passados como cadeias de caracteres como parâmetros que precedem a função. As cadeias de caracteres não são alteradas no processo de minificação. Quando o Angular for construir o controlador com o novo array, irá substituir a1 por S1, a2 por S2, ... 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]:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// data mínima
$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;
- é isso que se faz na linha 6. Cria-se o campo [minDate] com o valor da data de hoje;
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 é associado ao controlador [rdvMedecinsCtrl] através do atributo [ng-controller]. Isto significa que tudo o que estiver dentro da baliza <body> utilizará o controlador [rdvMedecinsCtrl] para gerir os seus eventos e obter o seu modelo M. Uma página HTML pode depender de vários controladores, aninhados ou não uns nos outros:
Acima:
- o conteúdo de [div1] (linhas 1-10) apresenta o modelo M1 gerido pelo controlador c1. As tags desta área podem fazer referência a gestores de eventos do controlador c1;
- o conteúdo de [div11] (linhas 3-4) apresenta o modelo M11 gerido pelo controlador c11, mas também o modelo M1. Existe herança de modelos. As balizas desta zona podem referenciar tanto gestores de eventos do controlador c11 como gestores de eventos do controlador c1. Não podem referenciar nem o modelo M12 do controlador c12 nem os gestores de eventos deste último. De facto, o controlador c12 não é reconhecido nas linhas 3-5;
- linhas 7-9: pode-se seguir um raciocínio análogo ao apresentado 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, [$scope.minDate]. O campo é sempre procurado no objeto $scope.
3.7.2. Exemplo 2: localização das datas
Por enquanto, o calendário não nos é de grande utilidade, uma vez que se trata de um calendário inglês. É possível adaptá-lo à localização:
![]() |
- em [1], temos um calendário em francês;
- em [2], passamos-o 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, alteramos 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">
<!-- o calendário-->
<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>
<!-- os idiomas -->
<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>
As alterações são poucas. Trata-se simplesmente da adição, nas linhas 21 a 31, da lista suspensa de idiomas. Pela primeira vez, deparamo-nos com um gestor de eventos nas linhas 27 e 28:
- linha 27: o atributo [ng-click] é um atributo do Angular que indica o gestor de eventos a executar quando se clica no elemento com esse atributo. Aqui, a função [$scope.setLang('fr')] será executada. Esta função definirá o calendário em francês;
- linha 28: aqui, definimos o calendário em inglês;
- linha 35: como o JavaScript do controlador é bastante extenso, colocamo-lo num ficheiro [rdvmedecins.js];
O Angular gere a localização das vistas com um módulo chamado [ngLocale]. A definição do nosso módulo [rdvmedecins] será, portanto, a seguinte:
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);
Na linha 2, não se deve esquecer as dependências, pois o Angular por vezes não é muito preciso nas suas mensagens de erro. Esquecer uma dependência é, por isso, particularmente difícil de detetar. Aqui temos uma nova dependência do módulo [ngLocale].
Por predefinição, o Angular apenas gere a localização de datas, números, etc., que têm variantes locais. Não gere a internacionalização de textos. Para tal, utilizaremos a biblioteca [angular-translate]. A gestão da localização é feita 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 consta no 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 vemos os elementos que permitem criar um calendário francês:
- linhas 10-18: a tabela dos dias da semana;
- linhas 19-32: a tabela dos meses do ano;
- linhas 33-41: a tabela dos dias da semana abreviada;
- linhas 42-55: a tabela dos meses do ano abreviados;
- linhas 56-63: formatos de data e hora. Na linha 62, reconhece-se o formato «dd/mm/aa» das datas francesas;
- linhas 65-95: informações sobre a formatação de números. Isso não nos interessa aqui;
- linha 96: o identificador «fr-fr» da configuração regional 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 do USA (en-us).
O código acima não é muito fácil de ler. Ao lê-lo com atenção, descobre-se que todo este código define a variável [$locale] da linha 4. É ao alterar o valor desta variável que se consegue a internacionalização de datas, números, moeda, etc. Curiosamente, o Angular não previu a alteração da variável [$locale] durante a execução. Define-se essa variável de uma vez por todas, importando o ficheiro da localização pretendida:
<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>
Não adianta importar todos os ficheiros das configurações regionais desejadas, pois, como vimos, cada ficheiro faz apenas uma coisa: definir a variável [$locale]. É o último ficheiro importado que prevalece e, a partir daí, já não há forma de alterar a configuração regional.
Ao navegar na Internet à procura de uma solução para este problema, não encontrei nenhuma. Proponho aqui uma: [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. A ideia é colocar as diferentes configurações regionais de que precisamos num dicionário. É aí que iremos buscá-las quando for necessário alterá-las. O código JavaScript de [rdvmedecins.js] tem a seguinte estrutura:
![]() |
Se retirarmos a definição das configurações regionais, que ocupa 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: a função de construção do controlador recebe dois parâmetros:
- $scope: para criar o modelo da vista;
- $locale: que é a variável que gere a localização do calendário. É esta que deve ser alterada quando se muda de idioma;
- linha 13: a variável [minDate] do modelo é inicializada com a data de hoje;
- linha 15: define o dicionário [locales]. Note-se que não se escreveu [$scope.locales]. A variável [locales] não faz, de facto, parte do modelo apresentado na 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 obtidos, respetivamente, dos ficheiros JS, [angular-locale_fr-fr.js] e [angular-locale_en-us.js]. O mais difícil é não se enganar nas inúmeras parênteses deste dicionário...
- linha 217: inicializa-se a variável $locale com locales['fr'], ou seja, a versão francesa da localização. Não se pode escrever simplesmente [$locale=locales['fr']], o que atribui a $locale o endereço de locales['fr']. É necessário efetuar uma cópia de valor. Isto pode ser feito com a função predefinida [angular.copy];
- linha 219: a variável [jour] do modelo é inicializada com a data de hoje. Isto faz com que o calendário seja apresentado com a data predefinida;
- linhas 223-230: definem o gestor de eventos que é chamado aquando da mudança de idioma. Note-se a sintaxe:
para definir um gestor de eventos que se chamaria [nom_fonction] e que aceitaria os parâmetros [param1, param2, ...];
Recordemos o código HTML da lista suspensa:
<!-- os idiomas -->
<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: a seleção do francês leva à chamada de [setLang('fr')];
- linha 9: a seleção do inglês leva à chamada de [setLang('en')];
- linha 3: o atributo [is-open] é um valor booleano que controla a abertura (true) ou o fecho (false) da lista suspensa. É inicializado com a variável [isopen] do modelo da vista;
Voltemos ao código de [rdvmedecins.js]:
- linha 225: alteramos o valor da variável [$locale] com o valor adequado do dicionário [locales];
- linha 227: já referimos 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 apresentado pela vista V. É necessário encontrar uma forma de alterar este modelo M para que o calendário seja atualizado e utilize a sua nova configuração regional. Aqui, alteramos a variável [jour] do modelo do calendário. Inicializa-se com um novo ponteiro (new) que aponta para uma data idêntica à que está a ser apresentada. [$scope.jour.getTime()] é o número de milissegundos decorridos entre 1 de janeiro de 1970 e a data apresentada pelo calendário. Com este número, reconstrói-se uma nova data. É claro que vamos obter a mesma data e o calendário permanecerá posicionado na data que exibia. Mas o valor de [$scope.jour], que é, na realidade, um ponteiro, terá mudado e o calendário será atualizado;
- linha 229: atribui-se a false o valor da variável [isopen] do modelo. 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] passará a false, o que terá como efeito fechar a lista suspensa.
3.7.3. Exemplo 3: internacionalização dos textos
Voltemos à localização do calendário:
![]() |
Em [3], vemos que o calendário está em inglês, mas os textos em [Calendrier, Langues] não estão. Por predefinição, o Angular não oferece nenhuma ferramenta para a internacionalização de mensagens. Vamos utilizar aqui a biblioteca [angular-translate] (https://github.com/angular-translate/angular-translate).
Vamos desenvolver o seguinte exemplo:
![]() |
- em [1], a vista 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] é alterado da seguinte forma:
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// configuração i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// mensagens em francês
$translateProvider.translations("fr", {
'msg_header': 'Consultório Médico <br/> Os Médicos Associados',
'msg_langues': 'Idiomas',
'msg_agenda': 'Agenda de {{título}} {{nome próprio}} {{apelido}}<br/>no {{dia}}',
'msg_calendrier': 'Calendário',
'msg_jour': 'Dia selecionado: ',
'msg_meteo': "Hoje vai chover..."
});
// mensagens em inglês
$translateProvider.translations("en", {
'msg_header': 'The Associated Doctors',
'msg_langues': 'Idiomas',
'msg_agenda': "Diário de {{título}} {{nome próprio}} {{apelido}}<br/> em {{dia}}",
'msg_calendrier': 'Calendário',
'msg_jour': 'Dia selecionado: ',
'msg_meteo': 'Hoje vai chover...'
});
// idioma predefinido
$translateProvider.preferredLanguage("fr");
}]);
- linha 2: a primeira alteração consiste na 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]. Ao iniciar uma aplicação Angular, o framework instancia todos os serviços necessários à aplicação, tanto os predefinidos pelo Angular como os definidos pelo utilizador. Por enquanto, ainda não definimos quaisquer serviços. A função [config] do módulo de uma aplicação é executada antes de qualquer instanciação de serviço. Pode ser utilizada para definir informações de configuração dos 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] é um array ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)], em que Oi é um objeto conhecido e 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] aceita dois parâmetros:
- o primeiro parâmetro é a chave de um idioma. Pode-se definir o que se quiser. Aqui, definimos «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':'msg1', 'chave2':'msg2', ...};
- 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 predefinido. O seu parâmetro é um dos argumentos utilizados como primeiro parâmetro da função [$translateProvider.translations], ou seja, neste caso, «fr» (linha 7) ou «en» (linha 16);
- note-se que existem três tipos de mensagens:
- mensagens sem parâmetros nem elementos HTML (linhas 9, 11, 12, ...),
- mensagens com elementos HTML (linhas 8, 10, ...),
- mensagens com parâmetros (linhas 10, 19);
Agora, duplicamos o [app-11.html] no [app-12.html] e fazemos as seguintes alterações:
<div class="container">
<!-- um primeiro texto com elementos HTML -->
<h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
<!-- um segundo texto com parâmetros -->
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
<!-- um terceiro texto traduzido pelo controlador -->
<h3 class="alert alert-danger">{{msg2}}</h3>
<pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- o calendário-->
<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>
<!-- os idiomas -->
<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, 23;
- é possível distinguir três sintaxes:
- 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={{dictionnaire]}}] (linha 5), 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 desta vista:
Consultório Médico<br/>Os Médicos Associados | Os Médicos Associados | |
Calendário | Calendário | |
Idiomas | Idiomas | |
Dia selecionado: | Dia selecionado: |
Vamos agora analisar a linha 5:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Note-se que [msg.text] e [msg.model] não estão entre apóstrofos. Não se trata de cadeias de caracteres, mas sim de elementos do modelo:
- msg.text: define a chave da mensagem parametrizada a utilizar;
- 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 como valor a chave [msg_agenda], que está associada a dois valores:
- Agenda de {{título}} {{nome próprio}} {{apelido}}<br/>no {{dia}} no dicionário francês;
- Diário de {{título}} {{nome próprio}} {{apelido}}<br/> no {{dia}} no dicionário inglês;
A mensagem a apresentar tem, portanto, quatro parâmetros [titre, prenom, nom, jour];
- linha 245: o campo [model] é um dicionário que atribui um valor a estes quatro parâmetros. Existe uma dificuldade com o parâmetro [jour]. Pretende-se apresentar o nome completo do dia. Este varia consoante seja em francês ou em inglês. Utiliza-se, então, o filtro [date] já utilizado na vista na forma {{ dia | data:'fullDate'}}. É possível utilizar qualquer filtro no código JavaScript na forma $filter('filter')(valor, complementos), em que $filter é um objeto predefinido do Angular e 'filter' é o nome do filtro;
- linhas 33-34: o objeto predefinido $filter é passado como parâmetro para o controlador, o que permite a sua utilização na linha 245;
Voltemos a outra linha da vista apresentada:
<!-- um terceiro texto traduzido pelo controlador -->
<h3 class="alert alert-danger">{{msg2}}</h3>
Todas as traduções anteriores foram feitas na vista através de atributos do módulo [pascalprecht.translate]. Também é possível optar por fazer esta tradução do 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');
Utiliza-se a mesma sintaxe que para o filtro «date», uma vez que «translate» também é um filtro. Aqui, solicita-se a mensagem de chave «msg_meteo».
Vamos analisar o mecanismo de mudança de idiomas. Vimos que a função [config] de configuração do módulo [rdvmedecins] tinha definido o francês como idioma predefinido (linha 9 abaixo):
// configuração i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// mensagens em francês
$translateProvider.translations("fr", {...});
// mensagens em inglês
$translateProvider.translations("en", {...});
// idioma predefinido
$translateProvider.preferredLanguage("fr");
}]);
Recorde-se também que a localização predefinida era igualmente o francês. Na inicialização do controlador [rdvmedecins], foi escrito:
// definimos a localização para francês
angular.copy(locales['fr'], $locale);
- linha 2: [locales] é um dicionário que criámos;
Não existe qualquer ligação entre a internacionalização das mensagens proporcionada pelo módulo [pascalprecht.translate] e a localização das 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 que não se influenciam mutuamente.
Chegou agora a altura de ver o que acontece quando o utilizador muda de idioma:

- linha 251: durante uma mudança de idioma, 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 impacto no idioma das traduções;
- linha 259: altera-se o idioma das traduções. Utiliza-se o objeto [$translate] fornecido pelo módulo [pascalprecht.translate]. Para tal, é necessário inseri-lo no controlador:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {
Nas linhas 3 e 4 acima, insere-se o objeto $translate;
- o parâmetro «lang» da função [$translate.use(lang)] deve ter como valor uma das chaves utilizadas na configuração como primeiro parâmetro da função [$translateProvider.translations], ou seja, «fr» ou «en». É precisamente esse o caso;
- linha 261: recalcula-se o valor de msg2. Porquê? Na vista, após a mudança de idioma efetuada pela linha 259, todos os atributos [translate] presentes serão reavaliados. Não será o caso da expressão {{msg2}}, que não possui esse atributo. Por isso, calcula-se o seu novo valor no controlador. Isto deve ser feito após a mudança de idioma da linha 259, para que o novo idioma seja utilizado no cálculo de [msg2];
Se ficarmos por aqui, observamos duas anomalias:
![]() |
- em [1], o dia permaneceu em francês, enquanto o resto da vista está em inglês;
- em [2] e [3], o dia selecionado é 24 de junho, enquanto que em [1], o dia permanece fixado em 20 de junho;
Vamos tentar explicar o que se passa antes de encontrar soluções. A mensagem [1] é gerada 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 apresentada 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]. É então possível forçar essa reavaliação no controlador:
// ------------------- gestor de eventos
// mudança de idioma
$scope.setLang = function (lang) {
...
// atualização do msg2
$scope.msg2 = $filter('translate')('msg_meteo');
// e a data da mensagem
$scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};
A cada mudança de idioma, a linha 8 acima reavalia o dia apresentado. Isto resolve efetivamente o primeiro problema, mas não o segundo (o dia apresentado na mensagem não muda quando se seleciona outro dia no calendário). A razão para este comportamento é a seguinte. A mensagem é apresentada na vista com o código seguinte:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
A vista exibida V 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 é gerido, o que faz com que o modelo [msg] não mude e, consequentemente, a vista também não mude. Atualizamos, na vista, a definição do calendário:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>
Acima, indicamos que o clique no calendário deve ser tratado pela função [$scope.calendarClick]. Esta função é a seguinte:

- linha 267: o gestor do clique no calendário;
- linha 269: forçamos a atualização do dia apresentado através da mensagem [msg];
3.7.4. Exemplo 4: um serviço de configuração
Voltemos à arquitetura de uma aplicação Angular JS:
![]() |
Vamos centrar-nos aqui no conceito de serviço. Trata-se de um conceito bastante abrangente. Se, no exemplo anterior, a camada [DAO] é 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 reconhece-o através desse nome;
- um serviço pode ser injetado pelo Angular nos controladores e noutros serviços;
Alguns dos serviços que vamos configurar no módulo [rdvmedecins] terão de ser configurados. Como um serviço pode ser injetado noutro serviço, é tentador fazer a configuração num serviço a que chamaremos [config] e injetá-lo nos serviços e controladores a configurar. Vamos agora descrever este processo.
Duplicamos o [app-13.html] para o [app-14.html] e efetuamos as seguintes alterações:
<div class="container">
<!-- verificação da mensagem em espera -->
<label>
<input type="checkbox" ng-model="waiting.visible">
<span>Voir le message d'attente</span>
</label>
<!-- a mensagem em espera -->
<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 das linhas 9-15 é ou não exibida. O valor da caixa de seleção é colocado na variável [waiting.visible] do modelo M da vista V. Este valor é true se a caixa estiver marcada e false caso contrário. Isto funciona nos dois sentidos. Se atribuirmos o valor «true» à variável [waiting.visible], a caixa de seleção ficará 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 marcarmos a caixa de seleção da 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 quando se desmarca a caixa de seleção da linha 4: a mensagem de espera é ocultada;
- linha 10: a mensagem de espera é traduzida (filtro translate);
- linha 11: ao clicar no botão, é executado o método [waiting.cancel()] (atributo ng-click);
- linha 12: o texto do botão é traduzido;
- linha 19: o código JavaScript da aplicação é colocado num novo ficheiro JS [rdvmedecins-02] para não perder o código já escrito e que agora tem de ser reorganizado;
O resultado é o seguinte:
![]() |
- em [1], caixa não marcada;
- em [2], caixa 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 no controlador o dicionário locales={'fr':..., 'en': ...}, que tinha 200 linhas. Este dicionário é claramente um elemento de configuração, pelo que é migrado para o serviço [config], nas linhas 38-39. Este serviço está definido da seguinte forma:

- linhas 38-39: é criado um serviço com a função [factory] do objeto [angular.module]. A sintaxe desta função é semelhante à das anteriores: factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){...}]), em que os Oi são os nomes de objetos conhecidos pelo Angular (pré-definidos ou criados pelo programador) e que o Angular injeta como parâmetro da função factory. Como, neste caso, a função não tem parâmetros, utilizou-se uma sintaxe mais curta, também aceite: `factory('nom_service', function (){...}])`;
- linha 40: a função [factory] deve implementar o serviço por meio de um objeto que ela devolve. É esse objeto que constitui o serviço. É por isso que a função se chama «factory» (fábrica de criação de objetos);
Em geral, o código de um serviço tem o seguinte formato:
Angular.module('nom_module')
.factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
// preparação do serviço
...
// retornamos o objeto que implementa o serviço
return {
// campos
...
// métodos
...
}
});
- linha 6: é devolvido um objeto JS que pode conter tanto campos como métodos. São estes últimos que prestam o serviço;
Aqui, o serviço [config] define apenas campos e nenhum método. Nele será colocado tudo o que pode ser configurado na aplicação:
- linhas 42-47: as chaves das mensagens a traduzir;
- linhas 59-62: os URL da aplicação;
- linhas 64-69: os URL do serviço web remoto;
- linha 71: uma chamada HTTP para um serviço web que não responde; pode demorar algum tempo. Define-se aqui em 1 segundo o tempo máximo de espera pela resposta do serviço web. Passado este prazo, a chamada HTTP falha e é lançada uma exceção JS;
- linha 73: antes de cada chamada ao servidor, vamos simular uma espera cuja duração é aqui definida 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 possa ser cancelada, a operação tem de durar pelo menos alguns segundos. Utilizaremos esta espera artificial para simular operações demoradas;
- linha 75: no modo [debug=true], são apresentadas 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 das duas configurações regionais «fr» e «en». Anteriormente, este encontrava-se no controlador [rdvMedecinsCtrl];
Com este serviço, o controlador [rdvMedecinsCtrl] sofre as seguintes alterações:

- linhas 284-285: o serviço [config] é inserido 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 da 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 da linha 316. Este campo é, portanto, um método ou uma função;
- linha 316: a função [cancel] é privada (não se escreveu $scope.cancel=function(){}). Voltemos ao 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 de cancelamento, o método [$scope.waiting.cancel()] é chamado. Em última análise, é a função privada cancel, na linha 316, que é executada. Esta limita-se a ocultar a mensagem de espera, definindo como «false» a variável do modelo [waiting.visible] (linha 318);
3.7.5. Exemplo 5: programação assíncrona
Apresentamos agora um novo serviço com um novo conceito: o da 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. Vamos apresentar dois deles;
- [dao]: o serviço de acesso ao serviço web de marcação de consultas. Iremos apresentá-lo em breve;
Vamos escrever a seguinte aplicação:
![]() |
![]() |
- o objetivo é fazer com que o banner [2] apareça durante um período de tempo definido por [1]. A espera pode ser cancelada por [3].
Duplicamos o [app-01.html] no [app-15.html] e alteramos o código da seguinte forma:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<!-- a mensagem de espera -->
<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>
<!-- o formulário -->
<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 a exibição do campo antes de as expressões Angular do mesmo terem sido calculadas. Isto evita que o campo seja exibido brevemente antes da avaliação do atributo [ng-show], que irá, de facto, provocar a sua ocultação;
- 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 as mensagens;
- linha 41: o serviço [config] que descrevemos;
- linha 286: o serviço [utils] que vamos criar;
- linha 315: o controlador [rdvmedecinsCtrl] que vamos criar;
Adicionamos à função [config] uma nova chave de mensagem (linhas 6, 11):
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// mensagens em francês
$translateProvider.translations("fr", {
...
'msg_waiting_time_text': «Tempo de espera: »
});
// mensagens em inglês
$translateProvider.translations("en", {
...
'msg_waiting_time_text': "Tempo de espera:"
});
// idioma predefinido
$translateProvider.preferredLanguage("fr");
}]);
Adicionamos ao serviço [config] uma nova linha (linha 6) para esta chave de mensagem:
angular.module("rdvmedecins")
.factory('config', function () {
return {
// mensagens a internacionalizar
...
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) {
// exibição da representação JSON de um objeto
function debug(message, data) {
if (config.debug) {
var text = data ? message + " : " + angular.toJson(data) : message;
console.log(text);
}
}
// espera
function waitForSomeTime(milliseconds) {
// espera assíncrona de milissegundos
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// retorna a tarefa
return task;
};
// instância do serviço
return {
debug: debug,
waitForSomeTime: waitForSomeTime
}
}]);
- linha 2: o serviço chama-se [utils] (1.º parâmetro). Tem dependências de três serviços: dois serviços Angular predefinidos, $timeout e $q, e o serviço config. O serviço [$timeout] permite executar uma função após ter decorrido um determinado período de tempo. O serviço [$q] permite criar 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 coerência, foram-lhes atribuídos os nomes das funções a que fazem referência;
- linhas 4-9: o método [debug] escreve na consola uma mensagem [message] e, eventualmente, a representação JSON de um objeto [data]. Isto permite exibir objetos de qualquer complexidade;
- linhas 12-20: o método [waitForSomeTime] cria uma tarefa assíncrona com a duração de [milliseconds] milissegundos;
- linha 14: criação de uma tarefa através do objeto predefinido [$q] (https://docs.angularjs.org/api/ng/service/$q). Segue-se o API da tarefa denominada [deferred] na documentação do Angular:

- uma tarefa assíncrona [task] é criada pela instrução [$q.defer()];
- esta é concluída através de um dos dois métodos:
- [task.resolve(value)]: que conclui a tarefa com sucesso e devolve o valor [value] aos que aguardam a conclusão da tarefa;
- [task.reject(value)]: que conclui a tarefa com falha e devolve o valor [value] aos que aguardam a conclusão da tarefa;
A tarefa [task] pode fornecer informações regularmente a quem aguarda a sua conclusão:
- [task.notify(value)]: envia o valor [value] para quem está à espera do fim da tarefa. A tarefa continua a ser executada;
Quem quiser aguardar a conclusão da tarefa utiliza o campo [promise] da mesma:
O objeto [promise] tem o seguinte API (http://www.frangular.com/2012/12/api-promise-angularjs.html):

Para gerir tanto o sucesso como o fracasso da tarefa, escrever-se-á:
- linha 1: recupera-se a promessa da tarefa;
- linha 2: definem-se as funções a executar em caso de sucesso ou de falha. É possível não definir uma função de falha. A função [successCallback] só será executada no final da tarefa [task], caso a tarefa [task.resolve()] tenha sido bem-sucedida. A função [errorCallBack] só será executada no final da tarefa [task], caso a tarefa [task.reject()] tenha falhado.
- linha 3: define-se a função a executar após a execução de uma das duas funções anteriores. Coloca-se aqui o código comum às duas funções [successCallback, errorCallBack].
Voltemos ao código da função [waitForSomeTime]:
// espera
function waitForSomeTime(milliseconds) {
// espera assíncrona de milissegundos
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// retorna a tarefa
return task;
};
- linha 4: é criada uma tarefa;
- linhas 5-7: o objeto [$timeout] permite definir uma função (1.º parâmetro) que é executada após um determinado intervalo de tempo expresso em milissegundos (2.º parâmetro). Aqui, o segundo parâmetro da função [$timeout] é o parâmetro do método (linha 1);
- linha 6: ao fim do intervalo de tempo [milliseconds], a tarefa é concluída com sucesso;
- linha 9: devolve-se a tarefa [task]. É importante compreender que a linha 9 é executada imediatamente após a definição do objeto [$timeout]. Não se aguarda que o prazo [milliseconds] tenha decorrido. O código das linhas 2 a 10 é, portanto, executado em dois momentos diferentes:
- uma primeira vez, quando se define o objeto [$timeout];
- uma segunda vez, quando o tempo de espera [milliseconds] tiver decorrido;
Trata-se, neste caso, de uma função assíncrona: o seu resultado é obtido num momento posterior ao da sua execução.
O código do controlador que utiliza o serviço [config] é o seguinte:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
function ($scope, utils, config, $filter) {
// ------------------- inicialização do modelo
// mensagem de espera
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
// tarefa em espera
var task;
// registos
utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
utils.debug("locales['fr']=", config.locales['fr']);
// execução da ação
$scope.execute = function () {
// registo
utils.debug('début', new Date());
// é apresentada a mensagem de espera
$scope.waiting.visible = true;
// espera simulada
task = utils.waitForSomeTime($scope.waiting.time);
// fim da espera
task.promise.then(function () {
// sucesso
utils.debug('fin', new Date());
}, function () {
// falha
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// fim da espera em todos os casos
$scope.waiting.visible = false;
});
};
// cancelamento da espera
function cancel() {
// a tarefa está a ser concluída
task.reject();
}
}]);
- linha 3: o controlador utiliza o serviço [config];
- linha 7: foi adicionado 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 da mensagem de espera apresentada pela vista é colocada no modelo [$scope.waitingTimeText]. De um modo geral, tudo o que é apresentado 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]. Obtém-se o seguinte resultado na consola:
Na linha 2, obtém-se a notação JSON do objeto locales['fr'].
- linha 16: o método executado quando o utilizador clica no botão [Executer];
- linha 18: apresenta a hora de início da execução do método;
- linha 22: inicia-se a tarefa [waitForSomeTime]. Não se aguarda a sua conclusão. A execução continua com a linha 24 seguinte;
- linhas 24-30: definem-se as funções a executar quando a tarefa terminar com sucesso (linha 26) e em caso de erro (linha 29);
- linha 26: apresenta a hora de fim 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 [Annuler]. A instrução da linha 41 interrompe então a tarefa assíncrona com um código de falha;
- linhas 31-34: define-se a função a executar após a execução de uma das duas funções anteriores;
É importante compreender as sequências de execução deste código. No caso de o utilizador definir um tempo de espera de 3 segundos e não cancelar a espera:
- quando clica no botão [Exécuter], a função [$scope.execute] é executada. As linhas 16-34 são executadas sem esperar os 3 segundos. No final desta execução, a vista V é sincronizada com o modelo M. A mensagem de espera é apresentada (ng-show=$scope.waiting.visible=true, linha 20) e o formulário é ocultado (ng-hide=$scope.waiting.visible=true, linha 20);
- a partir desse momento, o utilizador pode voltar a interagir com a vista. Pode, nomeadamente, clicar no botão [Annuler];
- se não o fizer, ao fim de 3 segundos, a função [$timeout] (ver linhas 5-7 abaixo) é executada:
// espera
function waitForSomeTime(milliseconds) {
// espera assíncrona de milissegundos milissegundos
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// retorno da tarefa
return task;
};
- assim, 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 todos os códigos que aguardavam esta conclusão (linha 4 abaixo):
// espera simulada
task = utils.waitForSomeTime($scope.waiting.time);
// fim da espera
task.promise.then(function () {
// sucesso
utils.debug('fin', new Date());
}, function () {
// falha
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// fim da espera em todos os casos
$scope.waiting.visible = false;
});
- a linha 6 acima (conclusão com sucesso) será, portanto, executada. Em seguida, será a vez das linhas 11-14. 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 é apresentado (ng-hide=$scope.waiting.visible=false, linha 13);
As imagens no ecrã são então as seguintes:
Como se pode ver acima, existe um intervalo de 3 segundos (06:01-05:58) entre o início e o fim da espera. Se, pelo contrário, o utilizador cancelar a espera antes dos 3 segundos, surge a seguinte mensagem:
Por fim, é importante compreender que, em qualquer momento, existe apenas um thread de execução denominado «thread do UI» (Interface do Utilizador). O fim de uma tarefa assíncrona é sinalizado por um evento, exatamente como acontece com o clique num botão. Este evento não é processado imediatamente. É colocado na fila de eventos que aguardam a sua execução. Quando chega a sua vez, é processado. Este processamento utiliza o thread do UI e, por isso, durante esse tempo, a interface fica congelada. Não reage às solicitações do utilizador. Por isso, é importante que o processamento de um evento seja rápido. Como cada evento é processado pelo thread do UI, nunca é necessário resolver problemas de sincronização entre threads que se executam em simultâneo. Em cada momento, apenas o thread do UI está a ser executado.
3.7.6. Exemplo 6: os 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:

Vamos duplicar o [app-01.html] para o [app-16.html], que iremos depois modificar da seguinte forma:
<div class="container" ng-cloak="">
<h1>Rdvmedecins - v1</h1>
<!-- a mensagem de espera -->
<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>
<!-- o pedido -->
<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>
<!-- a lista de médicos -->
<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>
<!-- a lista de erros -->
<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 não fica visível quando a mensagem de espera é apresentada (ng-hide="waiting.visible"). É importante referir que os quatro valores introduzidos são armazenados em (atributos ng-model) [waiting.time (ligne 16), server.url (ligne 20), server.login (ligne 24), server.password (ligne 28)];
- linhas 34-39: apresentam 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 percorrer uma lista. A sintaxe ng-repeat="medecin in medecins.data" determina que a baliza <li> seja repetida para cada elemento da lista [medecins.data]. O elemento atual da lista é designado por [medecin];
- linha 37: para cada <li>, escrevem-se o título, o nome próprio e o apelido do médico atual designado pela variável [medecin];
- linhas 42-47: apresentam a lista de erros. Esta lista nem sempre está visível (ng-show="errors.show"). A sua apresentação segue o mesmo modelo que a apresentação da lista de médicos. De um modo geral, para apresentar uma lista de objetos, utiliza-se 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 evolui 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 serve para codificar em Base64 a cadeia [login:password] enviada ao serviço web para autenticação;
- linhas 12-13: a função de inicialização que contém as nossas mensagens internacionalizadas. Surgem novas mensagens. Não as apresentaremos novamente;
- linhas 69-70: o serviço [config] que configura a nossa aplicação. São adicionadas novas chaves de mensagem. Não as apresentaremos mais;
- linhas 318-319: o serviço [utils], que contém métodos utilitários. São adicionados novos métodos. Iremos apresentá-los;
- linhas 385-386: o serviço [dao] responsável pelas interações com o serviço web. É nele que nos vamos concentrar;
- linhas 467-468: o controlador C da vista V que acabámos de apresentar. Vamos apresentá-lo agora, pois é ele o «maestro» 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) {
// ------------------- inicialização do modelo
// modelo
$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;
// tarefa assíncrona
var task;
// execução da ação
$scope.execute = function () {
// atualização do UI
$scope.waiting.visible = true;
$scope.medecins.show = false;
$scope.errors.show = false;
// espera simulada
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// espera
promise = promise.then(function () {
// solicita-se a lista de médicos;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
// analisa-se o resultado da chamada anterior
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// inserimos os dados recolhidos no modelo
$scope.medecins.data = result.data;
// atualiza-se o UI
$scope.medecins.show = true;
$scope.waiting.visible = false;
} else {
// ocorreram erros ao obter a lista de médicos
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
// atualiza-se o UI
$scope.waiting.visible = false;
}
});
};
// cancelamento pendente
function cancel() {
// a tarefa está concluída
task.reject();
// está a ser atualizado o UI
$scope.waiting.visible = false;
$scope.medecins.show = false;
$scope.errors.show = false;
}
}
])
;
- linha 2: o controlador tem uma nova dependência, a do serviço [dao];
- linhas 6-13: o modelo M da vista V é inicializado para a primeira exibição desta;
- linha 8: o [$scope.server] será utilizado para recuperar três das quatro informações do formulário V, sendo a quarta armazenada no [$scope.waiting.time] (linha 6);
- linha 9: [$scope.medecins] irá reunir as informações necessárias para a exibição da lista de médicos:
<!-- a lista de médicos -->
<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] irá controlar se o banner é ou não apresentado (atributo ng-show="medecins.show"). O atributo [medecins.model] é um dicionário vazio e assim permanecerá. Serve apenas 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á reunir as informações necessárias para a exibição da lista de erros:
<!-- lista de erros -->
<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. Está definido no serviço [config]. O atributo [errors.show] irá controlar se o banner é ou não apresentado (atributo ng-show="errors.show"). O atributo [errors.model] é um dicionário vazio e assim permanecerá. Serve 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 apresentar (linha 5).
- linha 16: a tarefa assíncrona. O controlador irá iniciar sucessivamente duas tarefas assíncronas. As referências a estas tarefas sucessivas serão colocadas na variável [task]. Isto permitirá cancelá-las (linha 55);
- linha 19: o método executado quando o utilizador clica no botão [Liste des médecins]:
<button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
- linhas 21-23: a interface visual é atualizada: a mensagem de espera é exibida, todo o resto é ocultado;
- linha 25: cria-se a tarefa assíncrona de espera. Receber-se-á um sinal (tarefa concluída) ao fim do tempo introduzido pelo utilizador no formulário;
- linha 26: recupera-se a promessa da tarefa assíncrona. É com ela que o programa que lança a tarefa trabalha. No entanto, é necessário ter a referência da própria tarefa para poder cancelá-la (linha 55);
- linhas 28-32: define-se o trabalho a realizar quando a espera terminar;
- linha 30: utiliza-se o método [dao.getData] para iniciar uma nova tarefa assíncrona. Passam-se-lhe as informações de que necessita:
- o URL raiz do serviço web [$scope.server.url], por exemplo, [http://localhost:8080];
- o nome de utilizador [$scope.server.login] para autenticar, por exemplo, [admin];
- a palavra-passe [$scope.server.password] para iniciar sessão, por exemplo, [admin];
- o URL que executa o serviço solicitado [config.urlSvrMedecins], neste caso [/getAllMedecins]. No total, o URL completo será [http://localhost:8080/getAllMedecins];
O método [dao.getData] devolve um resultado que pode assumir duas formas possíveis:
- (continuação)
- {err: 0, data: [med1, med2, ...]}, em que [medi] é um objeto que representa um médico (título, nome próprio, apelido),
- {err: n, messages: [msg1, msg2, ...]}, em que [msgi] é uma mensagem de erro e n é diferente de 0;
- linha 31: devolve-se a promessa da tarefa. Aqui há algo a compreender. Temos duas promessas:
- promise.then(): devolve uma primeira promessa [promise1];
- return task.promise: devolve uma segunda promessa [promise2];
- no final, promise=promise.then(...; return task.promise) é uma cadeia de duas promessas [promise2.promise1]. [promise1] só será avaliada quando a promessa [promise2] for obtida, ou seja, quando a tarefa [dao.getData] estiver concluída. A promessa [promise1] não depende de nenhuma tarefa assíncrona. Por conseguinte, será obtida imediatamente;
- linhas 34-50: da explicação anterior, conclui-se que estas linhas só serão executadas quando a tarefa [dao.getData] estiver concluída. O parâmetro [result] passado à função da linha 34 é construído pelo método [dao.getData] e transmitido ao código chamador pela operação [task.resolve(result)], em que [result] tem a seguinte forma:
- {err: 0, data: [med1, med2, ...]}, em que [medi] é um objeto que representa um médico (título, nome próprio, apelido),
- {err: n, messages: [msg1, msg2, ...]}, em que [msgi] é uma mensagem de erro e n é diferente de 0;
- linha 37: verifica-se o código de erro [result.err];
- linhas 38-42: se não houver erro (result.err == 0), então recupera-se a lista de médicos e apresenta-se;
- linhas 44-47: se, pelo contrário, houver um erro (result.err ≠ 0), então recupera-se a lista de mensagens de erro e exibe-se;
- linhas 53-56: a mensagem de espera com o seu botão de cancelamento permanece visível enquanto as duas operações assíncronas não estiverem concluídas. Vejamos o que acontece consoante o momento do cancelamento:
- é preciso, em primeiro lugar, compreender que as linhas 19-50 são executadas de uma só vez. Foi, então, iniciada uma única tarefa assíncrona, a da linha 25;
- após esta primeira execução, a vista V é atualizada e, por isso, a barra de espera e o seu botão de cancelamento ficam visíveis. Se o utilizador cancelar a espera antes de a tarefa da linha 25 estar concluída, o método da linha 53 é então executado e a tarefa é cancelada com falha (linha 55);
- linhas 56-59: a interface é atualizada: o formulário é novamente apresentado e todo o resto é ocultado,
- retorna-se então à vista V e o navegador processa o evento seguinte. Uma vez que a tarefa terminou, obtém-se a promessa dessa tarefa, o que cria 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. Obtém-se uma nova promessa, a que é sempre devolvida por [promise.then] e sempre obtida,
- tendo o evento sido processado, volta-se à vista V e o navegador vai processar o evento seguinte. Uma vez que a [promise] da linha 28 foi processada, a da linha 34 vai ser resolvida, o que vai provocar um novo evento. Este é então processado;
- 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 existe nenhuma função definida para o caso de falha, nenhum código é executado,
- chegando-se assim à linha 50. Já não há espera por tarefas e a nova vista V é apresentada;
- suponhamos agora que a anulação ocorra enquanto a segunda tarefa assíncrona [dao.getData] está a ser executada. O raciocínio anterior pode ser aplicado novamente. O fim da tarefa provocará a execução das linhas 34-50 com um fim de tarefa com falha. Em breve, vamos descobrir que o método [dao.getData] efetua uma chamada assíncrona HTTP para o serviço web. Esta chamada não será cancelada, mas o seu resultado não será utilizado.
É importante compreender esta interação constante entre a exibição da vista V e o tratamento dos eventos do navegador. Os eventos são desencadeados pelo utilizador (um clique) ou por operações do sistema, tais como a conclusão de uma operação assíncrona. O estado de repouso do navegador corresponde à exibição da vista V. É retirado desse estado de repouso por um evento que ocorre e que ele passa então a processar. Assim que o evento for processado, o navegador regressa ao seu estado de repouso. A vista V é então atualizada se o evento processado tiver alterado o seu modelo M. O navegador é retirado do seu estado de repouso pelo evento seguinte.
Tudo decorre num único thread. Dois eventos nunca são processados simultaneamente. A sua execução é sequencial. O navegador só passa para o evento seguinte quando o anterior lhe cede o controlo, geralmente porque foi totalmente processado.
Resta-nos um ponto a explicar. Para apresentar as 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:
// análise dos erros na resposta do servidor JSON
function getErrors(data) {
// dados {err:n, mensagens:[]}, err!=0
// erros
var errors = [];
// código de erro
var err = data.err;
switch (err) {
case 2 :
// não autorizado
errors.push('not_authorized');
break;
case 3 :
// proibido
errors.push('forbidden');
break;
case 4 :
// erro local
errors.push('not_http_error');
break;
case 6 :
// documento não encontrado
errors.push('not_found');
break;
default :
// outros casos
errors = data.messages;
break;
}
// se não houver mensagem, insere-se uma
if (! errors || errors.length == 0) {
errors=['error_unknown'];
}
// apresenta-se a lista de erros
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 criar uma tabela de mensagens de erro. Estas mensagens são internacionalizadas. Por esse motivo, não são as próprias mensagens que colocamos na tabela, mas sim as suas chaves de internacionalização, exceto na linha 27. Neste caso, utilizamos 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, por isso, não serão encontradas. Neste caso, o módulo [translate] apresenta a chave de mensagem que não encontrou, ou seja, neste caso, uma mensagem verdadeira. Este é o resultado pretendido;
- linhas 32-34: tratam o caso em que [data.messages], na linha 27, é igual a null. Isto acontece com o serviço web escrito. Este caso deveria ter sido evitado.
3.7.6.4. O serviço [dao]
![]() |
O serviço [dao] assegura 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) {
// registos
utils.debug("[dao] init");
// ----------------------------------métodos privados
// obter dados do serviço web
function getData(serverUrl, username, password, urlAction, info) {
// operação assíncrona
var task = $q.defer();
// pedido de URL HTTP
var url = serverUrl + urlAction;
// autenticação básica
var basic = "Basic " + $base64.encode(username + ":" + password);
// a resposta
var réponse;
// todas as solicitações HTTP devem ser autenticadas
var headers = $http.defaults.headers.common;
headers.Authorization = basic;
// efetua-se a solicitação 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);
// devolve-se a própria tarefa para que possa ser cancelada
return task;
// sucesso
function success(response) {
// response.data={status:0, data:[med1, med2, ...]} ou {status:x, data:[msg1, msg2, ...]
utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
// resposta
var payLoad = response.data;
réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
// a resposta é devolvida
task.resolve(réponse);
}
// falha
function failure(response) {
utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
// analisamos o estado
var status = response.status;
var error;
switch (status) {
case 401 :
// não autorizado
error = 2;
break;
case 403:
// proibido
error = 3;
break;
case 404:
// não encontrado
error = 6;
break;
case 0:
// erro local
error = 4;
break;
default:
// outra coisa
error = 5;
}
// a resposta está a ser enviada
task.resolve({err: error, messages: [response.statusText]});
}
}
// --------------------- instância do serviço [dao]
return {
getData: getData
}
}]);
- linhas 77-79: o serviço tem apenas um único campo: o método [getData], que permite obter informações do serviço web / JSON;
- linha 2: surge uma dependência [$http] que ainda não tínhamos encontrado. Trata-se de um serviço predefinido do Angular que permite a comunicação HTTP com uma entidade remota;
- linha 6: um registo para ver em que momento do ciclo de vida da aplicação o código é executado;
- linha 10: o método [getData] aceita cinco parâmetros:
- [serverUrl]: a raiz do serviço web (http://localhost:8080);
- [urlAction]: o URL do serviço específico solicitado (/getAllMedecins);
- [username]: o nome de utilizador;
- [password]: a sua palavra-passe;
- [info]: objeto que reúne informações complementares quando o URL do serviço específico solicitado é solicitado através de uma operação POST. No caso do URL (/getAllMedecins), este parâmetro não foi passado. Por conseguinte, é [undefined];
- linha 12: cria-se uma tarefa assíncrona;
- linha 14: o URL conclui o serviço solicitado (http://localhost:8080/getAllMedecins);
- linha 16: a autenticação é efetuada enviando o seguinte cabeçalho HTTP:
onde [code] é o código Base64 da cadeia [username:password];
A linha 16 constrói a parte [Basic code] do cabeçalho HTTP;
- linha 18: a resposta do serviço web;
- linha 20: os cabeçalhos HTTP enviados por predefinição pelo Angular numa solicitação HTTP são definidos no objeto [$http.defaults.headers.common]. O cabeçalho [Authorization:Basic code] não faz parte deste conjunto;
- linha 21: adiciona-se este cabeçalho aos cabeçalhos HTTP a enviar sistematicamente. À esquerda da atribuição, temos o cabeçalho [Authorization] a inicializar e, à direita, o valor do cabeçalho, neste caso o valor definido na linha 16. Assim, 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, neste caso, o parâmetro [info] tem o valor [undefined], é a linha 27 que é executada. O URL (http://localhost:8080/getAllMedecins) é solicitado com um GET. Para não esperar demasiado tempo, define-se um tempo de espera máximo (timeout) para obter a resposta do servidor. Por predefinição, este tempo é de um segundo;
- linha 29: definem-se os dois métodos a executar quando a promessa for obtida:
- [success]: definida na linha 34, é o método a executar quando a promessa é obtida após o sucesso da tarefa;
- [failure]: definido na linha 45, é o método a executar quando a promessa é obtida após uma falha da tarefa;
- os dois métodos (deveríamos dizer funções) estão definidos no interior da função [getData]. Isto é possível em JavaScript. As variáveis definidas em [getData] são conhecidas nas duas funções internas [success, failure];
- linha 31: devolve-se a tarefa criada na linha 12. É importante ter aqui em conta o código chamador:
promise = promise.then(function () {
// solicita-se a lista de médicos;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
Na linha 3 acima, recupera-se efetivamente uma tarefa.
- linha 34: a função [success] é executada posteriormente, quando a chamada HTTP termina com sucesso. Este conceito de sucesso está relacionado com a primeira linha de uma resposta HTTP. Esta tem o seguinte formato:
O código é um texto de três dígitos que indica se a chamada foi bem-sucedida ou não. Em termos gerais, pode dizer-se que os códigos 2xx e 3xx são códigos de sucesso, sendo os restantes códigos de falha. O texto é uma breve explicação. Aqui estão duas respostas possíveis, uma em caso de sucesso e outra em caso de falha:
- linha 36: exibe-se na consola a resposta do servidor. No erro [404 Not Found], obtém-se 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. Este terá uma das seguintes formas:
- {status: 0, data: [med1, med2, ...]}, em que [medi] é um objeto que representa um médico (título, nome próprio, apelido),
- {status: n, data: [msg1, msg2, ...]}, em que [msgi] é uma mensagem de erro e n é diferente de 0;
![]() |

- linha 39: constrói-se 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. Este tratou do 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 HTTP 200, indicando que a ordem HTTP foi processada na íntegra. É por isso que ambos os casos são tratados na mesma função [success];
- linha 41: a tarefa está concluída ([task.resolve]) e é devolvida uma das duas respostas:
- {err: 0, data: [med1, med2, ...]}, em que [medi] é um objeto que representa um médico (título, nome próprio, apelido),
- {err: n, messages: [msg1, msg2, ...]}, em que [msgi] é uma mensagem de erro e n é diferente de 0;
É necessário associar este código à forma como esta resposta é recuperada no código que chama o controlador:
// analisamos o resultado da chamada anterior
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
...
}
A resposta de [task.resolve(réponse)] encontra-se acima na variável [result].
- linha 45: a função [failure] quando a tarefa assíncrona termina com falha. Existem dois casos possíveis:
- o servidor sinaliza essa falha devolvendo um código que não é nem 2xx nem 3xx,
- o Angular cancela a chamada HTTP. Nesse caso, não há chamada. Ocorre uma exceção do Angular, mas não é devolvido nenhum código de erro HTTP pelo servidor. É o caso, por exemplo, se for fornecido um URL inválido que não pode ser chamado;
- linha 46: exibe-se a resposta na consola;
- linha 48: recorde-se que a resposta do servidor tem o seguinte formato:
{"data":"...","status":404,"config":{...},"statusText":"Not Found"}
Na linha 48, recuperamos o atributo [status] acima referido;
- linhas 50-70: a partir do código de erro HTTP, vamos gerar um novo código de erro para ocultar dos códigos chamadores a natureza HTTP do método [dao.getData]. É possível verificar que, no controlador que utiliza este método, nada sugere que exista uma chamada HTTP no método;
- linha 51: o erro [401] corresponde a uma falha na autenticação (por exemplo, palavra-passe incorreta),
- linha 55: o erro [403] corresponde a uma chamada não autorizada. O utilizador autenticou-se corretamente, mas não possui direitos suficientes para solicitar o URL que pediu. Isto acontecerá com o utilizador [user / user]. Este existe efetivamente na base de dados, mas não tem permissão para utilizar a aplicação. Apenas o utilizador [admin / admin] tem essa permissão;
- linha 59: o erro [404] corresponde a um URL não encontrado. O erro pode ter várias causas:
- o utilizador cometeu um erro ao introduzir o URL do serviço;
- o serviço web não foi iniciado;
- o serviço web não respondeu com rapidez suficiente (tempo limite de um segundo por predefinição);
- linha 63: o código de erro HTTP 0 não existe. Estamos perante o caso em que o Angular não efetuou a chamada HTTP solicitada porque o URL introduzido pelo utilizador é inválido e não pode ser chamado. Mais adiante, iremos deparar-nos com outros casos em que o Angular é levado a não executar a chamada HTTP solicitada;
- linha 72: a tarefa é concluída com sucesso (task.resolve), devolvendo uma resposta do tipo {err, messages}, em que o array [messages] é composto apenas pela mensagem [response.statusText]. Caso o Angular não tenha efetuado a chamada HTTP solicitada, teremos uma cadeia vazia;
Agora que temos uma visão global e detalhada da aplicação, podemos dar início aos testes.
3.7.6.5. Testes da aplicação - 1
Comecemos com entradas válidas:

![]() |
- em [1], introduzimos 0 para que não haja espera;
- em [2], aparece uma mensagem de erro, apesar de as entradas estarem corretas. Não apresentámos as diferentes mensagens de erro. A mensagem exibida 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 uma chamada HTTP. Nestes casos, é necessário consultar os registos da consola JavaScript. Há duas formas de o fazer:
- executar [F12] no navegador Chrome;
- utilizar a consola do WebStorm;
Na consola do Webstorm, encontramos várias mensagens, incluindo esta:
- linha 1: o Angular sinaliza um erro ao qual voltaremos mais tarde;
- linha 2: o registo do método [dao.getData]. Encontramos aqui alguns detalhes interessantes:
- [status] tem o valor 0, o que indica que não houve qualquer chamada ao HTTP. Consequentemente, o [statusText] está vazio,
- [url] é igual a [http://localhost:8080/getAllMedecins], o que está correto;
- o cabeçalho HTTP de autenticação [Authorization":"Basic YWRtaW46YWRtaW4=] também está correto;
Então, porque é que não funcionou? A frase-chave nos registos é [No 'Access-Control-Allow-Origin' header is present]. Para a compreender, é necessária uma explicação mais detalhada. Comecemos por rever a arquitetura geral da aplicação cliente/servidor:

- as páginas HTML / CSS / JS da aplicação Angular provêm do servidor [1];
- em [2], o serviço [dao] faz uma solicitação a outro servidor, o servidor [2]. Ora, isso é proibido pelo navegador que executa a aplicação Angular, porque constitui uma falha de segurança. A aplicação só pode consultar o servidor de onde provém, ou seja, o servidor [1];
Na verdade, não é correto dizer que o navegador proíbe a aplicação Angular de consultar o servidor [2]. Na realidade, a aplicação consulta-o para perguntar se este autoriza que um cliente que não provém do seu próprio domínio o consulte. A esta técnica de partilha chama-se CORS (Cross-Origin Resource Sharing). O servidor [2] dá o seu consentimento enviando cabeçalhos HTTP específicos. Foi precisamente porque, neste caso, o nosso servidor [2] não os enviou que o navegador recusou efetuar a chamada HTTP solicitada pela aplicação.
Vamos agora entrar em pormenores. Vamos analisar as trocas de dados de rede que ocorreram durante a chamada HTTP. Para tal, no navegador Chrome, pressionamos [F12] para aceder às ferramentas do programador e selecionamos o separador [Network] para ver as trocas de dados de rede:
![]() |
- em [1], selecionamos o separador [network];
- no [2], solicitamos a lista de médicos;
Obtenemos as seguintes informações no separador [network]:
![]() |
- em [1], as informações enviadas para o servidor;
- em [2], a resposta do servidor;
Pode-se ver em [1] que o navegador enviou uma solicitação HTTP [OPTIONS] sobre a URL solicitada. [OPTIONS] é um dos comandos HTTP possíveis, sendo os mais conhecidos o [GET] e o [POST]. Permite solicitar informações a um servidor, nomeadamente sobre as opções HTTP que este suporta, daí o nome do comando. O servidor responde em [2]. Para indicar que aceita pedidos de clientes que não estão no seu domínio, deve devolver um cabeçalho específico denominado [Access-Control-Allow-Origin]. E foi precisamente por não o ter devolvido que o Angular não executou a chamada HTTP solicitada e devolveu o erro:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Não existe nenhum cabeçalho «Access-Control-Allow-Origin» no recurso solicitado. Por conseguinte, a origem «http://localhost:63342» não tem acesso permitido.
Por isso, temos de alterar o nosso servidor para que envie o cabeçalho HTTP esperado.
3.7.6.6. Alteração do servidor web / JSON
Voltamos ao Eclipse. Para preservar o trabalho já realizado, duplicamos a versão atual do servidor web / JSON [rdvmedecins-webapi-v2] em [rdvmedecins-webapi-v3] [1]:
![]() |
Efetua-se uma primeira alteração no [ApplicationModel], que é um dos elementos de configuração do serviço web:
package rdvmedecins.web.models;
...
@Component
public class ApplicationModel implements IMetier {
// a camada [métier]
@Autowired
private IMetier métier;
// dados provenientes da camada [métier]
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// dados de configuração
private boolean CORSneeded = true;
...
public boolean isCORSneeded() {
return CORSneeded;
}
}
- linha 17: criamos uma variável booleana que indica se aceitamos ou não clientes externos ao domínio do servidor;
- linhas 21-23: o método de acesso 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;
// envio das opções ao cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// define-se o cabeçalho CORS
response.addHeader("Access-Control-Allow-Origin", "*");
}
}
// lista de médicos
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
}
- linhas 28-31: definem um controlador para o URL [/getAllMedecins] quando este é solicitado com o comando HTTP [OPTIONS];
- linha 29: o método [getAllMedecins] aceita como parâmetro o objeto [HttpServletResponse], que será enviado ao cliente que efetuou o pedido. Este objeto é injetado pelo Spring;
- linha 30: o tratamento do pedido é delegado ao método privado das linhas 19-25;
- linhas 15-16: o objeto [ApplicationModel] é injetado;
- linhas 20-23: se o servidor estiver configurado para aceitar clientes externos ao seu domínio, então envia-se o cabeçalho HTTP:
Access-Control-Allow-Origin: *
o que significa que o servidor aceita clientes de qualquer domínio (*).
Estamos agora prontos para novos testes. Lançamos a nova versão do serviço web e descobrimos que o problema persiste. Nada mudou. Se, na linha 30 acima, colocarmos uma saída de consola, esta nunca é exibida, o que demonstra que o método [getAllMedecins] da linha 29 nunca é chamado.
Após algumas pesquisas, descobrimos que o Spring MVC processa ele próprio os comandos HTTP e [OPTIONS] com um tratamento por predefinição. Assim, é sempre o Spring que responde e nunca o método [getAllMedecins] da 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 {
// configuração do DispatcherServlet para os cabeçalhos CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
- linha 8: a classe é uma classe de configuração do Spring. Declara os beans que serão colocados no contexto do Spring;
- linha 12: o bean [dispatcherServlet] serve para definir o servlet que gere as solicitações 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: cria-se uma instância do tipo [DispatcherServlet];
- linha 15: solicitamos que a servlet encaminhe os comandos HTTP e [OPTIONS] para a aplicação;
- linha 16: definimos a servlet com esta configuração;
Resta-nos 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. Testes da aplicação - 2
Lançamos a nova versão do serviço web / JSON e tentamos obter a lista de médicos com o nosso cliente Angular. Analisamos as trocas de dados de rede no separador [Network]:
![]() |
- em [1], é possível verificar que o cabeçalho HTTP [Access-Control-Allow-Origin: *] está agora presente na resposta do servidor. No entanto, continua a não funcionar. Analisamos, em [2], os registos da consola. Encontramos o seguinte registo:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. O campo de cabeçalho de pedido «Authorization» não é permitido pelo «Access-Control-Allow-Headers»
Vemos que o navegador aguarda um novo cabeçalho HTTP [Access-Control-Allow-Headers] que lhe indique que temos autorização para lhe enviar o cabeçalho de autenticação:
Isto pode ser um bom sinal. O Angular pode ter pretendido enviar o comando HTTP GET. Mas, como este vem acompanhado de um cabeçalho de autenticação, o navegador pergunta se o servidor o aceita.
Alteramos o nosso servidor web / JSON para enviar este cabeçalho. A classe [RdvMedecinsCorsController] passa a ter a seguinte forma:
// envio das opções para o cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// define-se o cabeçalho CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// autorização do cabeçalho [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 com o cliente Angular:
![]() |
Desta vez, está tudo bem. 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"}
Vemos 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 o URL e o [/getAllMedecins] foram processados sem erros;
- [data]: que contém a lista JSON de médicos;
Vamos agora apresentar outros casos interessantes:
Há um erro nos identificadores [login, password]:
![]() |
Iniciamos sessão com a identidade [user / user], que não tem acesso à aplicação (apenas [admin] tem acesso):
![]() |
Desta vez, o erro já não é [Erreur d'authentification], mas sim [Accès refusé].
3.7.7. Exemplo 7: lista de clientes
Retomamos a aplicação anterior para, desta vez, apresentar a lista de clientes numa lista suspensa do tipo [Bootstrap select] (ver parágrafo 3.6.6).
3.7.7.1. A vista V
A vista inicial será a seguinte:
![]() |
Para obter a vista V, duplicamos o código [app-16.html] em [app-17.html] e alteramo-lo da seguinte forma:
<div class="container" >
<h1>Rdvmedecins - v1</h1>
<!-- a mensagem de espera -->
<div class="alert alert-warning" ng-show="waiting.visible" >
...
</div>
<!-- o pedido -->
<div class="alert alert-info" ng-hide="waiting.visible" >
...
<button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
</div>
<!-- a lista de clientes -->
<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>
<!-- a lista de erros -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
- linhas 5-7: a barra de espera não se altera;
- linhas 10-13: o formulário não se altera, exceto o texto do botão (linha 12);
- linhas 28-30: a barra de erros não se altera;
- linhas 16-25: a exibição dos clientes é feita numa lista suspensa com estilo definido pelo componente [Bootstrap-selectpicker] (atributos data-style, class, linha 19);
- linha 20: utiliza-se a diretiva [ng-repeat] para gerar as diferentes opções da lista suspensa. Note-se que o texto de uma opção é do tipo [Mme Julienne Tatou] e que o valor da opção é do tipo [100], em que 100 é o identificador (id) do cliente apresentado;
- linha 34: o código JavaScript é transferido para um novo ficheiro [rdvmedecins-05];
3.7.7.2. O controlador C e o modelo M
O código JavaScript do ficheiro [rdvmedecins-05] é obtido por cópia do ficheiro [rdvmedecins-04]:

Praticamente nada muda, 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) {
// ------------------- inicialização do modelo
// modelo
$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;
// tarefa assíncrona
var task;
// execução da ação
$scope.execute = function () {
// atualização do UI
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// espera simulada
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// espera
promise = promise.then(function () {
// solicita-se a lista de clientes;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
return task.promise;
});
// análise do resultado da chamada anterior
promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// inserimos os dados recolhidos no modelo
$scope.clients.data = result.data;
// atualiza-se o UI
$scope.clients.show = true;
$scope.waiting.visible = false;
// aplica-se o estilo à lista suspensa
$('.selectpicker').selectpicker();
} else {
// ocorreram erros ao obter a lista de clientes
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// atualiza-se o UI
$scope.waiting.visible = false;
}
});
};
// A espera pelo cancelamento
function cancel() {
// a tarefa está a ser concluída
task.reject();
// está a ser atualizado o UI
$scope.waiting.visible = false;
$scope.clients.show = false;
$scope.errors.show = false;
}
}
])
;
- Muito poucas coisas mudam no controlador. Anteriormente, fornecia uma lista de médicos. Agora, fornece uma lista de clientes;
- linha 9: [$scope.clients] será o modelo do banner dos clientes na vista V;
- linha 30: é o URL [/getAllClients] que passa a ser utilizado;
- linhas 35-36: as duas formas de resposta devolvidas pelo método [dao.getData]. Agora temos clientes em vez de médicos;
- linha 44: uma instrução bastante rara num código Angular. Manipulamos diretamente o DOM (Document Object Model). Aqui, pretendemos aplicar o método [selectpicker] (que faz parte de [bootstrap-select.min.js]) aos elementos do DOM que têm a classe [selectpicker] [$('.selectpicker')]. Existe apenas um, a lista suspensa:
<select data-style="btn-primary" class="selectpicker" select-enable="">
....
</select>
No parágrafo 3.6.6, foi demonstrado que isto formatava a lista suspensa da seguinte forma:
![]() | ![]() |
Tal como foi feito para os médicos, temos de alterar também o serviço web.
3.7.7.3. Alteração do serviço web - 1
![]() |
A classe [RdvMedecinsController] é enriquecida com um novo método:
package rdvmedecins.web.controllers;
...
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// envio das opções ao cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// definir o cabeçalho CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// autoriza-se o cabeçalho [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
}
// lista de médicos
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
// lista de clientes
@RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
public void getAllClients(HttpServletResponse response) {
sendOptions(response);
}
}
- linhas 29-32: o método [getAllClients] irá gerir o pedido HTTP [OPTIONS] que lhe será enviado pelo navegador;
3.7.7.4. Testes da aplicação – 1
Estamos agora prontos para um teste. 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 solicitada. Nesse caso, é necessário procurar as causas nos registos da consola. Aí encontra-se a seguinte mensagem:
XMLHttpRequest cannot load http://localhost:8080/getAllClients. Não existe nenhum cabeçalho «Access-Control-Allow-Origin» no recurso solicitado. Por conseguinte, não é permitido o acesso à origem «http://localhost:63342».
Um problema que se pensava estar resolvido. Vamos, então, analisar as trocas de dados de rede que ocorreram:

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

Os cabeçalhos HTTP do CORS estão presentes. Vamos agora analisar as trocas HTTP durante o GET:

A solicitação HTTP parece estar correta. Observa-se, nomeadamente, o cabeçalho de autenticação.
Para além da mensagem de erro anterior, encontra-se nos registos da consola a seguinte mensagem:
[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. É possível observar duas coisas:
- [status=0]: isto significa que foi o Angular que cancelou a solicitação HTTP;
- [method=GET]: e foi a solicitação GET que foi cancelada;
Juntando isto à primeira mensagem, significa que, também para a solicitação GET, o Angular espera aqui os cabeçalhos CORS. 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 explicações.
Por isso, é necessário alterar novamente o serviço web.
3.7.7.5. Alteração do serviço web – 2
![]() |
Os métodos [GET] e [POST] são processados na classe [RdvMedecinsController]. Temos de a alterar para que estes métodos enviem os cabeçalhos CORS. Fazemo-lo da seguinte forma:
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// lista de clientes
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients(HttpServletResponse response) {
// cabeçalhos CORS
rdvMedecinsCorsController.getAllClients(response);
// estado da aplicação
if (messages != null) {
return new Reponse(-1, messages);
}
// lista de clientes
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, inserimo-lo aqui;
- linha 14: o método que processa o pedido [GET /getAllClients]. Fazemos duas alterações:
- linha 14: injetamos o objeto [HttpServletResponse] nos parâmetros do método,
- linha 16: utilizamos os métodos da classe [RdvMedecinsCorsController] para inserir neste objeto os cabeçalhos CORS;
3.7.7.6. Testes 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 efetivamente uma resposta, mas esta está vazia [2];
- em [3]: as comunicações de rede decorreram 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 corretamente a lista de clientes. Depois de verificar o código, começamos a suspeitar da instrução seguinte, que não dominamos muito bem:
// estilo da lista suspensa
$('.selectpicker').selectpicker();
Colocamos a linha 2 em comentário e tentamos novamente. Obtemos então a seguinte resposta:
![]() |
Conseguimos, portanto, localizar o problema. É a aplicação do método [selectpicker] à lista suspensa que está a causar o problema. Ao analisarmos o código-fonte da página com erro, encontramos o seguinte:
![]() |
- descobrimos que, em [1], a lista suspensa está bem presente com os seus elementos, mas não é apresentada em [style='display:none'];
- em [2], vemos o botão [bootstrap select] a ser apresentado. Os elementos da lista suspensa deveriam aparecer na lista <ul role='menu'>. Não estão lá e, por isso, 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 Internet em busca de uma solução, encontramos esta. Substituímos o código:
// estilo da lista suspensa
$('.selectpicker').selectpicker();
pelo seguinte:
// estilizar a lista suspensa
$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 executar uma função após um determinado intervalo de tempo. Aqui, a ausência de intervalo equivale a um intervalo nulo. As linhas anteriores colocam um evento na fila de espera de eventos do navegador. Quando o processamento do evento em curso (clique no botão [Liste des clients]) estiver concluído, a vista V será apresentada. Logo a seguir, o navegador consultará a sua lista de eventos. Devido ao seu intervalo nulo, 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 voltarmos a analisar o código-fonte da página apresentada, temos o seguinte:
![]() |
O botão [bootstrap-select], que anteriormente estava vazio, contém agora a lista de clientes.
3.7.7.7. Utilização de uma diretiva
Encontrámos no controlador C da vista V o seguinte código:
// estilizar a lista suspensa
$('.selectpicker').selectpicker();
Estamos a manipular um objeto do DOM. Muitos programadores Angular têm aversão à manipulação do DOM no código de um controlador. Para eles, isso deve ser feito numa diretiva. Uma diretiva Angular pode ser vista como uma extensão da linguagem HTML. Assim, é possível 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 a que já estamos habituados:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)
A diretiva pertence ao módulo [rvmedecins]. Trata-se de uma função que aceita dois parâmetros:
- (continuação)
- o primeiro é o nome da diretiva [selectEnable];
- o segundo é um array ['obj1','obj2',..., function(obj1, obj2,...)], em que os [obj] são os objetos a injetar 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 é, neste caso, uma função que aceita três parâmetros:
- scope: o modelo da vista na qual a diretiva é utilizada;
- element: o elemento da vista, objeto da diretiva;
- attrs: os atributos desse 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]. É importante ter em atenção a alteração na grafia entre o nome da diretiva e o atributo que lhe está associado. Passa-se de uma grafia [camelCase] para uma grafia [camel-case].
A diretiva [selectEnable] também poderia ser utilizada da seguinte forma:
Aqui, a diretiva [doSomething] é aplicada sob a forma de uma baliza 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 em que se encontra a <div>;
- element: é a própria <div>;
- attrs: é o conjunto de atributos da <div>. Estes podem ser utilizados para transmitir informação à diretiva. No exemplo acima, escrever-se-á attrs['selectEnable'] para obter a informação [data]. Note-se bem a alteração na escrita [selectEnable] para designar o 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: encontramos aqui o código que tínhamos colocado anteriormente no controlador. Este é executado quando se encontra a diretiva [select-enable] (na forma de elemento ou atributo) durante a exibição da vista V.
Para implementar esta diretiva, copiamos o ficheiro [app-17.html] para [app-17B.html] e alteramo-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=""];
Alteramos também 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 o ficheiro [selectEnable.js] de diretiva. Não se deve esquecer este último ponto. Se o ficheiro de diretiva estiver ausente, o atributo [select-enable=""] não será gerido, mas o Angular não indicará qualquer erro.
<script type="text/javascript" src="rdvmedecins-05B.js"></script>
<script type="text/javascript" src="selectEnable.js"></script>
No ficheiro JS [rdvmedecins-05B.js], eliminamos do controlador as seguintes linhas:
// estilizar a lista suspensa
$timeout(function(){
$('.selectpicker').selectpicker();
});
uma vez que esta operação é agora realizada pela diretiva.
3.7.7.8. Testes da aplicação – 3
Ao testar a nova aplicação [app-17B.html], obtém-se o seguinte resultado:
![]() |
- Na [1], obtém-se uma lista vazia.
Os registos da consola apresentam o seguinte:
- linha 1: inicialização do serviço [dao];
- linha 2: na exibição inicial da vista V, a diretiva [selectEnable] é executada;
- linha 3: esta linha aparece quando o utilizador clica no botão [Liste des clients]. Verifica-se então que a diretiva [selectEnable] não é executada uma segunda vez. No final, foi executada quando a lista de clientes estava vazia e, por isso, temos uma lista suspensa vazia;
Por outras palavras, a operação:
$('.selectpicker').selectpicker();
não ocorreu no momento certo. É possível tentar resolver o problema de várias formas. Após inúmeros testes infrutíferos, percebe-se que a operação acima só deve ocorrer uma vez e apenas quando a lista suspensa tiver sido preenchida. Para obter este resultado, reescreve-se a baliza <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>
Na linha 1, a baliza <select> só é gerada se [clients.data] existir. Não é esse o caso durante a exibição inicial da vista V. A baliza <select> não será, portanto, gerada e a diretiva [selectEnable] não será avaliada. Quando o utilizador clicar no botão [Liste des clients], [clients.data] terá um novo valor no modelo M. Como o modelo M mudou, a baliza <select> será reavaliada e, neste caso, gerada. A diretiva [selectEnable] será, portanto, também avaliada. Quando é avaliada, as linhas 2 a 4 da tag <select> ainda não foram avaliadas. Temos, portanto, uma lista de clientes vazia. 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, consequentemente, será apresentada uma lista suspensa vazia no ecrã. Por isso, deve escrever-se:
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 à diretiva [$timeout] da linha 5, a linha 6 só será executada após a avaliação completa da vista V, ou seja, num momento em que a baliza <select> tenha todos os seus elementos.
3.7.8. Exemplo 8: a 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], solicita-se a agenda da Sra. PELISSIER [2], no dia 25 de junho de 2014 [3];
Obtém-se o seguinte resultado [4]:
![]() |
Vamos analisar as duas vistas separadamente.
3.7.8.2. O formulário
Duplicamos o ficheiro [app-17.html] para [app-18.html] e, em seguida, alteramos o código da seguinte forma:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- a mensagem de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- o pedido -->
<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>
<!-- a lista de erros -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- a agenda -->
<div id="agenda" ng-show="agenda.show">
...
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
- linhas 5-7: a mensagem de espera não se altera;
- linhas 12-19: a lista de médicos do tipo [bootstrap select];
- linhas 20-26: o calendário de [ui-bootstrap] que já apresentámos. Note-se que o dia selecionado é inserido no modelo [calendar.jour] (atributo ng-model);
- linha 28: o botão que solicita a agenda;
- linhas 32-34: a lista de erros não se altera;
- linhas 37-39: a agenda que apresentaremos posteriormente;
- 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:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- inicialização do modelo
// modelo
$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()};
// estilizar a lista suspensa
$timeout(function () {
$('.selectpicker').selectpicker();
});
// configuração regional francesa para o calendário
angular.copy(config.locales['fr'], $locale);
...
}
])
;
- linha 7: define-se um tempo de espera de 3 segundos antes de efetuar a chamada HTTP;
- linha 8: definem-se de forma fixa os elementos necessários para a ligação HTTP;
- linhas 10-17: a lista de médicos é definida de forma fixa;
- linha 18: o modelo [agenda] configura a apresentação da agenda na vista;
- linha 19: o modelo [calendar] configura a exibição do calendário na vista. Define-se uma data mínima [minDate] para hoje e a data atual também para hoje;
- linhas 21-23: a lista suspensa é estilizada com o método visto anteriormente;
- linha 25: define-se a localização da aplicação para «fr». Por predefinição, está definida para «en»;
O método executado quando se solicita o calendário é o seguinte:
// execução da ação
$scope.execute = function () {
// informações do formulário
var idMedecin = $('.selectpicker').selectpicker('val');
// verificação
utils.debug("[homeCtrl] idMedecin", idMedecin);
utils.debug("[homeCtrl] jour", $scope.calendar.jour);
// formato do dia como aaaa-MM-dd
var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
// atualização da visualização
$scope.waiting.visible = true;
$scope.errors.show = false;
$scope.agenda.show = false;
...
};
- linha 4: recupera-se o atributo [value] do médico selecionado. Aqui, utiliza-se novamente o método [selectpicker], proveniente do ficheiro [bootstrap-select.min.js]. É importante lembrar-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 identificador [id] do médico.
- linha 11: coloca-se o dia escolhido pelo utilizador no formato [aaaa-mm-jj], que é o formato de data esperado pelo servidor web;
- linhas 13-15: quando o método [execute] estiver concluído, será exibida a barra de espera e todo o resto ficará oculto;
O código prossegue da seguinte forma:
// simulação de espera
var task = utils.waitForSomeTime($scope.waiting.time);
// solicitação da agenda do médico
var promise = task.promise.then(function () {
// o caminho do URL de serviço
var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
// solicitação da agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// retorna-se a confirmação de conclusão da tarefa
return task.promise;
});
// analisa-se o resultado da chamada ao serviço [dao]
promise.then(function (result) {
// fim da espera
$scope.waiting.visible = false;
// erro?
if (result.err == 0) {
// prepara-se o modelo da agenda
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// formatação da exibição dos horários
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
// a criar um evento para aplicar estilo à tabela após a exibição da vista
$timeout(function () {
$("#creneaux").footable();
});
} else {
// ocorreram erros ao obter a agenda
$scope.errors = {
title: config.getAgendaErrors,
messages: utils.getErrors(result),
show: true
};
}
- linha 2: a tarefa assíncrona de espera de 3 segundos;
- linhas 5-10: o código que será executado quando esta espera terminar;
- linha 6: constrói-se o URL consultado pelo [/getAgendaMedecinJour/1/2014-06-25];
- linha 8: o URL é consultado. É iniciada uma tarefa assíncrona;
- linha 10: é devolvida a promessa desta tarefa assíncrona;
- linhas 14-38: o código que será executado quando a chamada HTTP tiver devolvido a sua resposta;
- linha 13: [result] é a resposta enviada pelo método [dao.getData]. É importante recordar aqui o formato da resposta do servidor web:
![]() |
O parâmetro [result.data] da linha 19 é o atributo [data] [1] acima referido. Este atributo contém, por sua vez, o atributo [creneauxMedecin] [2] acima referido. Trata-se de uma tabela de intervalos, contendo, para cada um deles, as duas informações:
- [rv]: o formato JSON de um compromisso ou [null], caso não haja nenhum compromisso marcado nesse intervalo;
- [hDeb, mDeb, hFin, mFin]: as informações horárias do intervalo;
Voltemos ao código do controlador:
- linha 15: a espera terminou;
- linha 19: preenche-se o modelo [$scope.agenda], que controla a visualização da agenda;
- linha 20: a agenda é tornada visível;
- linhas 22-24: percorremos cada um dos elementos C da tabela [creneauxMedecin] de que acabámos de falar;
- linha 23: cada elemento C possui um atributo [creneau], que corresponde ao intervalo horário. Este é complementado com um atributo [text], que será a representação textual do intervalo horário na forma [10h20:10h40];
- linhas 26-28: tornamos «responsiva» a tabela HTML utilizada para apresentar os intervalos da agenda. Abordámos este conceito no parágrafo 3.6.7;
![]() |
- linha 27: para tornar a tabela «responsiva», é necessário aplicar-lhe o método [footable]. Encontramos aqui a mesma dificuldade que a encontrada para o componente [bootstrap-select]. Se escrevermos simplesmente a linha 17, verificamos que a tabela não é «responsiva». Este problema resolve-se da mesma forma com a função [$timeout] (linha 26);
- linhas 31-34: o caso em que a chamada à função HTTP falhou. Nesse caso, são exibidas as mensagens de erro;
3.7.8.4. Exibição da agenda
Voltamos agora ao código da agenda no ficheiro [app-18.html]. É o seguinte:
<!-- a agenda -->
<div id="agenda" ng-show="agenda.show">
<!-- caso do médico sem horários de consulta -->
<h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
translate="agenda_medecinsanscreneaux"></h4>
<!-- agenda do médico -->
<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] é a agenda, e que [agenda.data.creneauxMedecin] é um tabuleiro de objetos do tipo [creneauMedecin]. Cada elemento deste último tipo tem um atributo [creneauMedecin.creneau], que é um intervalo horário. Cada intervalo horário tem dois elementos que nos interessam:
- [creneauMedecin.creneau.rv], que é o eventual RV (rv!=null) associado ao intervalo horário;
- [creneauMedecin.creneau.text], que corresponde ao texto [début:fin] do intervalo horário;
- linha 4: apresenta uma mensagem especial se o médico não tiver intervalos horários disponíveis. É improvável, mas acontece que a nossa base de dados está incompleta e este caso existe. A geração ou não da mensagem HTML é controlada pela diretiva [ng-if];

A diretiva [ng-if] é diferente das diretivas [ng-show, ng-hide]. Estas últimas limitam-se a ocultar um campo presente no documento. Se for [ng-if='false'], então o campo é removido do documento. Utilizámo-la aqui a título de ilustração;
- linha 9: o atributo [id='creneaux'] é importante. É este que é utilizado na instrução:
$("#creneaux").footable();
- linhas 10-22: apresentam os cabeçalhos da tabela [1];
- linhas 23-45: apresentam o conteúdo da tabela [2];
![]() |
- linha 24: percorre-se a tabela [agenda.data.creneauxMedecin];
- linhas 26-29: escreve-se o texto [3]. Utiliza-se a diretiva [ng-class], que irá gerar o atributo [class] do elemento. Neste caso, se tivermos [creneauMedecin.rv==null], isso significa que o horário está disponível e atribuímos um fundo verde ao texto. Caso contrário, atribuímos um fundo vermelho;
- linha 32: escreve-se o nome do cliente para quem foi reservado o RV [4]. Se for [rv==null], estas informações não existem, mas o Angular lida corretamente com este caso e não declara qualquer erro;
- linhas 34-39: exibem um dos dois botões [Réserver] ou [Supprimer]. É a existência ou não de um compromisso que determina a escolha de um ou outro botão;
3.7.8.5. Alteração do servidor web
Tal como nos exemplos anteriores, o servidor web deve ser alterado para que o URL [/getAgendaMedecinJour] envie os cabeçalhos CORS:
![]() |
Na classe [RdvMedecinsCorsController], adiciona-se um novo método:
// agenda do médico
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getAgendaMedecinJour(HttpServletResponse response) {
sendOptions(response);
}
Este método irá enviar os cabeçalhos CORS para as consultas HTTP e [OPTIONS]. É necessário fazer o mesmo para as solicitações HTTP e [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) {
// cabeçalhos CORS
rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}
3.7.8.6. Utilização de diretivas
Tal como foi feito anteriormente, vamos transferir o tratamento do DOM para diretivas. Temos dois tratamentos do DOM:
- durante a exibição inicial da vista:
// estiliza-se a lista suspensa
$timeout(function () {
$('.selectpicker').selectpicker();
});
- ao apresentar a agenda:
// cria-se um evento para aplicar estilo à tabela após a exibição da vista
$timeout(function () {
$("#creneaux").footable();
});
No primeiro caso, vamos utilizar a diretiva [selectEnable] já apresentada. No segundo caso, criamos a diretiva [footable] no ficheiro JS [footable.js] a seguir:
angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive footable");
$timeout(function () {
$("#creneaux").footable();
})
}
}
}]);
Utilizamos, portanto, a mesma técnica que para a diretiva [selectEnable].
O código HTML [app-18.html] é duplicado em [app-18B.html]. Em seguida, é alterado 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: aplica-se a diretiva [selectEnable] (através do atributo [select-enable]) à baliza <select> dos 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: aplica-se a diretiva [footable] (através do atributo [footable]) à tabela HTML da agenda;
<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- diretrizes -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- linhas 3-4: são referenciados os ficheiros JS das duas diretivas;
- linha 1: o código JS de [app-18B.html] corresponde ao 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], com exceção de dois detalhes. As linhas que tratam do DOM desaparecem:
// estiliza-se a lista suspensa
$timeout(function () {
$('.selectpicker').selectpicker();
});
// Cria-se um evento para aplicar estilo à tabela após a exibição da vista
$timeout(function () {
$("#creneaux").footable();
});
Assim, a execução da aplicação [app-18B.html] produz os mesmos resultados que a 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. A vista V da aplicação
Apresentaremos o seguinte formulário:
![]() |
- em [1], será possível efetuar reservas. A reserva que for efetuada será para um cliente aleatório;
- em [2], será possível eliminar as reservas que tivermos efetuado;
Duplicamos o ficheiro [app-18.html] para [app-19.html] e, em seguida, alteramos o código da seguinte forma:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- a mensagem de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- a lista de erros -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- a agenda -->
<div id="agenda" ng-show="agenda.show">
..
<!-- agenda do médico -->
<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 espera é a da versão anterior;
- linhas 10-12: a mensagem de erro é a da versão anterior;
- linhas 15-36: o calendário é o da versão anterior, com duas pequenas diferenças:
- linha 26: o clique no botão [réserver] (atributo ng-click) é gerido pelo método [reserver] do modelo M da vista V. Passa-se para ele o número do intervalo horário de reserva;
- linha 26: o clique no botão [supprimer] é tratado pelo método [reserver] do modelo M da vista V. É-lhe passado o número do compromisso a eliminar;
- linha 39: o código JS que gere a aplicação encontra-se no ficheiro [rdvmedecins-07.js];
- linha 40: o código JS da diretiva [footable] aplicada na linha 20;
3.7.9.2. O controlador C
O código JS de [rdvmedecins-07.js] é obtido, em primeiro lugar, por cópia do ficheiro [rdvmedecins-06.js]. Em seguida, é modificado. Mantêm-se sempre os grandes blocos de código habituais. As alterações são feitas essencialmente no controlador:

Vamos descrever o controlador C da 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) {
// ------------------- inicialização do modelo
// modelo
$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"}
];
// formato de data francês
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 inicial
// a tarefa assíncrona global
var task;
// solicitação da agenda
getAgenda();
// ------------------------------------------------------------------ reserva
$scope.reserver = function (creneauId) {
....
};
// ------------------------------------------------------------ eliminação RV
$scope.supprimer = function (idRv) {
...
};
// obtenção da agenda
function getAgenda() {
...
}
// cancelamento em espera
function cancel() {
...
}
} ]);
- linha 6: configuração da mensagem de espera. Por predefinição, aguardar-se-á 3 segundos antes de efetuar uma chamada HTTP;
- linha 7: as informações necessárias para as chamadas HTTP;
- linha 8: configuração da mensagem de erro;
- linhas 9-17: os médicos predefinidos;
- linha 18: um médico particular. É para os horários deste médico que se farão as marcações;
- linhas 19-24: os clientes fixos;
- linha 26: pretende-se gerir datas francesas;
- linha 27: as consultas serão marcadas para a data de hoje;
- linha 28: o serviço web de marcação espera datas no formato «aaaa-mm-dd»;
- linha 29: a data de hoje no formato [jeudi 26 juin 2014];
- linha 30: configuração da agenda. O atributo [model] transporta os parâmetros da mensagem internacionalizada que vai ser apresentada:
agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
- linha 35: a variável global [task] representa, num determinado momento, a tarefa assíncrona em execução;
- linha 37: solicita-se a agenda inicial;
É tudo o que é feito durante o carregamento inicial da página. Se tudo correr bem, a vista apresenta a agenda do dia da Sra. PELISSIER.

3.7.9.4. Obtenção da agenda
A agenda é obtida através do seguinte método [getAgenda]:
// obtenção da agenda
function getAgenda() {
// o caminho do serviço URL
var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
// solicitação da agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// mensagem de espera
$scope.waiting.visible = true;
// análise do resultado da chamada ao serviço [dao]
task.promise.then(function (result) {
// fim da espera
$scope.waiting.visible = false;
// erro?
if (result.err == 0) {
// prepara-se o modelo da agenda
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// formatação da apresentação dos horários
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
} else {
// ocorreram erros ao obter a agenda
$scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
}
});
}
Este código é o que foi analisado na aplicação anterior. Existem duas alterações:
- não há espera simulada antes da chamada HTTP;
- linha 4: utiliza-se 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, uma vez que também é utilizado pelas funções [reserver] e [supprimer].
3.7.9.5. Reserva de um horário
![]() | ![]() |
Recorde-se que os clientes são selecionados aleatoriamente.
O código de reserva é o seguinte:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// está a ser criado um RV com um cliente aleatório no intervalo identificado por [id]
var idClient = clients[Math.floor(Math.random() * clients.length)].id;
utils.debug("réservation du créneau pour le client", idClient);
// espera simulada
$scope.waiting.visible = true;
var task = utils.waitForSomeTime($scope.waiting.time);
// adiciona-se o intervalo
var promise = task.promise.then(function () {
// o caminho do URL de serviço
var path = config.urlSvrResaAdd;
// os dados a transmitir ao serviço
var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
// inicia-se a tarefa assíncrona
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// retorna-se a promessa de conclusão da tarefa
return task.promise;
});
// análise do resultado da tarefa
promise = promise.then(function (result) {
if (result.err != 0) {
// ocorreram erros na validação do rv
$scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
} else {
// solicita-se a nova agenda
getAgenda();
}
});
};
- linha 1: recorde-se que o parâmetro da função [reserver] é o n.º do intervalo (atributo id);
- linha 4: é selecionado aleatoriamente um cliente da lista de clientes definida de forma fixa no código de inicialização. Retém-se o seu identificador [id];
- linhas 7-8: espera de 3 segundos;
- linhas 11-18: estas linhas só são executadas no final dos 3 segundos;
- linha 12: o URL do serviço de reservas [/ajouterRv]. Este URL é diferente dos que vimos 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: o URL não tem parâmetros e é solicitado com um POST;
- linha 2: os parâmetros enviados assumem a forma de um objeto JSON. Este será deserializado no parâmetro [post] (@RequestBody);
Vimos um exemplo deste POST (ponto 2.12.2):
![]() |
- em [0], o URL do serviço web;
- em [1], é utilizado o método POST;
- em [2], o texto JSON das informações transmitidas ao serviço web na forma {dia, idClient, idCreneau};
- em [3], o cliente indica ao serviço web que lhe está a enviar informações JSON;
Voltemos ao código JS da função [reserver]:
- linha 14: cria-se o valor a enviar na forma de um objeto JS. O Angular irá serializá-lo para JSON quando for enviado;
- linha 16: é efetuada a chamada à função HTTP. O valor a enviar é o último parâmetro da função [dao.getData]. Quando este parâmetro está presente, a função [dao.getData] gera um POST em vez de um GET (ver o código no parágrafo 3.7.6.4);
- linha 18: devolve-se a promessa da chamada HTTP;
- linhas 23-29: só são executadas 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 tiverem ocorrido erros, a mensagem de erro é apresentada;
- linha 28: se a reserva tiver sido bem-sucedida, volta a ser apresentada a nova agenda;
3.7.9.6. Alteração no servidor
![]() |
Na classe [RdvMedecinsCorsController], adicionamos o seguinte método:
// envio das opções ao cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// definir o cabeçalho CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// autorização do cabeçalho [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 das linhas 2 a 8 serão enviados para o URL [/ajouterRv] (linha 10) e para o método HTTP [OPTIONS] (linha 10).
A classe [RdvMedecinsController] é alterada da seguinte forma:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
// cabeçalhos CORS
rdvMedecinsCorsController.ajouterRv(response);
...
Para o método [POST] (linha 1) e os métodos URL e [/ajouterRv] (linha 1), o método que acabámos de adicionar em [RdvMedecinsCorsController] é chamado (linha 4), devolvendo assim os mesmos cabeçalhos HTTP que para os métodos HTTP e [OPTIONS].
3.7.9.7. Tests
Vamos fazer um primeiro teste em que reservamos um horário qualquer:
![]() |
Como sempre nestes casos, é necessário consultar 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 foi o Angular que cancelou o pedido. A causa do erro está nos registos:
XMLHttpRequest cannot load http://localhost:8080/ajouterRv. O campo de cabeçalho da solicitação Content-Type não é permitido pelo Access-Control-Allow-Headers.
Se analisarmos o tráfego de rede, observamos o seguinte:
![]() |
- em [1] e [2]: houve apenas um pedido HTTP, o pedido [OPTIONS];
- em [3], o cliente Angular solicita duas autorizações:
- a de enviar os cabeçalhos HTTP e [accept, authorization, content-type];
- a autorização para enviar um comando POST;
- em [4]: o servidor autoriza o cabeçalho [authorization]. Recorde-se que, do lado do servidor, somos nós próprios que enviamos esta autorização;
A novidade é, portanto, que numa operação POST, o cliente Angular solicita mais autorizações ao servidor. É, portanto, necessário alterar o servidor para que este as conceda:
![]() |
Na classe [RdvMedecinsCorsController], alteramos o método privado que gera os cabeçalhos HTTP enviados para os comandos OPTIONS, GET e POST:
// envio das opções para o cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// define-se o cabeçalho CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// autorização de determinados cabeçalhos
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// autoriza-se o POST
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
- linha 7: foi adicionada uma autorização para os cabeçalhos HTTP e [accept, content-type];
- linha 9: foi adicionada uma autorização para o método POST;
Repetimos o teste após reiniciar o servidor:
![]() |
Desta vez, conseguimos efetuar a reserva.
3.7.9.8. Eliminação de um compromisso
![]() | ![]() |
O código da função [supprimer] é o seguinte:
$scope.supprimer = function (idRv) {
utils.debug("suppression rv n°", idRv);
// espera simulada
$scope.waiting.visible = true;
task = utils.waitForSomeTime($scope.waiting.time);
// adiciona-se o intervalo
var promise = task.promise.then(function () {
// o caminho do serviço URL
var path = config.urlSvrResaRemove;
// os dados a transmitir ao serviço
var post = {idRv: idRv};
// inicia-se a tarefa assíncrona
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// retorna-se a promessa de conclusão da tarefa
return task.promise;
});
// análise do resultado da tarefa
promise = promise.then(function (result) {
if (result.err != 0) {
// ocorreram erros ao eliminar o rv
$scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
// atualiza-se o UI
$scope.waiting.visible = false;
} else {
// solicita-se a nova agenda
getAgenda();
}
});
};
- linha 1: é importante lembrar que o parâmetro da função é o número do compromisso a eliminar. Trata-se de um código muito semelhante ao da reserva. Apenas comentamos as diferenças;
- linha 9: o URL do serviço é aqui [/supprimerRV] e, também neste caso, é acedido através de um 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 é, mais uma vez, transmitido na forma JSON. No parágrafo 2.12.17, demonstrámos a natureza do POST criado manualmente:
![]() |
- em [1], o URL do serviço web;
- em [2], é utilizado o método POST;
- em [3], o texto JSON das informações transmitidas ao serviço web na forma {idRv};
- em [4], o cliente indica ao serviço web que lhe está a enviar informações JSON;
Voltemos ao código JS da função [supprimer]:
- linha 11: cria-se o objeto a enviar. O Angular irá serializá-lo automaticamente em JSON;
O resto do código é semelhante ao da reserva.
3.7.9.9. Alterações no servidor
Do lado do servidor, efetuamos as seguintes alterações:
![]() |
Na classe [RdvMedecinsCorsController], adicionamos o seguinte método:
// envio das opções ao cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// definimos o cabeçalho CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// autorizam-se determinados cabeçalhos
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// autorização do POST
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 a 16. Os cabeçalhos das linhas 2 a 10 serão enviados para o método URL [/supprimerRv] (linha 13) e para o método HTTP [OPTIONS] (linha 13).
A classe [RdvMedecinsController], por sua vez, é alterada da seguinte forma:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
// cabeçalhos CORS
rdvMedecinsCorsController.supprimerRv(response);
...
Para o método [POST] (linha 1) e os métodos URL e [/supprimerRv] (linha 1), o método que acabámos de adicionar em [RdvMedecinsCorsController] é chamado (linha 4), devolvendo assim os mesmos cabeçalhos HTTP que para os métodos HTTP e [OPTIONS].
3.7.10. Exemplo 10: criar e cancelar reservas - 2
Apresentamos agora a mesma aplicação que anteriormente, mas em vez de efetuar uma reserva para um cliente aleatório, este será selecionado numa 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 apresentamos apenas as principais diferenças.
Duplicamos o ficheiro [app-19.html] para [app-20.html] e, em seguida, criamos o código da lista suspensa de clientes [1]:
<!-- a lista de clientes -->
<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 com o componente [bootstrap-select];
- linha 1: a diretiva [selectEnable] é aplicada através do atributo [select-enable];
- linha 1: a baliza <select> só é gerada se [clients.data] existir (# null, undefined). Este ponto é importante e foi explicado no parágrafo 3.7.7.8;
Além disso, importamos novos ficheiros JS:
<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- diretrizes -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- linha 1: o ficheiro [rdvmedecins-08.js] é obtido por cópia do ficheiro [rdvmedecins-0.js];
- linhas 3-4: importam-se os ficheiros das duas diretivas;
3.7.10.2. O controlador C
O código do controlador C evolui da seguinte forma:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- inicialização do modelo
...
// os clientes
$scope.clients = {title: config.listClients, show: false, model: {}};
//------------------------------------------- inicialização da vista
// a tarefa assíncrona global
var task;
// solicitam-se os clientes e, em seguida, a agenda
getClients().then(function () {
getAgenda();
});
...
// execução da ação
function getClients() {
....
};
} ]);
- linha 8: o objeto [$scope.clients] configura a lista suspensa de clientes na vista V;
- linhas 14-16: de forma assíncrona, solicita-se primeiro a lista de clientes e, depois de obtida, solicita-se a agenda da Sra. PELISSIER para o dia de hoje. A sintaxe aqui utilizada só funciona porque a função [getClients] devolve uma promessa (promise);
O método [getClients] solicita a lista de clientes:
function getClients() {
// atualiza-se o UI
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// solicita-se a lista de clientes;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analisa-se o resultado da chamada anterior
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// inserimos os dados recolhidos no modelo
$scope.clients.data = result.data;
// atualiza-se o UI
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// ocorreram erros ao obter a lista de clientes
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// atualiza-se o UI
$scope.waiting.visible = false;
}
});
// cumpre-se a promessa
return promise;
};
Este é um código que já abordámos e comentámos. O elemento importante a destacar é a linha 31:
- linha 27: devolve-se a promessa da linha 10, ou seja, a última promessa obtida no código. Esta promessa só será obtida quando a chamada HTTP tiver devolvido a sua resposta;
O método [reserver] sofre uma ligeira alteração:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// cria-se um RV para o cliente selecionado
var idClient = $(".selectpicker").selectpicker('val');
...
});
- linha 4: já não se faz a reserva para um cliente aleatório, mas sim para o cliente selecionado na lista de clientes.
3.7.11. Exemplo 11: uma diretiva [selectEnable2]
Este exemplo volta a abordar 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 da vista [app-21.html] é o seguinte:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- a mensagem de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- a lista de erros -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- a lista de clientes -->
<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>
<!-- a lista de médicos -->
<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>
<!-- instruções -->
<script type="text/javascript" src="selectEnable2.js"></script>
- linhas 19-23: a lista suspensa de clientes;
- linha 19: aplica-se a diretiva [selectEnable2] (atributo [select-enable2]);
- 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: aplica-se a diretiva [selectEnable2] (atributo [select-enable2]);
- linha 33: apenas se [medecins.data] não estiver vazio;
- linha 33: a lista suspensa é identificada pelo atributo [id="selectpickerMedecins"];
- linha 43: importa-se um novo ficheiro JS [rdvmedecins-09.js];
- linha 45: importa-se o ficheiro JS da nova diretiva;
3.7.11.3. A diretiva [selectEnable2]
O código da 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: exibe-se o valor do parâmetro [attrs] para compreender o funcionamento do código. Verifica-se que attrs['id']='selectpickerClients' para a lista de clientes;
- linha 6: para localizar no DOM um elemento do [id='x'], escreve-se [$('#x')]. Portanto, deve escrever-se [$('#selectpickerClients')] para localizar a lista de clientes. Isto é obtido com 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. O controlador C
O controlador C encontra-se no ficheiro JS [rdvmedecins-09.js] e tem a seguinte estrutura:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- inicialização do modelo
// a mensagem de espera
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
// informações de início de sessão
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
// erros
$scope.errors = {show: false, model: {}};
// os médicos
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
// os clientes
$scope.clients = {title: config.listClients, show: false, model: {}};
// a tarefa assíncrona global
var task;
// ---------------------------------------------------- inicialização da vista
// atualiza-se o UI
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.medecins.show = false;
$scope.errors.show = false;
// solicitam-se os clientes e, em seguida, os médicos
getClients().then(function () {
getMedecins();
});
// lista de clientes
function getClients() {
...
}
// lista de médicos
function getMedecins() {
...
}
// cancelamento pendente
function cancel() {
...
}
} ]);
- linhas 26-28: primeiro são solicitados os clientes e, em seguida, os médicos;
3.7.11.5. Os testes
Teste esta nova versão.
3.7.12. Exemplo 12: uma diretiva [list]
Retomamos o mesmo exemplo anterior, mas pretendemos simplificar o código HTML utilizando uma diretiva. De facto, temos atualmente o seguinte código HTML:
<!-- lista de clientes -->
<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>
<!-- lista de médicos -->
<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 a 26 são idênticas às linhas 1 a 13. Aplicam-se a médicos em vez de clientes. Gostaríamos de poder escrever o seguinte:
<!-- lista de clientes -->
<list model="clients" ng-if="clients.show"></list>
<!-- lista de médicos -->
<list model="medecins" ng-if="medecins.show"></list>
Este código envolve uma nova diretiva, [list], que vamos criar agora.
3.7.12.1. A diretiva [list]
A diretiva [list] está incluída no ficheiro JS [list.js]. O seu código é o seguinte:
angular.module("rdvmedecins")
.directive("list", ['utils', '$timeout', function (utils, $timeout) {
// instância da diretiva devolvida
return {
// elemento HTML
restrict: "E",
// URL do fragmento
templateUrl: "list.html",
// âmbito único para cada instância da diretiva
scope: true,
// função de ligação ao documento
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 denominada «list»;
- linha 6: o atributo [restrict] define as formas de utilizar a diretiva. [restrict: "E"] significa que a diretiva [list] pode ser utilizada como elemento HTML <list ...>...</list>. [restrict: "A"] significa que a diretiva [list] pode ser utilizada como atributo, por exemplo <div ... list='...'>. [restrict: "AE"] significa que a diretiva [list] pode ser utilizada como atributo e como elemento;
- linha 8: o atributo [templateUrl] indica o nome do fragmento HTML a utilizar quando a baliza for encontrada. Este fragmento constituirá o corpo da baliza;
- linha 10: o atributo [scope] define o âmbito do modelo da diretiva. [scope: true] significa que dois elementos do tipo <list> terão, cada um, o seu próprio modelo. Por predefinição (âmbito 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 ter em conta a utilização que será feita da diretiva:
<!-- a lista de clientes -->
<list model="clients" ng-if="clients.show"></list>
<!-- a lista de médicos -->
<list model="medecins" ng-if="medecins.show"></list>
A diretiva [list] é utilizada como elemento <list> HTML. Este elemento tem dois atributos:
- [model]: cujo valor será o elemento do modelo M da vista V na qual se encontra a diretiva [list]. Este elemento irá alimentar o modelo da diretiva;
- [ng-if]: que fará com que o código HTML da diretiva não seja gerado se não houver nada para visualizar;
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 associar este código JS ao código HTML que utiliza a diretiva:
<list model="clients" ng-if="clients.show"></list>
- linha 3: attrs['model'] tem aqui o valor «clientes»;
- linha 3: scope[attrs['model']] tem como valor scope['clients'] e representa, assim, [$scope.clients], ou seja, o campo [clients] do modelo da vista. Este campo terá como valor {id: '...', data: [client1, client2, ...], show: ..., title: '...'};
- linha 3: adiciona-se um campo [model] ao modelo da diretiva. Esta herdou o modelo da vista na qual se encontra. Por isso, é necessário evitar colisões com um eventual campo [model] que a vista também possa ter. Neste caso, não haverá colisão;
- linha 4: exibe-se [scope.model] para compreender melhor o código;
- linhas 5-7: encontramos um código já visto anteriormente. A diferença é que o id do componente era anteriormente incluído num atributo attrs['id']. Aqui, será incluído em [scope.model.id];
Agora, vamos analisar o código HTML gerado pela diretiva. Devido ao atributo [templateUrl: "list.html"] da diretiva, é necessário procurá-lo no ficheiro [list.html]:
<!-- uma lista de clientes ou de médicos -->
<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] (o 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 visualização 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 à baliza <select>. Este id é utilizado pelo código JS da diretiva;
- linha 6: utilização de [model.data] para gerar o <select> apenas se houver dados a apresentar;
- linhas 7-9: utilização de [model.data] para gerar os elementos da lista suspensa;
3.7.12.2. O código HTML
O código HTML da aplicação [app-22.html] é o seguinte:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- a mensagem de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- a lista de erros -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- a lista de clientes -->
<list model="clients" ng-if="clients.show"></list>
<!-- a lista de médicos -->
<list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- instruções -->
<script type="text/javascript" src="list.js"></script>
- linha 22: não se esqueça de incluir o código JS da diretiva;
3.7.12.3. O controlador C
O controlador C sofreu poucas alterações:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- inicialização do modelo
...
// os médicos
$scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
// os clientes
$scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
- nas linhas 7 e 9, adicionamos o atributo [id] aos modelos dos médicos e dos clientes;
3.7.12.4. Os testes
Os testes apresentam os mesmos resultados que no exemplo anterior.
3.7.13. Exemplo 13: atualização do modelo de uma diretiva
Continuamos a analisar as diretivas e mantemos o exemplo da lista suspensa. Pretendemos aqui estudar o comportamento da diretiva [list] quando o conteúdo da lista suspensa muda.
3.7.13.1. As vistas V
As diferentes vistas são as seguintes:
![]() |
- em [1], solicita-se pela primeira vez a lista de clientes;
![]() |
- em [2], solicita-se pela segunda vez a lista de clientes. Esta segunda lista é então acumulada com a primeira, [3]. É a atualização do componente [Bootstrap select] que pretendemos analisar neste exemplo.
3.7.13.2. A página HTML
A página HTML [app-23.html] é obtida por cópia da página [app-22.html] e, em seguida, modificada da seguinte forma:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- a mensagem de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- a lista de erros -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- o botão -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- a lista de clientes -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- diretivas -->
<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] na [list2.js] é a seguinte:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instância da diretiva devolvida
return {
// elemento HTML
restrict: "E",
// URL do fragmento
templateUrl: "list.html",
// âmbito único para cada instância da diretiva
scope: true,
// função de ligação ao documento
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')], solicita-se ao componente [Bootstrap-select] que se atualize. A ideia por trás disto é que, sempre que o utilizador solicitar uma nova lista de clientes, a lista suspensa será atualizada. Isto 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], obtido por cópia do ficheiro [rdvmedecins-10.js]:
// os clientes
$scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
// lista de clientes
$scope.getClients = function getClients() {
// atualiza-se o UI
$scope.waiting.visible = true;
$scope.errors.show = false;
// solicita-se a lista de clientes;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analisa-se o resultado da chamada anterior
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// colocamos os dados obtidos num novo modelo para forçar a atualização da visualização
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
// atualiza-se o UI
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// ocorreram erros ao obter a lista de clientes
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// atualiza-se o UI
$scope.waiting.visible = false;
}
});
}
- linha 1: para permitir a concatenação de tabelas no [clients.data], este objeto é inicializado com uma tabela vazia;
- linha 18: concatenamos a nova lista de clientes com as que já se encontram na tabela [clients.data];
Antes, 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]:
<!-- a lista de clientes -->
<list2 model="clients" ng-if="clients.show"></list2>
O modelo utilizado pela diretiva [list2] é o [clients]. Esta só será reavaliada na vista V se o [clients] se alterar no modelo M da vista. A primeira ideia que surge para a modificação é escrever:
para ter em conta o facto de que a nova lista de clientes deve ser adicionada às anteriores. Ao fazê-lo, altera-se o [clients.data], mas não o [clients]. Não conheço os meandros do JavaScript, mas não seria de estranhar que [clients] fosse um ponteiro, tal como [clients.data]. O ponteiro [clients] não se altera quando se altera o ponteiro [clients.data]. A diretiva [list2] não é, portanto, reavaliada. É efetivamente isso que se verifica 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};
Assegura-se que [$scope.clients] recebe efetivamente um novo valor. O ponteiro [$scope.clients] aponta para um novo objeto. A diretiva [list2] deveria, então, ser reavaliada. No entanto, não obtemos o resultado pretendido. Analisemos as capturas de ecrã quando solicitamos duas vezes a lista de clientes:
![]() |
- em [1], temos apenas quatro elementos em vez de oito;
- em [2], esses quatro elementos encontram-se num [select], mas este está oculto (style='display: none');
![]() |
- em [3], encontramos os quatro clientes noutra arquitetura, HTML, e é esta que o utilizador vê quando clica na lista suspensa;
Por fim, os registos da consola indicam o seguinte:
- linha 1: o serviço [dao] é instanciado;
- linha 2: o serviço [dao] obtém uma primeira lista de clientes;
- linha 3: a diretiva [list2] é executada;
- linha 4: o serviço [dao] obtém uma segunda lista de clientes;
A exibição da linha 2 resulta do seguinte código na diretiva:
link: function (scope, element, attrs) {
utils.debug('directive list2');
...
}
Analisemos o ciclo de vida da diretiva [list2]:
- entre as linhas 1 e 2, não está ativada, embora a vista tenha sido apresentada 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 a obtenção da primeira lista de médicos, [clients.show] passa para «true» e a diretiva é ativada;
- após a obtenção da segunda lista de clientes, verifica-se que o código da diretiva [list2] não é chamado. É por isso que não se vê a segunda lista;
Para resolver este problema, alteramos a diretiva [list2] da seguinte forma:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instância da diretiva devolvida
return {
// elemento HTML
restrict: "E",
// URL do fragmento
templateUrl: "list.html",
// âmbito único para cada instância da diretiva
scope: true,
// função de ligação ao documento
link: function (scope, element, attrs) {
// sempre que o atributo attrs["model"] se alterar, o modelo da diretiva deve também ser alterado
scope.$watch(attrs["model"], function (newValue) {
utils.debug("directive list2 newValue", newValue);
// atualiza-se o modelo da diretiva
scope.model = newValue;
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
});
}
}
}]);
- linha 14: a função [scope.$watch] permite observar um valor do modelo. A sua sintaxe é [scope.$watch('var'), f], em que [var] é o identificador de uma variável do modelo e f é a função a executar quando essa variável muda de valor. Neste caso, queremos monitorizar a variável [clients]. Por isso, devemos 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 executar quando a variável observada alterar o seu valor. O parâmetro [newValue] é o novo valor da variável, ou seja, para nós, o novo valor da variável [clients] do modelo;
- linha 17: este novo valor é atribuído ao campo [model] do modelo da diretiva;
Após esta alteração, os registos mudam:
![]() |
Acima, vemos que, após obter a segunda lista de clientes, a diretiva [list2] é efetivamente executada novamente, o que é 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>
<!-- a mensagem de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- a lista de erros -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- o botão -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- a lista de clientes -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
- linhas 5-7: a mensagem de espera;
- linhas 10-12: a mensagem de erro;
Decidimos colocar os códigos HTML destas duas mensagens em 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>
<!-- a mensagem de espera -->
<waiting model="waiting"></waiting>
<!-- a lista de erros -->
<errors model="errors"></errors>
<!-- o botão -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- a lista de clientes -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- diretivas -->
<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 das três diretivas;
3.7.14.2. A diretiva [waiting]
O código JS da diretiva [waiting] encontra-se no seguinte ficheiro [waiting.js]:
angular.module("rdvmedecins")
.directive("waiting", ['utils', function (utils) {
// instância da diretiva devolvida
return {
// elemento HTML
restrict: "E",
// URL do fragmento
templateUrl: "waiting.html",
// âmbito único para cada instância da diretiva
scope: true,
// função de ligação ao documento
link: function (scope, element, attrs) {
// sempre que o atributo ["model"] mudar, o modelo da página deve mudar também
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[waiting] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Este código segue a mesma lógica que o da diretiva [list2] já analisada.
Na linha 8, é feita 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] deste código HTML será definido da seguinte forma:
// a mensagem de espera
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time: 3000};
3.7.14.3. A diretiva [errors]
O código JS da diretiva [errors] encontra-se no seguinte ficheiro [errors.js]:
angular.module("rdvmedecins")
.directive("errors", ['utils', function (utils) {
// instância da diretiva devolvida
return {
// elemento HTML
restrict: "E",
// URL do fragmento
templateUrl: "errors.html",
// âmbito único para cada instância da diretiva
scope: true,
// função de ligação ao documento
link: function (scope, element, attrs) {
// sempre que o atributo ["model"] mudar, o modelo da página deve mudar também
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[errors] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Este código segue a mesma lógica que o da diretiva [list2] já analisada.
Na linha 8, é feita 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] deste código HTML será definido da seguinte forma:
// ocorreram erros ao obter a lista de clientes
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};
3.7.15. Exemplo 15: navegação
Até agora, utilizámos aplicações de página única. Neste exemplo, abordamos as 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], a URL da vista n.º 1;
- em [2], o seu conteúdo;
- em [3], passa-se para a página 2;
- em [4], a vista n.º 2;
- em [5], passa-se para a página 3;
![]() |
- em [6], a vista n.º 3;
- em [7], passa-se para a página 1;
- em [8], voltamos à vista n.º 1;
3.7.15.2. Organização do código
Começamos 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 contentor das vistas
As vistas da 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">
<!-- a barra de navegação -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- a vista atual -->
<ng-view></ng-view>
</div>
...
<!-- o módulo -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- os controladores -->
<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 externo HTML, neste caso uma barra de navegação;
- linha 12: as diferentes vistas apresentadas pelo contentor são exibidas no interior da diretiva [ng-view]. No final, temos um contentor que apresenta:
- sempre a mesma barra de navegação (linha 9);
- diferentes vistas na linha 12;
- linhas 16-22: importam-se 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:
// --------------------- módulo Angular
angular.module("rdvmedecins", [ 'ngRoute' ]);
angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ encaminhamento
$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-se o módulo [rdvmedecins]. Este tem uma dependência do módulo [ngRoute] fornecido pela biblioteca [angular-route.min.js]. É este módulo que permite o encaminhamento definido nas linhas 6 a 24;
- linha 4: define a função [config] do módulo [rdvmedecins]. Recorde-se que esta função é executada antes de qualquer instanciação do serviço. Trata-se de uma função de configuração do módulo. Aqui, é o seu encaminhamento que está a ser configurado. Isto é feito através do 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 à aplicação. O URL é, na verdade, o [/rdvmedecins-angular-v1/app-21.html#/page1]. Vê-se que é sempre o URL do contentor [/rdvmedecins-angular-v1/app-21.html] que é utilizado, mas com uma informação adicional atrás de um carácter #. É esta informação adicional que o encaminhamento do Angular gere;
- linha 8: indica o fragmento HTML a inserir na diretiva [ng-view] do contentor:
- linha 9: indica o nome do controlador deste fragmento;
- linhas 11-15: definem a vista a apresentar quando o utilizador solicita o URL [/page2];
- linhas 16-20: definem a vista a apresentar quando o utilizador solicita o URL [/page3];
- linhas 21-24: definem o encaminhamento a efetuar quando o URL solicitado não for uma das três anteriores (caso contrário, linha 21);
- linha 23: redirecionamento para o URL [/page1], ou seja, para a vista definida nas linhas 6-10;
3.7.15.5. O controlador do contentor de vistas
Vimos que o contentor de vistas declarava um controlador:
<div class="container" ng-controller="mainCtrl">
O controlador [mainCtrl] está definido no ficheiro [mainController.js]:
// controlador
angular.module("rdvmedecins")
.controller('mainCtrl', ['$scope', '$location',
function ($scope, $location) {
// modelos das páginas
$scope.page1 = {};
$scope.page2 = {};
$scope.page3 = {};
// modelo global
var main = $scope.main = {};
main.text = "[Modèle global]";
// métodos expostos à vista
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 encaminhamento [ngRoute]. Este objeto permite mudar de vista (linhas 16, 19, 22);
Voltemos ao código do contentor:
<div class="container" ng-controller="mainCtrl">
<!-- a barra de navegação -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- a vista atual -->
<ng-view></ng-view>
</div>
- o controlador [mainCtrl] constrói o modelo da á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 constrói o modelo da área exibida na linha 6. Temos, então, nesta área dois modelos:
- o modelo construído pelo controlador [mainCtrl];
- o modelo construído pelo controlador [page1Ctrl];
Existe uma herança de modelos. Na vista apresentada na linha 6, os modelos dos controladores [mainCtrl] e [pagexCtrl] estão ambos visíveis. Se duas variáveis destes modelos tiverem o mesmo nome, uma irá ocultar a outra. Para evitar esta colisão de nomes, criamos quatro modelos com quatro nomes diferentes:
recipiente | mainCtrl | mão | 11 |
página 1 | page1Ctrl | página 1 | 7 |
página 2 | page2Ctrl | página 2 | 8 |
página 3 | page3Ctrl | página 3 | 9 |
- linha 12: define um elemento [text] no modelo [main];
As linhas 7 a 11 têm uma consequência muito específica: definem o [$scope] do controlador [mainCtrl] e, neste, criam quatro variáveis [main, page1, page2, page3]. Estas quatro variáveis serão utilizadas como modelos respetivos do contentor e das três vistas que este irá conter sucessivamente.
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">
<!-- a barra de navegação -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- a vista atual -->
<ng-view></ng-view>
</div>
A barra de navegação é definida na linha 3. Isto significa que apenas 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: um clique na ligação [Page1] irá iniciar a execução do método [$scope.main.showPage1]. Este está definido no controlador [mainCtrl] da seguinte forma:
// modelo global
var main = $scope.main = {};
main.text = "[Modèle global]";
// métodos expostos na vista
main.showPage1 = function () {
$location.path("/page1");
};
- linha 6: no código anterior, verifica-se que o método [main.showPage1] é, na realidade, o método [$scope.main.showPage1]. É, portanto, este último que será executado;
- linha 7: alteramos o URL da aplicação, que passa a ser [/page1]. Voltemos ao encaminhamento que foi definido no módulo principal:
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
vemos que o fragmento [views/page1.html] vai 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>
Recorde-se 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] do fragmento [views/page1.html] define um modelo [page1]. É este que é utilizado na linha 5.
O código do controlador [page1Ctrl] é o seguinte:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope',
function ($scope) {
// modelo da página 1
var page1=$scope.page1;
page1.text="[Modèle local dans page 1]";
}]);
- linha 2: o [$scope] inserido aqui não está vazio. Uma vez que o controlador [page1Ctrl] controla uma zona inserida num contentor controlado por [mainCtrl], o [$scope] da 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 do [main, page1, page2, page3]. Isto significa que temos acesso aos modelos de todas as vistas. Não é necessariamente desejável, mas é o que acontece neste caso. Na versão final do cliente Angular, utilizaremos esta particularidade para armazenar no modelo [main] as informações que devem ser partilhadas entre as vistas. Teremos aqui um conceito análogo ao conceito de «sessão» do lado do servidor;
- linha 6: recuperamos no [$scope] o modelo [page1] da página 1 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 capturas de ecrã na página 240).
3.7.15.8. Controlo da navegação
Pretendemos agora controlar a navegação da seguinte forma: [page1 --> page2 --> page3 --> page1]. Assim, se o utilizador estiver na página 1 ([/page1]) e digitar no seu navegador URL ou [/page3], essa navegação não deve ser aceite e deve permanecer-se na página 1.
Para obter este resultado, alteramos os controladores das páginas da seguinte forma:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope', '$location',
function ($scope, $location) {
// navegação autorizada?
var main = $scope.main;
if (main.lastUrl && main.lastUrl != '/page3') {
// regressa-se à última URL
$location.path(main.lastUrl);
return;
}
// memoriza-se o URL da página
main.lastUrl = '/page1';
// modelo da página
var page1 = $scope.page1;
page1.text = "[Modèle local dans page 1]";
}]);
- linha 12: quando uma página for apresentada, guardaremos o seu URL no modelo [main.lastUrl]. Utilizamos aqui o conceito de que falámos anteriormente: utilizar o modelo [main] para armazenar informações partilhadas por todas as vistas. Neste caso, trata-se do último URL consultado;
- o código das linhas 4 a 12 é duplicado e adaptado às três vistas. Aqui, estamos na vista [/page1];
- linha 5: recupera-se 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: regressa-se então à última URL visitada;
Vamos fazer um teste:
![]() |
- 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
Analisámos todos os casos de utilização que iremos encontrar na versão final do cliente Angular. Quando o apresentarmos, comentaremos mais as funcionalidades da aplicação do que os seus detalhes de implementação. Quanto a estes últimos, limitar-nos-emos a fazer referência ao 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 a seguinte estrutura:
![]() |
![]() |
- em [1], o projeto completo. [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 diferentes vistas que são inseridas na página principal [app.html];
3.8.2. As dependências do projeto
As dependências do projeto são as seguintes:
![]() |
A função destes diferentes elementos foi explicada no parágrafo 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é">
<!-- o 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>
<!-- controlador [appCtrl], modelo [app] -->
<body ng-controller="appCtrl">
<div class="container">
...
</div>
<!-- Núcleo do Bootstrap 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>
<!-- AngularJS -->
<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>
<!-- módulos -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- serviços -->
<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>
<!-- diretivas -->
<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>
<!-- controladores -->
<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-se 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">
<!-- barras de navegação -->
<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>
<!-- o jumbotron -->
<ng-include src="'views/jumbotron.html'"></ng-include>
<!-- o título da página -->
<div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
translate-values="{{app.titre.model}}"></div>
<!-- erros da página -->
<errors model="app.errors" ng-show="app.errors.show"></errors>
<!-- a mensagem de espera -->
<waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
<!-- a vista atual -->
<ng-view></ng-view>
<!-- depuração -->
<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 das 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 espera:

- linha 17: uma informação de depuração:

Todos os elementos anteriores 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">
...
<!-- a vista atual -->
<ng-view></ng-view>
...
</div>
A linha 4 recebe as diferentes vistas da aplicação. Estas são definidas no módulo [main.js]:

O papel da configuração das diferentes rotas foi explicado no parágrafo 3.7.15.4, página 242.
A vista [login.html] está vazia, ou seja, não adiciona nenhum elemento aos que já se encontram 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 no parágrafo 1.3.3, página 7. Para facilitar a leitura deste novo capítulo, voltamos a apresentá-las aqui. A primeira vista é a seguinte:
![]() |
- em [6], a página inicial da aplicação. Trata-se de uma aplicação para marcação de consultas médicas;
- em [7], uma caixa de seleção que permite ativar ou desativar o modo [debug]. Este último caracteriza-se pela presença do quadro [8], que exibe o modelo da vista atual;
- em [9], um tempo de espera artificial em milissegundos. O valor predefinido é 0 (sem espera). Se N for o valor desse tempo de espera, qualquer ação do utilizador será executada após um tempo de espera de N milissegundos. Isto permite observar a gestão da espera implementada pela aplicação;
- em [10], o URL do servidor Spring 4. Seguindo o que foi referido anteriormente, trata-se do [http://localhost:8080];
- em [11] e [12], o identificador e a palavra-passe de quem 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 existe apenas para mostrar 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: o francês, por predefinição, e o inglês.
![]() |
- em [1], efetua-se o início de sessão;
![]() |
- uma vez conectado, pode-se escolher o médico com quem se deseja marcar uma consulta [2] e o dia da mesma [3];
- em [4], solicita-se a visualização da agenda do médico escolhido para o dia selecionado;
![]() |
- assim que tiver a agenda do médico, pode reservar um horário [5];
![]() |
- em [6], seleciona-se o doente para a consulta e confirma-se essa escolha em [7];
![]() |
Assim que a consulta for validada, é-se redirecionado automaticamente para a agenda, onde a nova consulta já se encontra registada. Esta consulta poderá ser posteriormente eliminada em [7].
As principais funcionalidades foram descritas. São simples. As que não foram descritas são funções de navegação para regressar a uma vista anterior. Terminemos com a gestão do idioma:
![]() |
- em [1], muda-se do francês para o inglês;
![]() |
- em [2], a vista passa 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 irá controlar a aplicação:
![]() |
- linha 4: o módulo chama-se [rdvmedecins];
- linha 5: o módulo [ngRoute] é utilizado para o encaminhamento dos URL;
- linha 6: o módulo [translate] é utilizado para a internacionalização dos textos;
- linha 7: o módulo [base64] é utilizado para codificar em Base64 a cadeia «login:password»;
- 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: a configuração das rotas;
- linha 40: a internacionalização das mensagens;
3.8.7. O controlador da página principal
Recorde-se o código HTML da página mestre [app.html]:
<body ng-controller="appCtrl">
<div class="container">
...
Na linha 1, todo o corpo (body) da página mestre é controlado pelo controlador [appCtrl]. Devido à sua posição, este é o controlador geral e principal da aplicação. Tal como explicado no parágrafo 3.7.15, o modelo construído por este controlador é herdado por todas as vistas que venham a ser 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) {
// depuração
utils.debug("[app] init");
// ----------------------------------------inicialização da página
// os modelos das # páginas
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// modelo da página atual
var app = $scope.app;
...
// ---------------------------------- métodos
// cancelar tarefa atual
app.cancel = function () {
...
};
// desligar sessão
app.deconnecter = function () {
...
};
// este código deve permanecer aqui, pois faz referência à função [cancel] que o precede
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 | |
resa.html | resaCtrl | |
agenda.html | agendaCtrl |
O que é importante compreender é que o objeto [$scope], sendo o modelo do controlador da página principal, é herdado por todas as vistas 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 modelos de outros controladores. A aplicação em análise evita cuidadosamente utilizar esta possibilidade. Assim, por exemplo, o controlador [loginCtrl] trabalha apenas com dois modelos:
- 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 tiver de transmitir informação ao controlador C2, proceder-se-á da seguinte forma:
No [C1]:
No [C2]:
Em ambos os casos, o $scope é herdado do controlador [appCtrl] e é, portanto, idêntico (trata-se de um ponteiro) no [C1] e no [C2]. O objeto [$scope.app], que serve de memória partilhada entre os controladores, será frequentemente referido como session nos comentários, por analogia com a sessão utilizada nas aplicações web clássicas, que designa a memória partilhada entre pedidos HTTP sucessivos.
Voltemos ao código do controlador [appCtrl]:
// os modelos das # páginas
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// modelo da página atual
var app = $scope.app;
// [app.debug] e [utils.verbose] têm de estar sempre sincronizados
app.debug = utils.verbose;
app.debug.on = config.debug;
// sem título da página por enquanto
app.titre = {show: false};
// sem barras de navegação
app.navbarrun = {show: false};
app.navbarstart = {show: false};
// sem erros
app.errors = {show: false};
// configuração regional predefinida
angular.copy(config.locales['fr'], $locale);
// a vista atual
app.view = {url: undefined, model: {}, done: false};
// a tarefa atual
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
- linha 8: [$scope.app] será o modelo da página principal. Será também a memória partilhada entre os diferentes controladores. Em vez de escrever [$scope.app.champ=value] em todo o lado, o ponteiro [$scope.app] é atribuído à variável [app] e, assim, escrever-se-á [app.champ=value]. Basta lembrar que [app] é o modelo apresentado na página-mestre;
- linha 11: [app.debug.on] é um valor booleano que controla o modo debug da aplicação. Por predefinição, está definido como true. O seu valor está associado à 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 do banner de erros;

- linha 22: [app.view] conterá informações sobre a vista atual, aquela que está atualmente a ser apresentada pela baliza [ng-view] da página principal. Nela, indicaremos as seguintes informações:
- [url]: o URL da vista atual, por exemplo, [/agenda];
- [model]: o modelo da vista atual, por exemplo, [$scope.agenda];
- [done]: indica, em relação a vrai, que a vista atual concluiu o seu trabalho e que se está a passar para outra vista;
Estas informações servem 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];
Foram fatorizados dois métodos no controlador [appCtrl]:
// cancelar tarefa atual
app.cancel = function () {
...
};
// desligar sessão
app.deconnecter = function () {
...
};
- linha 2: a função [app.cancel] serve para cancelar a tarefa atual para a qual está atualmente a ser apresentada uma mensagem de espera. Todas as vistas apresentam esta mensagem e, por isso, o cancelamento da tarefa será efetuado aqui;
- linha 7: a função [app.deconnecter] redireciona o utilizador para a página de autenticação. Todas as vistas, exceto a vista [/login], oferecem esta possibilidade;
A função [app.deconnecter] é a seguinte:
// desligar
app.deconnecter = function () {
// regresso à página de início de sessão
$location.path(config.urlLogin);
};
- linha 4: regressa-se à página de início de sessão de URL [/login];
3.8.8. Gestão da tarefa assíncrona
Na nossa aplicação, num determinado momento, apenas uma tarefa assíncrona estará em execução. É possível ter várias. Por exemplo, ao iniciar a aplicação, esta solicita ao serviço web a lista de médicos e, em seguida, a lista de clientes, através de duas requisições HTTP sucessivas. Poderíamos fazer o mesmo com duas solicitações HTTP simultâneas. O Angular oferece as ferramentas para esta gestão. Neste caso, não optámos por essa solução.
A tarefa em execução é cancelada com o seguinte código no controlador [appCtrl]:
// cancelamento da tarefa atual
app.cancel = function () {
utils.debug("[app] cancel task");
// cancela a tarefa assíncrona da vista atual
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
...
};
- linha 5: a tarefa é procurada em [app.view.model.task]. Além disso, 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 concluir a tarefa com falha. Esta notação é diferente da utilizada nos exemplos do Angular analisados:
- nos exemplos, o objeto [task] era um objeto [$q.defer()] que podia ser concluído;
- na versão final, o objeto [task] é um objeto com os campos [action, isFinished], em que [action] é o objeto [$q.defer()] quepode ser concluído e [isFinished] é um valor booleano que indica que a ação está concluída;
Vamos analisar o ciclo de vida do objeto [task] através de um exemplo. No arranque, após o controlador [appCtrl], é o controlador [loginCtrl] que assume o controlo para apresentar a vista [views/login.html]. O seu código de inicialização é o seguinte:
// recuperar o modelo pai
var login = $scope.login;
var app = $scope.app;
// vista atual
app.view = {url: config.urlLogin, model: login, done: false};
Na linha 5, temos [model=login]. Isto significa que, quando se altera o objeto [login], altera-se o objeto [app.view.model] e, consequentemente, o [$scope.app.view.model]. Quando, no controlador [loginCtrl], se pretende simular uma espera, escreve-se:
// espera simulada
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
Ao adicionar o campo [task] ao objeto [login], este foi, portanto, adicionado ao objeto [$scope.app.view.model]. Se o utilizador cancelar a espera, o código em [appCtrl.cancel]:
// modelo da página atual
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
concluirá corretamente a espera simulada (linhas 4-6).
3.8.9. Controlo da 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 | |
/agenda | sim | |
/página inicial | sim, se o controlador [homeCtrl] tiver indicado que concluiu o seu trabalho | |
/resa | sim | |
/agenda | sim | |
/agenda | sim, se o controlador [homeCtrl] tiver indicado que concluiu o seu trabalho | |
/resa | 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]:

- aqui não há qualquer controlo de navegação, uma vez que a regra estabelece que é possível aceder a URL [/login] a partir de qualquer lugar. Assim, se o utilizador introduzir este URL no seu navegador, funcionará independentemente da vista atual;
- linha 16: a nova vista atual;
O código para o controlador [homeCtrl] foi apresentado no parágrafo 3.8.7.
Por fim, para uma regra como:
/home | sim, se o controlador [homeCtrl] tiver indicado que concluiu o seu trabalho |
eis um exemplo de código que faz a transição do URL [/home] para o URL [/agenda]:
![]() |
Acima, estamos no método [afficherAgenda] do controlador [homeCtrl]. O utilizador solicitou a agenda de um médico.
- linha 107: a promessa da tarefa HTTP;
- linha 109: a variável [app] foi inicializada com [$scope.app]. Este último objeto é, como vimos, utilizado como modelo da vista [app.html]. Este modelo [$scope.app] é também utilizado para armazenar a informação que deve ser partilhada entre as vistas;
- linha 111: analisa-se o código de erro devolvido pela tarefa;
- linha 113: o resultado [result.data] é inserido no modelo [app];
- linha 116: o controlador [homeCtrl] vai passar o controlo ao controlador [agendaCtrl]. Indica-lhe que concluiu o seu trabalho com o código da 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. Os serviços
![]() |
Os serviços [config, utils, dao] são os já descritos na apresentação do Angular:
- o serviço [config] foi apresentado no parágrafo 3.7.4;
- o serviço [utils] foi apresentado no parágrafo 3.7.5;
- o serviço [dao] foi apresentado no parágrafo 3.7.6;
A título de recordação, relembramos a estrutura destes serviços:
Serviço [config]
![]() |
- no [1]: verifica-se que o código tem cerca de 250 linhas. A parte essencial deste código consiste na externalização das chaves das mensagens internacionalizadas [2]. Evita-se incluir estas chaves de forma estática no código;
Serviço [utils]
![]() |
- linha 8: ainda não tínhamos encontrado a variável [verbose]. Esta 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 do controlador [appCtrl]:
![]() |
- linha 21: [app.debug] assume o valor do ponteiro [utils.verbose]. Assim, qualquer alteração efetuada em [app.debug] será também efetuada em [utils.verbose];
- linha 22: o valor inicial de [app.debug.on] é obtido a partir do ficheiro de configuração. Por predefinição, é o valor true.. Este valor pode alterar-se ao longo do tempo. O utilizador tem, de facto, a possibilidade de o alterar nas 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. As diretivas
![]() |
As diretivas [errors, footable, list, waiting] são as já descritas na apresentação do Angular:
- a diretiva [footable] foi introduzida no parágrafo 3.7.8.6;
- a diretiva [list] foi introduzida no parágrafo 3.7.12;
- as diretivas [errors] e [waiting] foram introduzidas no parágrafo 3.7.14;
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 no formato JSON num banner Bootstrap (linha 1);
Esta diretiva é utilizada apenas na página mestre [app.html]:
![]() |
- A diretiva [debug] é utilizada na linha 35. Assim, apresenta a forma JSON do modelo [$scope.app] quando se está no modo de depuração (atributo ng-show). Isto resulta em algo semelhante a isto:
![]() |
Isto requer um bom conhecimento do código para ser interpretado, mas, uma vez adquirido esse conhecimento, a informação acima torna-se útil para a depuração. Destacámos aqui os elementos do modelo [$scope.app] apresentado. Recorde-se que [$scope.app] é a memória partilhada pelos controladores;
- [waitingBeforeTask]: o tempo de espera simulado antes de qualquer pedido HTTP;
- [debug]: o modo de depuração — é obrigatoriamente true se esta barra estiver a ser apresentada;
- [navbarrun]: valor booleano que controla a exibição da seguinte barra de navegação:
![]()
- [navbarstart]: valor booleano que controla a exibição da seguinte barra de navegação:
![]()
- [errors]: modelo da diretiva [errors];
- [view]: encapsula informações sobre a vista atualmente apresentada;
- [waiting]: modelo da diretiva [waiting];
- [serverUrl, username, password]: informações de ligação ao serviço web;
- [medecins]: modelo para a diretiva [list] aplicada aos médicos;
- [clients]: o mesmo para os clientes;
- [menu]: controla as opções do menu apresentadas. Estas são definidas em [navbar-run.html]:

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

O controlador [loginCtrl] é o seguinte:

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

- linhas 50-51: [app.waiting] é o modelo da barra de espera;
- linha 53: [app.errors] é o modelo da barra de erros;
- linha 55: é iniciada uma espera simulada. O objeto [action, isFinished] é referenciado por [login.task] e, por conseguinte, uma vez que [app.view.model=login], por [app.view.model.task]. Recorde-se que esta é a condição para que a tarefa possa ser cancelada;
- linha 57: após o fim da espera simulada, carregam-se os médicos;
- linha 62: quando a solicitação dos médicos for obtida, analisa-se essa solicitação. Se os médicos tiverem sido obtidos, solicita-se então os clientes;
- linha 83: analisa-se a resposta obtida e apresenta-se a vista final. Isto é feito com o seguinte código:

- linha 87: a variável booleana [task.isFinished] é definida como true nos seguintes casos:
- o utilizador cancelou a espera;
- o pedido dos médicos terminou com um erro;
- linhas 91-98: o caso em que se tiveram os clientes;
- linha 93: [app.clients] é o modelo da diretiva [list] que irá apresentar os clientes numa lista suspensa;
- linhas 97-98: preparamo-nos para mudar de vista (linha 98), mas antes indicamos que o controlador concluiu o seu trabalho (linha 97). Recorde-se que [$scope.app.view.done] é utilizado para o controlo de navegação;
O ponto importante a destacar aqui é que os médicos e os clientes foram armazenados em cache no navegador. A partir de agora, 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, associada à página principal, produz a seguinte página:

A estrutura do controlador [homeCtrl] é a seguinte:

- linhas 12-20: trata-se do controlo de navegação. Todos os controladores o possuem, exceto o [loginCtrl], uma vez que a página [/login.html] é acessível sem condições;

- linhas 25-28: aqui encontramos linhas semelhantes às encontradas no controlador [loginCtrl]. [home] é, assim, o modelo da vista associada ao controlador;
- linha 33: um atributo que ainda não tínhamos encontrado. Trata-se do modelo da barra de título da vista:
![]()
- linha 36: [home.datepicker] é o modelo do calendário;
- linha 38: [app.menu] é o modelo do menu da barra de navegação. Aqui estará presente a opção [Agenda]. É esta opção que permite consultar a agenda de um médico;
Por fim, o controlador tem dois métodos:

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

A estrutura do controlador [agendaCtrl] é a seguinte:

- as linhas 10-20 asseguram o controlo da navegação;

- linhas 23-26: [agenda] será o modelo da vista associada ao controlador [agendaCtrl];
- linhas 36-44: [app.titre] é o modelo da barra de título seguinte:

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

- linha 95: o método [agenda.supprimer] foi abordado no parágrafo 3.7.9;
O método [agenda.home] é um método de navegação pura:

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

- linha 73: o parâmetro da função [reserver] é o n.º do intervalo horário (id);
- linhas 77-86: destinam-se a localizar o intervalo horário com esse identificador;
- linha 82: o intervalo 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ção para [/resa.html];
3.8.15. O controlador [resaCtrl]
![]() |
O controlador [resaCtrl] está associado à vista [views/resa.html], que, associada à página principal, produz a página seguinte:

A estrutura do controlador [resaCtrl] é a seguinte:

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

- linhas 24-27: [resa] será o modelo da vista atual;
- linhas 38-45: [app.titre] é o modelo do banner 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 analisado no parágrafo 3.7.9.
3.8.16. Gestão de idiomas
Todos os controladores disponibilizam o seguinte método [setLang]:

Este método poderia ter sido fatorizado no controlador [appCtrl].




























































































































