3. El cliente Angular JS
3.1. Referencias del marco Angular JS
Al principio de este documento se proporcionaron dos referencias para el framework Angular JS. Las repetimos aquí:
- [ref1]: el libro «Pro AngularJS», escrito por Adam Freeman y publicado por Apress. Es un libro excelente. Los códigos fuente de los ejemplos de este libro están disponibles de forma gratuita en URL [http://www.apress.com/downloadable/download/sample/sample_id/1527/];
- [ref2]: la documentación oficial de Angular JS [https://docs.angularjs.org/guide];
Angular JS merece un libro por sí solo. El de Adam Freeman tiene más de 600 páginas y ninguna de ellas es superflua. Vamos a describir una aplicación Angular y, a lo largo de esta descripción, abordaremos los fundamentos de este framework. No obstante, nos limitaremos únicamente a las explicaciones necesarias para comprender la solución propuesta. Angular es un framework extremadamente completo y existen numerosas soluciones para llegar al mismo resultado. Esto supone una dificultad, ya que cuando se empieza, no se sabe si se está utilizando una solución peor o mejor que otra. Este es el caso de la solución que aquí se propone. Podría estar escrita de otra manera y quizá con mejores prácticas.
3.2. Arquitectura del cliente Angular
La arquitectura del cliente Angular se asemeja a la de una aplicación web clásica con algunas diferencias. Una aplicación web Spring, por ejemplo, tiene la siguiente arquitectura:
![]() |
El procesamiento de una solicitud de un cliente se lleva a cabo de la siguiente manera:
- solicitud: las URL solicitadas tienen el formato http://máquina:puerto/contexto/Acción/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] es la clase de Spring que procesa los URL entrantes. Ella «enruta» el URL hacia la acción que debe procesarlo. Estas acciones son métodos de clases específicas denominadas [Contrôleurs]. La C de MVC es aquí la cadena [Dispatcher Servlet, Contrôleur, Action]. Si no se ha configurado ninguna acción para procesar el URL entrante, el servlet [Dispatcher Servlet] responderá que no se ha encontrado el URL solicitado (error 404 NOT FOUND);
- procesamiento
- La acción seleccionada puede utilizar los parámetros parami que le ha transmitido el servlet [Dispatcher Servlet]. Estos pueden proceder de varias fuentes:
- la ruta [/param1/param2/...] del URL,
- de los parámetros [p1=v1&p2=v2] del URL,
- de los parámetros enviados por el navegador con su solicitud;
- en el procesamiento de la solicitud del usuario, la acción puede necesitar la capa [metier] [2b]. Una vez procesada la solicitud del cliente, esta puede generar diversas respuestas. Un ejemplo clásico es:
- una página de error si la solicitud no se ha podido procesar correctamente
- una página de confirmación en caso contrario
- la acción solicita que se muestre una vista determinada [3]. Esta vista mostrará datos que se denominan el modelo de la vista. Es la M de MVC. La acción creará este modelo M [2c] y solicitará que se muestre una vista V [3];
- respuesta: la vista V seleccionada utiliza el modelo M creado por la acción para inicializar las partes dinámicas de la respuesta HTML que debe enviar al cliente y, a continuación, envía dicha respuesta.
La arquitectura de nuestro cliente Angular será similar, aunque con una terminología ligeramente diferente. En primer lugar, las aplicaciones Angular suelen ser aplicaciones web de página única (APU) o Single Page Application (SPA):

- el usuario solicita la página inicial de la aplicación en el formato: http://máquina:puerto/contexto. El navegador consultará un servidor web para obtener el documento solicitado. Este es una página HTML con estilo definido por CSS y dinamizada por Javascript;
- A continuación, el usuario interactuará con las vistas que se le presentan. Se pueden distinguir varios tipos de interacciones:
- las que no requieren ninguna interacción con el exterior, por ejemplo, ocultar o mostrar elementos de la vista. Son gestionadas por el Javascript integrado;
- las que requieren datos procedentes de un servicio web remoto. Estos se recuperarán mediante una llamada AJAX (Asynchronous Javascript y Xml), se construirá un modelo y se mostrará una vista;
- las que requieren una vista distinta de la vista inicial. Esta se solicitará mediante una llamada Ajax al servidor que ha entregado la página inicial. A continuación, se repetirá el proceso anterior. La página obtenida se almacenará en la caché del navegador. En la siguiente llamada, no se solicitará al servidor remoto HTML;
Al final, el navegador solo realiza una única llamada HTTP, la que obtiene la página inicial. Las siguientes llamadas HTTP, al servidor de páginas HTML o a servicios web remotos, las realiza el Javascript integrado en las páginas.
A continuación, presentamos la arquitectura de la aplicación dentro del navegador. Dejamos de lado el servidor HTML que entrega las páginas HTML de la aplicación. A efectos explicativos, podemos considerar que todas ellas están presentes en la caché del navegador.
![]() |
En primer lugar, hay que situar esta arquitectura:
- en [1], estamos en un navegador;
- en [2], un usuario interactúa con las vistas mostradas por este;
- en [3], los datos se buscan en la red, a menudo en servicios web;
El usuario interactúa con las vistas: rellena formularios y los valida. Explicaremos este proceso con la vista V1 anterior. Supondremos que es la vista inicial de la aplicación. Se ha obtenido de la siguiente manera:
- el usuario solicita la vista inicial URL de la aplicación en el formato: http://máquina:puerto/contexto;
- el navegador solicitó el documento asociado a este URL. Recibió la página HTML / CSS / JS de la vista V1;
- el Javascript integrado en la página tomó entonces el control y lo cedió al controlador C1 [5];
- este construyó el modelo M1 [8] [9] de la vista V1. La construcción de este modelo pudo requerir el uso de servicios internos [6] y la consulta de servicios externos [7];
El usuario tiene ahora ante sí una vista V1. Imaginemos que se trata de un formulario. Lo rellena y, a continuación, lo valida:
- en [4], el usuario valida el formulario;
- en [5], este evento será procesado por uno de los métodos del controlador C1;
Si el evento solo provoca un simple cambio en la vista V1 (ocultar/mostrar campos), el controlador C1 modificará el modelo M1 de la vista V1 y, a continuación, volverá a mostrar la vista V1. Para ello, puede necesitar uno de los servicios de la capa [services] [6].
Si el evento requiere datos externos:
- en [6], el controlador C1 solicitará a la capa [DAO] que los obtenga;
- en [7], esta realizará una o varias llamadas AJAX para obtenerlos;
- en [8] y [9], se modificará la plantilla M1 y se mostrará la vista V1;
Si el evento provoca un cambio de vista, en los dos casos anteriores, en lugar de mostrar la vista V1, el controlador C1 solicitará una nueva URL [10]. Se trata de una URL interna del navegador. No se traduce inmediatamente en una llamada HTTP al servidor de páginas HTML. Este cambio de URL es gestionado por un enrutador configurado de tal manera que a cada URL interno le corresponde una vista V y su controlador C. El enrutador hace entonces que se muestre la nueva vista Vn. Antes de la visualización, su controlador Cn toma el control, construye el modelo Mn y, a continuación, muestra la vista Vn [11]. Si la página HTML de la vista Vn no estuviera almacenada en la caché del navegador, se solicitaría al servidor de páginas HTML.
La capa [Présentation] de esta arquitectura es similar a la arquitectura JSF (Java Server Faces):
- la vista V corresponde a la vista de tipo Facelet de JSF;
- el controlador C corresponde al bean JSF, una clase Java que contiene tanto el modelo M de la vista V como los gestores de eventos de esta;
La capa [Services] es diferente de las capas [Services] a las que estamos acostumbrados. En el desarrollo web del lado del servidor, lo más habitual es encontrar la siguiente arquitectura por capas:
![]() |
En el ejemplo anterior, la capa [web] solo se comunica con la capa [DAO] a través de la capa [métier]. Nada nos impediría inyectar en la capa [web] una referencia a la capa [DAO] que permitiera esta comunicación. Pero nos lo prohibimos.
Con Angular, no nos lo prohibimos. La arquitectura pasa entonces a ser la siguiente:
![]() |
- en [1], la capa [présentation] puede comunicarse directamente con cualquier servicio;
- en [2], los servicios se conocen entre sí. Un servicio puede utilizar uno o varios otros.
3.3. Las vistas del cliente Angular
Las vistas del cliente Angular ya se han presentado en el apartado 1.3.3. Para facilitar la lectura de este nuevo capítulo, las repetimos aquí. La primera vista es la siguiente:
![]() |
- en [6], la página de inicio de la aplicación. Se trata de una aplicación para concertar citas con médicos;
- en [7], una casilla de selección que permite estar o no en modo [debug]. Este último se caracteriza por la presencia del marco [8] que muestra la plantilla de la vista actual;
- en [9], un tiempo de espera artificial en milisegundos. Su valor por defecto es 0 (sin espera). Si N es el valor de este tiempo de espera, cualquier acción del usuario se ejecutará tras un tiempo de espera de N milisegundos. Esto permite ver la gestión de la espera implementada por la aplicación;
- en [10], el URL del servidor Spring 4. Si seguimos lo anterior, es [http://localhost:8080];
- en [11] y [12], el identificador y la contraseña de quien desea utilizar la aplicación. Hay dos usuarios: admin/admin (login/contraseña) con un rol (ADMIN) y user/user con un rol (USER). Solo el rol ADMIN tiene permiso para utilizar la aplicación. El rol USER solo está ahí para mostrar lo que responde el servidor en este caso de uso;
- en [13], el botón que permite conectarse al servidor;
- en [14], el idioma de la aplicación. Hay dos: el francés por defecto y el inglés.
![]() |
- en [1], se establece la conexión;
![]() |
- una vez conectado, se puede elegir el médico con el que se desea concertar una cita [2] y el día de la misma [3];
- se solicita en [4] ver la agenda del médico elegido para el día elegido;
![]() |
- una vez obtenido el agenda del médico, se puede reservar una franja horaria [5];
![]() |
- en [6], se selecciona al paciente para la cita y se valida esta selección en [7];
![]() |
Una vez validada la cita, se vuelve automáticamente a agenda, donde ya figura la nueva cita. Esta cita podrá eliminarse posteriormente en [7].
Se han descrito las principales funcionalidades. Son sencillas. Las que no se han descrito son funciones de navigation para volver a una vista anterior. Terminemos con la gestión del idioma:
![]() |
- en [1], se cambia del francés al inglés;
2

- en [2], la vista pasa a inglés, incluido el calendario;
3.4. Configuración del proyecto Angular
Vamos a construir nuestro cliente Angular de forma progresiva. Utilizamos Webstorm.
Creemos una carpeta vacía [rdvmedecins-angular-v1] y abrámosla con Webstorm:
![]() |
- en [1], abrimos una carpeta;
- en [2], seleccionamos la carpeta que hemos creado;
- en [3], obtenemos un proyecto de Webstorm vacío;
![]() |
- en [4], la configuración del proyecto se realiza mediante option y [File / Settings];
- en [5] y [6], se configura la propiedad [Spelling] que gestiona la revisión ortográfica. Por defecto, esta está activa. Dado que el software descargado está en inglés, nuestros comentarios en francés sobre los programas aparecerán subrayados como posibles errores ortográficos. Por lo tanto, desactivamos esta revisión ortográfica [7];
![]() |
- en [8], creamos un nuevo archivo;
- en [9], elegimos crear el archivo [package.json] que describe la aplicación con una sintaxis JSON;
- en [10], el archivo generado que se modifica como se muestra en [11];
- en [12], se guarda este archivo tanto en [package.json] como en [bower.json];
![]() |
- en [13], se vuelve a configurar el proyecto;
![]() |
- en [14], se configura la propiedad [Javascript / Bower] que nos permitirá declarar las bibliotecas Javascript que necesitamos;
- en [15], designamos el archivo [bower.json] que acabamos de crear;
![]() |
- en [16], añadamos una biblioteca Javascript;
- en [17] se muestran todas las bibliotecas Javascript descargables;
- en [18], podemos introducir un criterio para filtrar la lista [17]. Aquí indicamos que queremos la biblioteca [Angular JS];
- en [19], aparecen las características de la biblioteca. Aquí vemos que se va a descargar la version 1.2.18 de Angular;
- en [20], se descarga;
![]() |
- en [21], vemos que se ha descargado;
- en [22], se ve el archivo version descargado. Por lo tanto, en realidad es la versión 1.2.19;
- en [23], se ve la última version disponible;
![]() |
- en [24], siguiendo el mismo procedimiento que antes, se descargan las siguientes bibliotecas:
para codificar la cadena «user:password» en Base64; | ||
para internacionalizar el calendario | ||
para redirigir los URL internos de la aplicación al controlador y la vista correctos; | ||
permite la internacionalización de las vistas. Se trata de un proyecto independiente de Angular. Aquí se utilizarán dos idiomas: el francés y el inglés; | ||
proporciona componentes visuales compatibles con Bootstrap. Aquí utilizaremos su calendario; | ||
el framework CSS Bootstrap. Se utilizará para construir las vistas; | ||
proporciona un componente visual de tipo «tabla». Es «responsivo» en el sentido de que puede adaptarse al tamaño de la pantalla; | ||
proporciona un componente de tipo «lista desplegable»; |
![]() |
- en [25], las bibliotecas descargadas se han instalado en la carpeta [bower_components];
- en [26], se ve que se ha descargado la biblioteca JQuery. Esto se debe a que Bootstrap la utiliza. El sistema de instalación de las dependencias Javascript de un proyecto es similar al de Maven en el mundo Java: si una biblioteca descargada tiene a su vez dependencias, estas se descargan automáticamente;
El archivo [bower.json] ha cambiado:
Todas las dependencias descargadas se han registrado en el archivo.
3.5. La página inicial del cliente Angular
Creamos un primer version de la página inicial del cliente Angular:
![]() |
- en [1] y [2], creamos un archivo HTML llamado [app-01], [3] y [4];
El archivo [app-01.html] será nuestra página principal durante un tiempo. En él configuraremos la importación de los archivos CSS y JS que necesita la aplicación:
<!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é">
<!-- el CSS -->
<link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
- líneas 11-12: los archivos CSS para Bootstrap;
- línea 13: el archivo CSS para el componente [boostrap-select];
- línea 14: el archivo CSS para el componente [footable];
- líneas 21-24: los archivos JS de los componentes Bootstrap;
- línea 21: los componentes Bootstrap están impulsados por JQuery;
- línea 22: el archivo JS de Bootstrap;
- línea 23: el archivo JS para el componente [boostrap-select];
- línea 24: el archivo JS para el componente [footable];
- líneas 26-30: los archivos JS de Angular y de los proyectos relacionados;
- línea 26: el archivo JS de Angular. Debe cargarse después de JQuery si se utiliza esta biblioteca;
- línea 27: el archivo JS del proyecto [angular-ui-bootstrap];
- línea 28: el archivo JS del enrutador [angular-route];
- línea 29: el archivo JS del módulo de internacionalización de aplicaciones Angular;
- línea 30: el archivo JS del módulo [angular-base64];
Se puede comprobar la validez del archivo [app-01.html]:
![]() |
- en [1], se solicita la inspección del código;
- en [2], el resultado cuando todo va bien;
Se recomienda esta inspección sistemática del código antes de su ejecución. En este caso, esta detección permite detectar cualquier error de referencia en los archivos CSS y JS. Si una ruta es incorrecta, el inspector de código lo señalará.
- En [3], la página se puede cargar en un navegador mediante un depurador. Se obtiene el siguiente resultado en el navegador:
![]() |
- en [4], la página [app-01.html] ha sido servida por un servidor interno de Webstorm que opera aquí en el puerto 63342;
- en [5], la consola del depurador. Si se hubieran producido errores, habrían aparecido aquí. Aquí es también donde van a parar las salidas de pantalla generadas por la instrucción [console.log(expression)] de Javascript. Aprovecharemos al máximo esta posibilidad;
El modo de depuración permite modificar la página en Webstorm y ver los resultados de dichos cambios en el navegador sin tener que recargar la página. Así, si añadimos la línea 3 que aparece a continuación:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2>Version 1</h2>
</div>
y volvemos al navegador, vemos que la página ha cambiado:
![]() |
3.6. Descubrimiento de Bootstrap
Ahora vamos a ilustrar algunas de las características de Bootstrap utilizadas en la aplicación. Solo tengo un conocimiento limitado de este framework, obtenido mediante copiar y pegar códigos encontrados en Internet. Explicaré la función de las clases CSS que creo entender. Me abstendré de comentar las demás.
3.6.1. Ejemplo 1
En Angular, las operaciones que recogen información del exterior son asíncronas. Esto significa que la operación se inicia y se produce un retorno inmediato a la vista, con la que el usuario puede seguir interactuando. La aplicación recibe una notificación del final de la operación mediante un evento. Este evento es procesado por una función JS que puede entonces enriquecer la vista actual o cambiarla. Si la operación puede ser larga, es útil ofrecer al usuario la posibilidad de cancelarla. Se la ofreceremos sistemáticamente. Para ello, utilizaremos un banner de Bootstrap:

Para obtener este resultado, duplicamos [app-01.html] en [app-02.html] y modificamos las siguientes líneas:
<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>
- línea 1: la clase CSS [container] define un área de visualización dentro del navegador;
- línea 3: la clase CSS [alert] muestra un área de color. La clase [alert-warning] utiliza un color predefinido;
- línea 5: la clase [btn] diseña un botón. La clase [btn-primary] le da un color determinado. La clase [pull-right] lo coloca a la derecha del banner de alerta;
- línea 6: una imagen animada de espera;
3.6.2. Ejemplo 2
Las diferentes vistas de la aplicación tendrán un título común:

Para obtener este resultado, duplicamos [app-01.html] en [app-03.html] y modificamos las siguientes líneas:
<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>
- el área coloreada se obtiene con la clase [jumbotron] de la línea 4;
- línea 5: la clase [row] define una línea de 12 columnas;
- línea 6: la clase [col-md-2] define un área de dos columnas en la línea;
- línea 7: en estas dos columnas se coloca una imagen;
- líneas 9-11: en las otras 10 columnas se coloca el texto;
3.6.3. Ejemplo 3
Las vistas tendrán una barra de control superior. En ella se encontrarán opciones de control, enlaces o botones. También se encontrarán elementos de formulario. Por ejemplo:
![]() |
Para obtener este resultado, duplicamos [app-01.html] en [app-04.html] y modificamos las siguientes líneas:
<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 debug -->
<label style="width: 100px">
<input type="checkbox">
<span style="color: white">Debug</span>
</label>
<!-- formulario de identificación -->
<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>
- línea 4: la clase [navbar] aplicará el estilo a la barra de navigation. La clase [navbar-inverse] le da el fondo negro. La clase [navbar-fixed-top] hará que, al desplazarse por la página mostrada por el navegador, la barra de navigation permanezca en la parte superior de la pantalla;
- líneas 6-14: definen el área [1]. Se trata típicamente de una serie de clases que no entiendo. Utilizo el componente tal cual;
- línea 15: define una zona «responsive» de la barra de comandos. En un smartphone, esta zona desaparece en un área de menú;
- línea 16: la clase [navbar-form] diseña un formulario de la barra de comandos. La clase [navbar-right] lo desplaza a la derecha de esta;
- líneas 23-32: las cuatro zonas de entrada del formulario de la línea 17 [3]. Se encuentran dentro de una clase [form-group] que da estilo a los elementos de un formulario y cada una de ellas tiene la clase [form-control];
- línea 33: la clase [btn] que ya hemos visto, enriquecida con la clase [btn-success] que le da su color verde;
3.6.4. Ejemplo 4
La barra de control permitirá cambiar de idioma mediante un menú desplegable:

Para obtener este resultado, duplicamos [app-01.html] en [app-05.html] y añadimos las siguientes líneas a la barra de control:
<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>
Las líneas añadidas son las líneas 4-21.
- línea 5: la clase [btn-group] da estilo a un grupo de botones. Hay dos en las líneas 6 y 9;
- líneas 6-8: el primer botón define el texto de la lista desplegable. La clase [btn-danger] le da un color rojo;
- líneas 9-12: el segundo botón es el de la lista desplegable. Está junto al primero, lo que da la impresión de ser un único componente;
- línea 10: muestra la flecha hacia abajo que indica que el botón es una lista desplegable;
- línea 11: para «lectores de pantalla»;
- líneas 13-20: los elementos de la lista desplegable son los elementos de una lista no ordenada;
3.6.5. Ejemplo 5
Para validar un formulario o para navegar, el usuario dispondrá en la barra de control de opciones o botones como los que se muestran a continuación:
![]() |
Se han instalado opciones de menú en [1]. Para obtener este resultado, duplicamos [app-01.html] en [app-06.html] y añadimos las siguientes líneas:
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
...
</div>
<!-- opciones de menú -->
<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>
<!-- botones de la derecha -->
<form class="navbar-form navbar-right" role="form">
...
</form>
</div>
</div>
</div>
</div>
- Las opciones de menú se obtienen en las líneas 8-29. Se trata, de nuevo, de elementos de una lista <ul>. La clase [active] hace que el texto aparezca resaltado, indicando así que se puede hacer clic en option.
3.6.6. Ejemplo 6
Presentaremos a los médicos y los clients en listas desplegables como se muestra a continuación:
![]() |
La lista desplegable utilizada no es un componente nativo de Bootstrap. Se trata del componente [bootstrap-select] (http://silviomoreto.github.io/bootstrap-select/). Para obtener este resultado, duplicamos [app-01.html] en [app-07.html] y añadimos las siguientes líneas:
<!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 de Bootstrap JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- script local -->
<script>
$('.selectpicker').selectpicker();
</script>
</body>
</html>
- línea 5: hay que importar la hoja de estilo de [bootstrap-select];
- línea 13: el atributo [data-style] es utilizado por [bootstrap-select]. Sirve para dar estilo a la lista desplegable. Aquí le damos la forma de un botón azul [btn-primary];
- línea 13: el atributo [class] se utiliza en la línea 23. Puede ser cualquiera;
- líneas 14-17: los elementos de la lista desplegable. Aquí tenemos las etiquetas HTML clásicas;
- línea 22: hay que importar el JS desde [bootstrap-select];
- líneas 24-26: un script JS que se ejecuta al final de la carga de la página;
- línea 25: una instrucción JQuery. Se aplica el método [selectpicker] (selectpicker()) a todos los elementos que tengan la clase [selectpicker] ($('.selectpicker')). Solo hay uno, la etiqueta <select> de la línea 13. El método [selectpicker] proviene del archivo JS al que se hace referencia en la línea 22;
3.6.7. Ejemplo 7
Para mostrar el agenda de un médico, vamos a utilizar una tabla «responsive» proporcionada por la biblioteca JS [footable]:
![]() |
- en [1]: la tabla con una visualización normal;
- en [2]: la tabla cuando se reduce el tamaño de la ventana del navegador. La columna [Action] pasa automáticamente a la línea siguiente. Esto es lo que se denomina un componente «responsive» o, simplemente, adaptable.
Duplicamos [app-01.html] en [app-08.html] y añadimos las siguientes líneas:
...
<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>
- las líneas 2 y 60 ya están presentes en [app-01.html]. Se trata de los archivos CSS y JS proporcionados por la biblioteca [footable];
- la línea 3 hace referencia al siguiente archivo CSS:
@CHARSET "UTF-8";
#franjas th {
text-align: center;
}
#espacios 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;
}
Los estilos [status-*] proceden de un ejemplo de uso de la tabla [footable] que se encuentra en el sitio web de la biblioteca.
- línea 8: coloca la tabla en una línea [row] y un recuadro de color [alert alert-warning];
- línea 9: la tabla ocupará 6 columnas [col-md-6];
- línea 10: la tabla HTML está formateada con Bootstrap [class='table'];
- línea 13: el atributo [data-toggle] indica la columna que contiene el símbolo [+/-] que despliega/oculta la línea;
- línea 19: el atributo [data-hide='phone'] indica que la columna debe ocultarse si la pantalla tiene el tamaño de una pantalla de teléfono. También se puede utilizar el valor «tablet»;
3.6.8. Ejemplo 8
Para ayudar al usuario, vamos a crear ventanas emergentes de ayuda (tooltip) alrededor de los principales componentes de las vistas:
![]() |
Para obtener este resultado, duplicamos [app-01.html] en [app-09.html] y añadimos las siguientes líneas:
<!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>
<!-- opciones de menú -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- script local -->
<script>
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>
Las burbujas de ayuda las proporciona la biblioteca [angular-ui-bootstrap], que a su vez se basa en la biblioteca [angular]. En la línea 50 se importa la biblioteca [angular-ui-bootstrap]. Para implementar los componentes de la biblioteca [angular-ui-bootstrap], debemos crear un módulo Angular. Esto se hace en las líneas 52-55. Estas líneas definen un módulo Angular denominado [rdvmedecins] (primer parámetro). Un módulo Angular puede utilizar otros módulos Angular. Esto se denomina dependencias del módulo. Se proporcionan en una matriz como segundo parámetro de la función [angular.module]. Aquí, el módulo denominado [ui.bootstrap] lo proporciona la biblioteca [angular-ui-bootstrap]. Este módulo es el que nos proporcionará las burbujas de ayuda.
La línea 54 define un módulo Angular. Por defecto, esto no tiene ningún efecto en la página. Se indica que la página debe ser gestionada por Angular, vinculándola a un módulo Angular. Esto es lo que se hace en la línea 2. El atributo [ng-app='rdvmedecins'] vincula la página al módulo creado en la línea 54. A continuación, Angular analizará la página. Los atributos [tooltip] serán detectados y procesados por el módulo [ui.bootstrap].
La sintaxis de la burbuja de ayuda es la siguiente:
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
En el ejemplo anterior, se añade una burbuja de ayuda al texto [Home]:
- [tooltip]: define el texto de la burbuja de ayuda;
- [tooltip-placement]: define su posición (bottom, top, left, right);
Angular JS permite añadir nuevas etiquetas o nuevos atributos a los que ya existen en el lenguaje HTML. Esta extensión del lenguaje HTML se realiza mediante directivas de Angular. Aquí, los atributos [tooltip] y [tooltip-placement] son atributos creados por [angular-ui-bootstrap].
3.6.9. Ejemplo 9
Para ayudar al usuario a elegir el día de una cita, le ofreceremos un calendario:

Al igual que con las burbujas de ayuda, este calendario lo proporciona la biblioteca [angular-ui-bootstrap]. Para obtener este resultado, duplicamos [app-01.html] en [app-10.html] y añadimos las siguientes líneas:
<!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>
Al igual que antes, la página está asociada a un módulo Angular (líneas 2 y 28). El calendario se define mediante la etiqueta <datepicker> de la línea 16, definida por la biblioteca [angular-ui-bootstrap]:
- [show-weeks='true']: para mostrar los números de las semanas;
- [class='well']: para rodear el calendario con un área gris de esquinas redondeadas;
- [ng-model='jour']: los atributos [ng-*] son atributos de Angular. El atributo [ng-model] designa un dato que se colocará en la plantilla de la vista. Cuando el usuario haga clic en una fecha, esta se colocará en la variable [jour] del modelo. Esta variable se utiliza en la línea 10. La sintaxis {{expresión}} permite evaluar una expresión compuesta por elementos del modelo. Aquí, {{día}} mostrará el valor de la variable [jour] del modelo. Una característica destacada de Angular es que la vista seguirá automáticamente los cambios de la variable [jour]. Así, cuando el usuario cambie las fechas, estos cambios se mostrarán inmediatamente en la línea 10. En general, el funcionamiento es el siguiente:
- una vista V está asociada a un modelo M;
- Angular observa el modelo M y actualiza automáticamente la vista V cuando se produce un cambio en su modelo M;
La sintaxis {{día|fecha}} se denomina filtro. No es el valor de [jour] el que se muestra, sino el valor de [jour] filtrado por un filtro denominado [date]. Este filtro está predefinido en Angular. Sirve para dar formato a las fechas. Admite parámetros que especifican el formato deseado. Así, la expresión {{jour | date:'fullDate'}} indica que se desea el formato completo de la fecha, en este caso [Friday, June 20, 2014], ya que el calendario está en inglés por defecto. Abordaremos su internacionalización próximamente.
3.6.10. Conclusión
Hemos presentado los elementos del framework CSS Bootstrap que vamos a utilizar. Se trataba de componentes pasivos: sus eventos no se gestionaban. Por lo tanto, al hacer clic en los botones o enlaces no ocurría nada. Estos eventos se gestionarán en Javascript. Es posible utilizar este lenguaje sin la ayuda de marcos de trabajo, pero, al igual que ocurrió en el lado del servidor, algunos marcos de trabajo son imprescindibles en el lado del cliente. Es el caso del marco de trabajo Angular JS, que aporta una nueva forma de abordar el desarrollo de aplicaciones Javascript ejecutadas por un navegador. A continuación lo presentamos.
3.7. Descubrimiento de Angular JS
A continuación, ilustraremos algunas de las características del framework Angular JS utilizadas en la aplicación. Ya hemos visto algunas de ellas:
- una página HTML funciona con Angular JS si se le asocia un módulo:
<html ng-app="rdvmedecins">
- Angular permite crear nuevas etiquetas y nuevos atributos HTML mediante directivas:
- Angular permite crear filtros:
- Una vista V muestra un modelo M. Angular supervisa el modelo M y actualiza automáticamente la vista V cuando se produce un cambio en el modelo M. El valor de una variable del modelo M se muestra en la vista V mediante:
Vamos a empezar por profundizar en la implementación del patrón de diseño Modelo-Vista-Controlador en Angular. Recordemos las relaciones que existen entre ellos desde el punto de vista de la arquitectura:
![]() |
- la vista V1 muestra el modelo M1 construido por el controlador C1. Este último contiene no solo el modelo M1, sino también los gestores de eventos de la vista V1. Nos encontramos en el ciclo 5, 8, 9:
- [5]: se produce un evento en la vista V1. Es procesado por el controlador C1;
- este realiza su trabajo [6-7] y, a continuación, construye el modelo M1 [8];
- [9]: la vista V1 muestra el nuevo modelo M1. Como hemos dicho, este último paso es automático. No hay, como en otros marcos, un push explícito (C1 empuja la plantilla M1 a V1) ni un pull explícito (la vista V1 va a buscar el modelo M1 en C1). Hay un push implícito que el desarrollador no ve;
- luego se reanuda el ciclo 5, 8, 9;
3.7.1. Ejemplo 1: el modelo MVC de Angular
Retomaremos el ejemplo del calendario. Ya hemos visto la directiva que lo genera:
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
Esta directiva admite otros atributos además de los presentados anteriormente, entre otros el atributo [min-date], que establece la fecha mínima que se puede seleccionar en el calendario. Esto nos resultará útil. Cuando el usuario elige una fecha para una cita, esta debe ser igual o posterior a la del día actual. Por lo tanto, escribiremos:
<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>
donde [dateMin] será una variable de la plantilla de la página cuyo valor será la fecha de hoy. Esto dará como resultado la siguiente página:
![]() |
- en [1], estamos a 19 de junio de 2014. El cursor indica que se puede seleccionar el 19 de junio;
- en [2], el cursor indica que no se puede seleccionar el 18 de junio;
Duplicamos [app-10.html] en [app-11.html] y realizamos las siguientes modificaciones:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
...
<!-- script local -->
<script>
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// fecha mínima
$scope.minDate = new Date();
}]);
</script>
</body>
</html>
Examinemos primero el script local de las líneas 26-37:
- línea 28: creación del módulo [rdvmedecins] con su dependencia del módulo [ui.bootstrap], que proporciona el calendario;
- líneas 30-35: creación de un controlador. Este es el que contendrá la plantilla de nuestra página. Aquí no habrá ningún gestor de eventos;
- líneas 30-31: el controlador [rdvMedecinsCtrl] pertenece al módulo [rdvmedecins]. Se pueden añadir tantos controladores como se desee a un módulo. En nuestra aplicación tendremos:
- un módulo de gestión de la aplicación;
- un controlador por vista;
- el segundo parámetro de la función [controller] es una matriz de la forma ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)]. El último parámetro es la función que implementa el controlador. Sus parámetros son objetos que Angular JS proporcionará a la función.
Volvamos a la arquitectura de una aplicación Angular:
![]() |
En el ejemplo anterior, el controlador C1 contiene el conjunto de controladores de eventos de la vista V1, así como el modelo M1 de esta última. Los controladores de eventos pueden necesitar uno o varios servicios [6] para realizar su trabajo. Se pasan todos ellos como parámetros de la función de construcción del controlador:
Los servicios Si son singletons. Angular crea una única instancia de cada uno. Se identifican mediante un nombre Si. ¿Por qué aparecen dos veces en la tabla anterior? En producción, los scripts JS se minifican. En este proceso de minificación, la tabla anterior queda así:
Los parámetros pierden su nombre. Sin embargo, se trata del nombre de los servicios. Por lo tanto, es importante conservar esos nombres. Por eso se pasan como cadenas de caracteres como parámetros que preceden a la función. Las cadenas de caracteres no se modifican durante el proceso de minificación. Cuando Angular construya el controlador con la nueva matriz, sustituirá a1 por S1, a2 por S2, ... Por lo tanto, el orden de los parámetros es importante. Debe coincidir con el orden de los servicios que preceden a la definición de la función.
Volvamos a la definición del controlador [rdvMedecinsCtrl]:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// fecha mínima
$scope.minDate = new Date();
}]);
- líneas 3-4: el único objeto inyectado en el controlador es el objeto $scope. Se trata de un objeto predefinido que representa el modelo M de las vistas asociadas al controlador. Para enriquecer el modelo de una vista, basta con añadir campos al objeto $scope;
- eso es lo que se hace en la línea 6. Se crea el campo [minDate] con el valor de la fecha de hoy;
La vista V utiliza este modelo M de la siguiente manera:
<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>
...
- línea 1: el cuerpo de la página se asocia al controlador [rdvMedecinsCtrl] mediante el atributo [ng-controller]. Esto significa que todo lo que se encuentre dentro de la etiqueta <body> utilizará el controlador [rdvMedecinsCtrl] para gestionar sus eventos y obtener su modelo M. Una página HTML puede depender de varios controladores, ya sean anidados entre sí o no:
Arriba:
- el contenido de [div1] (líneas 1-10) muestra la plantilla M1 gestionada por el controlador c1. Las etiquetas de esta zona pueden hacer referencia a los gestores de eventos del controlador c1;
- el contenido de [div11] (líneas 3-4) muestra la plantilla M11 gestionada por el controlador c11, pero también la plantilla M1. Existe herencia de modelos. Las etiquetas de esta zona pueden hacer referencia tanto a los gestores de eventos del controlador c11 como a los del controlador c1. No pueden hacer referencia ni al modelo M12 del controlador c12 ni a los gestores de eventos de este. De hecho, el controlador c12 no se conoce entre las líneas 3-5;
- líneas 7-9: se puede seguir un razonamiento análogo al anterior;
Volvamos al código del calendario:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
El atributo [min-date] se inicializa con el valor [minDate] de la plantilla. Implícitamente, [$scope.minDate]. El campo siempre se busca en el objeto $scope.
3.7.2. Ejemplo 2: localización de fechas
Por el momento, el calendario no nos resulta de mucha utilidad, ya que es un calendario inglés. Es posible localizarlo:
![]() |
- en [1], tenemos un calendario en francés;
- en [2], lo cambiamos al inglés;
- en [3], el calendario en inglés;
Duplicamos la página [app-11.html] en [app-12.html] y luego modificamos esta última de la siguiente manera:
<!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">
<!-- el calendario-->
<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>
<!-- los 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>
Hay pocos cambios. Simplemente se han añadido las líneas 21-31 de la lista desplegable de idiomas. Por primera vez, nos encontramos con un gestor de eventos en las líneas 27-28:
- línea 27: el atributo [ng-click] es un atributo de Angular que indica el gestor de eventos que se debe ejecutar al hacer clic en el elemento que tiene este atributo. Aquí se ejecutará la función [$scope.setLang('fr')]. Esta función configurará el calendario en francés;
- línea 28: aquí, configuramos el calendario en inglés;
- línea 35: dado que el Javascript del controlador es bastante extenso, lo colocamos en un archivo [rdvmedecins.js];
Angular gestiona la localización de las vistas con un módulo llamado [ngLocale]. La definición de nuestro módulo [rdvmedecins] será, por tanto, la siguiente:
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);
Línea 2: no hay que olvidar las dependencias, ya que Angular a veces es poco preciso en sus mensajes de error. Olvidar una dependencia es, por lo tanto, especialmente difícil de detectar. Aquí tenemos una nueva dependencia del módulo [ngLocale].
Por defecto, Angular solo gestiona la localización de fechas, números, etc., que tienen variantes locales. No gestiona la internacionalización de textos. Para ello, utilizaremos la biblioteca [angular-translate]. La gestión de la localización la realiza la biblioteca [angular-i18n]. Esta biblioteca incluye tantos archivos como variantes hay para fechas, números, etc.
![]() |
Para el calendario francés, utilizaremos el archivo [angular-locale_fr-fr.js] y para el calendario inglés, el archivo [angular-locale_en-us.js]. Veamos, por ejemplo, qué hay en el archivo [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;}
});
}]);
En él vemos los elementos que permiten crear un calendario francés:
- líneas 10-18: la tabla de los días de la semana;
- líneas 19-32: la tabla de los meses del año;
- líneas 33-41: la tabla de los días de la semana abreviados;
- líneas 42-55: la tabla de los meses del año abreviados;
- líneas 56-63: formatos de fecha y hora. En la línea 62 se reconoce el formato «dd/mm/aa» de las fechas francesas;
- líneas 65-95: información sobre el formato de los números. Esto no nos interesa aquí;
- línea 96: el identificador «fr-fr» de la configuración regional del archivo (fr-fr: francés de Francia, fr-ca: francés de Canadá, ...)
En el archivo [angular-locale_en-us.js] tenemos exactamente lo mismo, pero esta vez para la versión en inglés de USA (en-us).
El código anterior no es muy fácil de leer. Al leerlo con atención, se descubre que todo este código define la variable [$locale] de la línea 4. Al cambiar el valor de esta variable se consigue la internacionalización de fechas, números, moneda, etc. Curiosamente, Angular no ha previsto que se cambie la variable [$locale] durante la ejecución. Se define de una vez por todas importando el archivo de la configuración regional deseada:
<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>
No sirve de nada importar todos los archivos de las configuraciones regionales deseadas, ya que cada archivo, como hemos visto, solo hace una cosa: definir la variable [$locale]. El último archivo importado es el que prevalece y, a partir de ahí, ya no hay forma de cambiar la configuración regional.
Al navegar por la red en busca de una solución a este problema, no he encontrado ninguna. Propongo una aquí: [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. La idea es poner las diferentes configuraciones regionales que necesitamos en un diccionario. Ahí es donde las buscaremos cuando haya que cambiarlas. El código Javascript de [rdvmedecins.js] tiene la siguiente estructura:
![]() |
Si eliminamos la definición de las configuraciones regionales, que ocupa 200 líneas (líneas 15-215 arriba), el código es sencillo:
- línea 6: define el módulo [rdvmedecins] y sus dependencias;
- líneas 8-10: define el controlador [rdvMedecinsCtrl] de la página;
- línea 9: la función de construcción del controlador recibe dos parámetros:
- $scope: para crear la plantilla de la vista;
- $locale: que es la variable que gestiona la localización del calendario. Es esta la que hay que cambiar cuando se cambia de idioma;
- línea 13: la variable [minDate] de la plantilla se inicializa con la fecha de hoy;
- línea 15: define el diccionario [locales]. Obsérvese que no se ha escrito [$scope.locales]. De hecho, la variable [locales] no forma parte de la plantilla expuesta en la vista;
- líneas 15-215: definen un diccionario {'fr':locale-fr-fr, 'en':locale-en-us}. Los valores [locale-fr-fr] y [locale-en-us] se toman, respectivamente, de los archivos JS, [angular-locale_fr-fr.js] y [angular-locale_en-us.js]. Lo más difícil es no equivocarse con los numerosos paréntesis de este diccionario...
- línea 217: se inicializa la variable $locale con locales['fr'], es decir, el version francés de la configuración regional. No se puede escribir simplemente [$locale=locales['fr']], ya que esto asigna a $locale la dirección de locales['fr']. Hay que hacer una copia de valor. Esto se puede hacer con la función predefinida [angular.copy];
- línea 219: la variable [jour] de la plantilla se inicializa con la fecha actual. Esto hace que el calendario se muestre situado en esa fecha;
- líneas 223-230: definen el gestor de eventos que se invoca al cambiar de idioma. Cabe destacar la sintaxis:
para definir un gestor de eventos que se llamaría [nom_fonction] y que admitiría los parámetros [param1, param2, ...];
Recordemos el código HTML de la lista desplegable:
<!-- 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>
- línea 8: la selección del francés provoca la llamada a [setLang('fr')];
- línea 9: la selección del inglés provoca la llamada a [setLang('en')];
- línea 3: el atributo [is-open] es un booleano que controla la apertura (true) o el cierre (false) de la lista desplegable. Se inicializa con la variable [isopen] del modelo de la vista;
Volvamos al código de [rdvmedecins.js]:
- línea 225: cambiamos el valor de la variable [$locale] por el valor del diccionario [locales] que corresponda;
- línea 227: se ha indicado que, cuando cambia el modelo M de una vista V, la vista V se actualiza automáticamente con el nuevo modelo. En la línea 225, se ha cambiado el valor de la variable [$locale], que no forma parte del modelo M mostrado por la vista V. Hay que encontrar una forma de cambiar este modelo M para que el calendario se actualice y utilice su nueva configuración regional. Aquí, cambiamos la variable [jour] del modelo del calendario. Se inicializa con un nuevo puntero (new) que apunta a una fecha idéntica a la que se muestra. [$scope.jour.getTime()] es el número de milisegundos transcurridos entre el 1 de enero de 1970 y la fecha mostrada por el calendario. Con este número, se reconstruye una nueva fecha. Por supuesto, obtendremos la misma fecha y el calendario seguirá situado en la fecha que mostraba. Pero el valor de [$scope.jour], que en realidad es un puntero, habrá cambiado y el calendario se actualizará;
- línea 229: se asigna a false el valor de la variable [isopen] de la plantilla. Esta variable controla uno de los atributos de la lista desplegable:
<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>
En la línea 1 anterior, el atributo [is-open] pasará a false, lo que tendrá como efecto cerrar la lista desplegable.
3.7.3. Ejemplo 3: internacionalización de los textos
Volvamos a la localización del calendario:
![]() |
En [3], vemos que el calendario está en inglés, pero no los textos [Calendrier, Langues]. Por defecto, Angular no ofrece ninguna herramienta para la internacionalización de los mensajes. Aquí vamos a utilizar la biblioteca [angular-translate] (https://github.com/angular-translate/angular-translate).
Vamos a desarrollar el siguiente ejemplo:
![]() |
- en [1], la vista en francés;
- en [2], la vista en inglés;
Veamos la configuración necesaria para la internacionalización. El script [rdvmedecins.js] se modifica de la siguiente manera:
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// configuración i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// mensajes en francés
$translateProvider.translations("fr", {
'msg_header': 'Cabinet Médical<br/>Les Médecins Associés',
'msg_langues': 'Langues',
'msg_agenda': 'Agenda de {{titre}} {{prenom}} {{nom}}<br/>le {{jour}}',
'msg_calendrier': 'Calendrier',
'msg_jour': 'Jour sélectionné : ',
'msg_meteo': "Aujourd'hui, il va pleuvoir..."
});
// mensajes en inglés
$translateProvider.translations("en", {
'msg_header': 'The Associated Doctors',
'msg_langues': 'Languages',
'msg_agenda': "{{titre}} {{prenom}} {{nom}}'s Diary<br/> on {{jour}}",
'msg_calendrier': 'Calendar',
'msg_jour': 'Selected day: ',
'msg_meteo': 'Today, it will be raining...'
});
// idioma predeterminado
$translateProvider.preferredLanguage("fr");
}]);
- línea 2: la primera modificación es la adición de una nueva dependencia. La internacionalización de la aplicación requiere el módulo Angular [pascalprecht.translate];
- líneas 5-26: definen la función [config] del módulo [rdvmedecins]. Al iniciar una aplicación Angular, el framework instancia todos los servicios necesarios para la aplicación, tanto los predefinidos de Angular como los definidos por el usuario. Por el momento, no hemos definido ningún servicio. La función [config] del módulo de una aplicación se ejecuta antes de cualquier instanciación de servicio. Se puede utilizar para definir información de configuración de los servicios que se van a instanciar a continuación. Aquí, la función [config] se utilizará para definir los mensajes internacionalizados de la aplicación;
- línea 5: el parámetro de la función [config] es una matriz ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)], donde Oi es un objeto conocido y proporcionado por Angular. Aquí, el objeto [$translateProvider] es proporcionado por el módulo [pascalprecht.translate]. [function] es la función que se ejecuta para configurar la aplicación;
- líneas 7-14: la función [$translateProvider.translations] admite dos parámetros:
- el primer parámetro es la clave de un idioma. Se puede poner lo que se quiera. Aquí hemos puesto «fr» para las traducciones al francés (línea 7) y «en» para las traducciones al inglés (línea 16),
- el segundo es la lista de traducciones en forma de diccionario {'cle1':'msg1', 'cle2':'msg2', ...};
- líneas 7-14: los mensajes en francés;
- líneas 16-23: los mensajes en inglés;
- línea 25: el método [preferredLanguage] establece el idioma por defecto. Su parámetro es uno de los argumentos utilizados como primer parámetro de la función [$translateProvider.translations], por lo que aquí será «fr» (línea 7) o «en» (línea 16);
- Cabe señalar que hay tres tipos de mensajes:
- mensajes sin parámetros ni elementos HTML (líneas 9, 11, 12, ...),
- mensajes con elementos HTML (líneas 8, 10, ...),
- mensajes con parámetros (líneas 10, 19);
Ahora duplicamos [app-11.html] en [app-12.html] y realizamos las siguientes modificaciones:
<div class="container">
<!-- un primer texto con elementos HTML en su interior -->
<h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
<!-- un segundo texto con parámetros -->
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
<!-- un tercer texto traducido por el controlador -->
<h3 class="alert alert-danger">{{msg2}}</h3>
<pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- el calendario-->
<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>
<!-- los 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>
- las traducciones se realizan en las líneas 3, 5, 9, 13 y 23;
- se pueden distinguir tres sintaxis:
- la sintaxis [translate={{'msg_key'}}] (línea 3), donde [msg_key] es una de las claves de un diccionario de traducción. Esta sintaxis es adecuada para mensajes con o sin elementos HTML, pero no para aquellos con parámetros;
- la sintaxis [translate={{'msg_key'}} translate-values={{dictionnaire]}}] (línea 5), adecuada para mensajes con o sin elementos HTML y con parámetros;
- la sintaxis [{{'msg_key'|translate}}] (líneas 9, 13, 23) es adecuada para mensajes sin parámetros y sin elementos HTML;
Veamos los diferentes mensajes de esta vista:
Consulta médica<br/>Les Médecins Associés | Los Médicos Asociados Doctors | |
Calendario | Calendar | |
Idiomas | Idiomas | |
Día seleccionado: | Seleccionado day: |
Veamos ahora la línea 5:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Cabe señalar que [msg.text] y [msg.model] no están entre comillas. No son cadenas de caracteres, sino elementos de la plantilla:
- msg.text: define la clave del mensaje parametrizado que se va a utilizar;
- msg.model: es el diccionario que proporciona los valores de los parámetros;
Los nombres de los campos [text, model] pueden ser cualquiera. En el controlador [rdvMedecinsCtrl] de la vista, el objeto [msg] se define de la siguiente manera:

- línea 245: la definición del objeto [msg];
- línea 245: el campo [text] tiene como valor la clave [msg_agenda], que está asociada a dos valores:
- Agenda de {{título}} {{nombre}} {{apellido}}<br/>el {{día}} en el diccionario francés;
- {{título}} {{nombre}} {{apellido}}'s Diary<br/> el {{día}} en el diccionario inglés;
Por lo tanto, el mensaje que se va a mostrar tiene cuatro parámetros [titre, prenom, nom, jour];
- línea 245: el campo [model] es un diccionario que asigna un valor a estos cuatro parámetros. Hay una dificultad con el parámetro [jour]. Queremos mostrar el nombre completo del día. Este varía según se trate de francés o inglés. Por lo tanto, utilizamos el filtro [date], ya utilizado en la vista en la forma {{ día | fecha:'fullDate'}}. Es posible utilizar cualquier filtro en el código Javascript en la forma $filter('filter')(valor, complementos), donde $filter es un objeto predefinido de Angular y 'filter' el nombre del filtro;
- líneas 33-34: el objeto predefinido $filter se pasa como parámetro al controlador, lo que permite utilizarlo en la línea 245;
Volvamos a otra línea de la vista mostrada:
<!-- un tercer texto traducido por el controlador -->
<h3 class="alert alert-danger">{{msg2}}</h3>
Todas las traducciones anteriores se han realizado en la vista mediante atributos del módulo [pascalprecht.translate]. También se puede optar por realizar esta traducción en el lado del servidor. Eso es lo que se hace aquí. En el controlador (línea 247 en la captura de pantalla anterior) tenemos el siguiente código:
$scope.msg2 = $filter('translate')('msg_meteo');
Se utiliza la misma sintaxis que para el filtro «date», ya que «translate» es también un filtro. Aquí se solicita el mensaje de clave «msg_meteo».
Examinemos el mecanismo de los cambios de idioma. Hemos visto que la función [config] de configuración del módulo [rdvmedecins] había designado el francés como idioma por defecto (línea 9 a continuación):
// configuración i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// mensajes en francés
$translateProvider.translations("fr", {...});
// mensajes en inglés
$translateProvider.translations("en", {...});
// idioma predeterminado
$translateProvider.preferredLanguage("fr");
}]);
Recordemos también que la configuración regional por defecto era igualmente el francés. En la inicialización del controlador [rdvmedecins] se escribió:
// se establece la configuración regional en francés
angular.copy(locales['fr'], $locale);
- línea 2: [locales] es un diccionario que hemos creado;
No hay ninguna relación entre la internacionalización de los mensajes que ofrece el módulo [pascalprecht.translate] y la localización de las fechas que hemos implementado. Esta última utiliza una variable $locale que no es utilizada por el módulo [pascalprecht.translate]. Son dos procesos que no interactúan entre sí.
Ahora es el momento de ver qué ocurre cuando el usuario cambia de idioma:

- línea 251: al cambiar de idioma, se llama a la función [setLang] con uno de los dos parámetros ['fr','en'];
- líneas 252-257: ya se han explicado; cambian la variable [$locale] del calendario. Esto no afecta al idioma de las traducciones;
- línea 259: se cambia el idioma de las traducciones. Se utiliza el objeto [$translate] proporcionado por el módulo [pascalprecht.translate]. Para ello, hay que inyectarlo en el controlador:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {
En las líneas 3 y 4 anteriores, se inyecta el objeto $translate;
- el parámetro lang de la función [$translate.use(lang)] debe tener como valor una de las claves utilizadas en la configuración como primer parámetro de la función [$translateProvider.translations], es decir, «fr» o «en». Así es;
- línea 261: se vuelve a calcular el valor de msg2. ¿Por qué? En la vista, tras el cambio de idioma realizado en la línea 259, todos los atributos [translate] presentes se volverán a evaluar. No será el caso de la expresión {{msg2}}, que no tiene este atributo. Por lo tanto, se calcula su nuevo valor en el controlador. Esto debe hacerse después del cambio de idioma de la línea 259 para que se utilice el nuevo idioma en el cálculo de [msg2];
Si nos quedamos ahí, observamos dos anomalías:
![]() |
- en [1], el día se ha mantenido en francés, mientras que el resto de la vista está en inglés;
- en [2] y [3], el día seleccionado es el 24 de junio, mientras que en [1], el día sigue fijado en el 20 de junio;
Intentemos buscar explicaciones antes de encontrar soluciones. El mensaje [1] se genera en el controlador con el siguiente código:
$scope.msg = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom': 'PELISSIER', 'jour': $filter('date')($scope.jour, 'fullDate')}};
y se muestra en la vista con el siguiente código:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
La anomalía [1] (el día se ha mantenido en francés mientras que el resto de la vista está en inglés) parece indicar que, si el atributo [translate] se reevalúa al cambiar de idioma, no ha sido así con el atributo [translate-values]. Por lo tanto, podemos forzar esta reevaluación en el controlador:
// ------------------- gestor de eventos
// cambio de idioma
$scope.setLang = function (lang) {
...
// se actualiza msg2
$scope.msg2 = $filter('translate')('msg_meteo');
// y el día de msg
$scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};
Cada vez que se cambia de idioma, la línea 8 anterior reevalúa el día mostrado. Esto resuelve efectivamente el primer problema, pero no el segundo (el día mostrado en el mensaje no cambia cuando se selecciona otro día en el calendario). La razón de este comportamiento es la siguiente. El mensaje se muestra en la vista con el siguiente código:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
La vista mostrada V solo cambia si cambia su modelo M. Sin embargo, en este caso, la selección de un nuevo día en el calendario desencadena un evento que no está gestionado, lo que hace que el modelo [msg] no cambie y, por lo tanto, la vista tampoco cambie. Actualizamos en la vista la definición del calendario:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>
Arriba, indicamos que el clic en el calendario debe ser gestionado por la función [$scope.calendarClick]. Esta es la siguiente:

- línea 267: el gestor del clic en el calendario;
- línea 269: se fuerza la actualización del día mostrado mediante el mensaje [msg];
3.7.4. Ejemplo 4: un servicio de configuración
Volvamos a la arquitectura de una aplicación Angular JS:
![]() |
Aquí nos centraremos en el concepto de servicio. Se trata de un concepto bastante amplio. Si bien, en el ejemplo anterior, la capa [DAO] es claramente un servicio, cualquier objeto Angular puede convertirse en un servicio:
- un servicio sigue una sintaxis concreta. Tiene un nombre y Angular lo reconoce a través de ese nombre;
- Angular puede inyectar un servicio en los controladores y en otros servicios;
Algunos de los servicios que vamos a configurar en el módulo [rdvmedecins] necesitarán ser configurados. Dado que un servicio puede inyectarse en otro servicio, resulta tentador realizar la configuración en un servicio que llamaremos [config] e inyectar este último en los servicios y controladores que hay que configurar. A continuación describimos este proceso.
Duplicamos [app-13.html] en [app-14.html] y realizamos las siguientes modificaciones:
<div class="container">
<!-- control del mensaje en espera -->
<label>
<input type="checkbox" ng-model="waiting.visible">
<span>Voir le message d'attente</span>
</label>
<!-- el mensaje en 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>
- líneas 3-6: una casilla de verificación que controla si se muestra o no el mensaje de espera de las líneas 9-15. El valor de la casilla de verificación se coloca en la variable [waiting.visible] del modelo M de la vista V. Este valor es true si la casilla está marcada y false en caso contrario. Esto funciona en ambos sentidos. Si asignamos el valor true a la variable [waiting.visible], la casilla quedará marcada. Tenemos una asociación bidireccional entre la vista V y su modelo M;
- líneas 9-15: un mensaje de espera con un botón para cancelar la espera (línea 11);
- línea 9: el mensaje solo es visible si la variable [waiting.visible] tiene el valor «true». Así, cuando marcamos la casilla de la línea 4:
- el valor true se asigna a la variable [waiting.visible] (ng-model, línea 4);
- como se ha producido un cambio en el modelo M, la vista V se reevalúa automáticamente. El mensaje de espera se hará entonces visible (ng-show, línea 9);
- el razonamiento es análogo cuando se desmarca la casilla de la línea 4: el mensaje de espera se oculta;
- línea 10: el mensaje de espera se traduce (filtro translate);
- línea 11: al hacer clic en el botón, se ejecuta el método [waiting.cancel()] (atributo ng-click);
- línea 12: el texto del botón se traduce;
- línea 19: se coloca el código Javascript de la aplicación en un nuevo archivo JS [rdvmedecins-02] para no perder el código ya escrito y que ahora debe reorganizarse;
Esto da como resultado la siguiente vista:
![]() |
- en [1], casilla sin marcar;
- en [2], casilla marcada;
El script [rdvmedecins-02] es una reorganización del script [rdvmedecins]:

- línea 6: el módulo [rdvmedecins] de la aplicación;
- líneas 9-10: la función de configuración de la aplicación;
- líneas 38-39: el servicio [config];
- líneas 283-284: el controlador [rdvMedecinsCtrl];
Anteriormente, habíamos definido en el controlador el diccionario locales={'fr':..., 'en': ...}, que ocupaba 200 líneas. Este diccionario es claramente un elemento de configuración, por lo que lo migramos al servicio [config] de las líneas 38-39. Este servicio se define de la siguiente manera:

- líneas 38-39: se crea un servicio con la función [factory] del objeto [angular.module]. La sintaxis de esta función es similar a la de las anteriores: factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){...}]), donde los Oi son los nombres de objetos conocidos por Angular (predefinidos o creados por el desarrollador) y que Angular inyecta como parámetro de la función factory. Como en este caso la función no tiene parámetros, se ha utilizado una sintaxis más corta que también es válida: factory('nom_service', function (){...}]);
- línea 40: la función [factory] debe implementar el servicio mediante un objeto que devuelve. Este objeto es el servicio. Por eso la función se llama factory (fábrica de creación de objetos);
En general, el código de un servicio tiene la forma:
Angular.module('nom_module')
.factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
// preparación del servicio
...
// se devuelve el objeto que implementa el servicio
return {
// campos
...
// métodos
...
}
});
- línea 6: se devuelve un objeto JS que puede contener tanto campos como métodos. Son estos últimos los que prestan el servicio;
Aquí, el servicio [config] solo define campos y ningún método. En él se incluirá todo lo que se pueda configurar en la aplicación:
- líneas 42-47: las claves de los mensajes que se van a traducir;
- líneas 59-62: los URL de la aplicación;
- líneas 64-69: los URL del servicio web remoto;
- línea 71: una llamada HTTP a un servicio web que no responde, puede ser long. Aquí se establece en 1 segundo el tiempo máximo de espera para la respuesta del servicio web. Transcurrido este tiempo, la llamada HTTP falla y se lanza una excepción JS;
- línea 73: antes de cada llamada al servidor, se simulará una espera cuya duración se fija aquí en milisegundos. Una espera de 0 significa que no hay espera. La aplicación se diseñará de tal forma que el usuario pueda cancelar una operación que haya iniciado. Para que pueda cancelarse, debe durar al menos unos segundos. Utilizaremos esta espera artificial para simular operaciones largas;
- línea 75: en el modo [debug=true], se muestra información adicional en la vista actual. Por defecto, este modo está activado. En producción, se pondría este campo en false;
- líneas 77-278: el diccionario de las dos configuraciones regionales «fr» y «en». Anteriormente se encontraba en el controlador [rdvMedecinsCtrl];
Con este servicio, el controlador [rdvMedecinsCtrl] evoluciona de la siguiente manera:

- líneas 284-285: el servicio [config] se inyecta en el controlador;
- línea 290: el diccionario [locales] se encuentra ahora en el servicio [config] y ya no en el controlador;
- línea 294: el objeto [waiting] que controla la visualización del mensaje de espera. La clave del mensaje de espera se encuentra en el servicio [config] (campo text). Por defecto, el mensaje de espera está oculto (campo visible). El campo cancel tiene como valor el nombre de la función de la línea 316. Por lo tanto, este campo es un método o una función;
- línea 316: la función [cancel] es privada (no se ha escrito $scope.cancel=function(){}). Volvamos al código del botón de cancelación:
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
Cuando el usuario hace clic en el botón de cancelación, se llama al método [$scope.waiting.cancel()]. Al final, lo que se ejecuta es la función privada cancel de la línea 316. Esta se limita a ocultar el mensaje de espera estableciendo en false la variable del modelo [waiting.visible] (línea 318);
3.7.5. Ejemplo 5: programación asíncrona
Presentamos ahora un nuevo servicio con un nuevo concepto: el de la programación asíncrona.
![]() |
Nuestra aplicación contará con tres servicios:
- [config]: el servicio de configuración que acabamos de presentar;
- [utils]: un servicio de métodos utilitarios. Vamos a presentar dos;
- [dao]: el servicio de acceso al servicio web de gestión de citas. Lo presentaremos próximamente;
Vamos a escribir la siguiente aplicación:
![]() |
![]() |
- Se trata de mostrar el banner [2] durante un tiempo fijado por [1]. La espera se puede cancelar mediante [3].
Duplicamos [app-01.html] en [app-15.html] y modificamos el código de la siguiente manera:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<!-- el mensaje 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>
<!-- el formulario -->
<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>
- línea 11: el atributo [ng-cloak] impide que se muestre el campo antes de que se hayan calculado sus expresiones Angular. Esto evita que el campo se muestre brevemente antes de que se evalúe el atributo [ng-show], que es el que realmente provocará su ocultación;
- línea 22: la entrada del usuario (tiempo de espera) se almacenará en el modelo [waiting.time] (atributo ng-model);
- línea 28: la página utiliza un nuevo script [rdvmedecins-03];
El script [rdvmedecins-03] es el siguiente:

- línea 6: el módulo Angular que gestiona la aplicación;
- línea 10: la función [config] utilizada para internacionalizar los mensajes;
- línea 41: el servicio [config] que hemos descrito;
- línea 286: el servicio [utils] que vamos a crear;
- línea 315: el controlador [rdvmedecinsCtrl] que vamos a crear;
Añadimos a la función [config] una nueva clave de mensaje (líneas 6, 11):
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// mensajes en francés
$translateProvider.translations("fr", {
...
'msg_waiting_time_text': "Temps d'attente : "
});
// mensajes en inglés
$translateProvider.translations("en", {
...
'msg_waiting_time_text': "Waiting time:"
});
// idioma predeterminado
$translateProvider.preferredLanguage("fr");
}]);
Añadimos al servicio [config] una nueva línea (línea 6) para esta clave de mensaje:
angular.module("rdvmedecins")
.factory('config', function () {
return {
// mensajes para internacionalizar
...
waitingTimeText: 'msg_waiting_time_text',
El servicio [utils] contiene dos métodos (líneas 4, 12):
angular.module("rdvmedecins")
.factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
// visualización de la representación Json de un objeto
function debug(message, data) {
if (config.debug) {
var text = data ? message + " : " + angular.toJson(data) : message;
console.log(text);
}
}
// espera
function waitForSomeTime(milliseconds) {
// espera asíncrona de milisegundos
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// se devuelve la tarea
return task;
};
// instancia del servicio
return {
debug: debug,
waitForSomeTime: waitForSomeTime
}
}]);
- línea 2: el servicio se llama [utils] (primer parámetro). Tiene dependencias de tres servicios: dos servicios Angular predefinidos, $timeout y $q, y el servicio config. El servicio [$timeout] permite ejecutar una función tras un tiempo determinado. El servicio [$q] permite crear tareas asíncronas;
- línea 4: una función local [debug];
- línea 12: una función local [waitForSomeTime];
- líneas 23-26: la instancia del servicio [utils]. Se trata de un objeto que expone dos métodos, los de las líneas 4 y 12. Tenga en cuenta que los campos del objeto pueden tener cualquier nombre. Por coherencia, se les han dado los nombres de las funciones a las que hacen referencia;
- líneas 4-9: el método [debug] escribe en la consola un mensaje [message] y, eventualmente, la representación JSON de un objeto [data]. Esto permite mostrar objetos de cualquier complejidad;
- líneas 12-20: el método [waitForSomeTime] crea una tarea asíncrona que dura [milliseconds] milisegundos;
- línea 14: creación de una tarea mediante el objeto predefinido [$q] (https://docs.angularjs.org/api/ng/service/$q). A continuación, el API de la tarea denominada [deferred] en la documentación de Angular:

- la instrucción [$q.defer()] crea una tarea asíncrona [task];
- se finaliza mediante uno de los dos métodos:
- [task.resolve(value)]: que finaliza la tarea con éxito y devuelve el valor [value] a quienes esperan el final de la tarea;
- [task.reject(value)]: que finaliza la tarea con error y devuelve el valor [value] a quienes esperan a que termine la tarea;
La tarea [task] puede proporcionar información periódicamente a quienes esperan su finalización:
- [task.notify(value)]: envía el valor [value] a quienes esperan a que finalice la tarea. La tarea continúa ejecutándose;
Quienes deseen esperar a que finalice la tarea utilizan el campo [promise] de la misma:
El objeto [promise] tiene el siguiente API (http://www.frangular.com/2012/12/api-promise-angularjs.html):

Para gestionar tanto el éxito como el fracaso de la tarea, se escribirá:
- línea 1: se recupera la promesa de la tarea;
- línea 2: se definen las funciones que se ejecutarán en caso de éxito o de fallo. Se puede omitir la función de fallo. La función [successCallback] solo se ejecutará al final de la tarea [task] con éxito [task.resolve()]. La función [errorCallBack] solo se ejecutará al finalizar la tarea [task] con error [task.reject()].
- Línea 3: se define la función que se ejecutará después de que se haya ejecutado una de las dos funciones anteriores. Aquí se coloca el código común a las dos funciones [successCallback, errorCallBack].
Volvamos al código de la función [waitForSomeTime]:
// espera
function waitForSomeTime(milliseconds) {
// espera asíncrona de milisegundos
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// se devuelve la tarea
return task;
};
- línea 4: se crea una tarea;
- líneas 5-7: el objeto [$timeout] permite definir una función (primer parámetro) que se ejecuta tras un determinado intervalo de tiempo expresado en milisegundos (segundo parámetro). Aquí, el segundo parámetro de la función [$timeout] es el parámetro del método (línea 1);
- línea 6: una vez transcurrido el tiempo de espera [milliseconds], la tarea finaliza con éxito;
- línea 9: se devuelve la tarea [task]. Hay que entender aquí que la línea 9 se ejecuta inmediatamente después de la definición del objeto [$timeout]. No se espera a que haya transcurrido el plazo [milliseconds]. Por lo tanto, el código de las líneas 2-10 se ejecuta en dos momentos diferentes:
- una primera vez, al definir el objeto [$timeout];
- una segunda vez cuando ha transcurrido el tiempo de espera [milliseconds];
Aquí tenemos una función asíncrona: su resultado se obtiene en un momento posterior al de su ejecución.
El código del controlador que utiliza el servicio [config] es el siguiente:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
function ($scope, utils, config, $filter) {
// ------------------- inicialización del modelo
// mensaje de espera
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
// tarea en espera
var task;
// registros
utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
utils.debug("locales['fr']=", config.locales['fr']);
// ejecución de acción
$scope.execute = function () {
// registro
utils.debug('début', new Date());
// se muestra el mensaje de espera
$scope.waiting.visible = true;
// espera simulada
task = utils.waitForSomeTime($scope.waiting.time);
// fin de la espera
task.promise.then(function () {
// éxito
utils.debug('fin', new Date());
}, function () {
// error
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// fin de la espera en todos los casos
$scope.waiting.visible = false;
});
};
// cancelación de la espera
function cancel() {
// se termina la tarea
task.reject();
}
}]);
- línea 3: el controlador utiliza el servicio [config];
- línea 7: se ha añadido el campo [time] al objeto [$scope.waiting]. El objeto [$scope.waiting.time] recibe el valor del tiempo de espera establecido por el usuario;
- línea 8: la clave del mensaje de espera mostrado por la vista se coloca en el modelo [$scope.waitingTimeText]. En general, todo lo que muestra una vista V debe colocarse en el objeto [$scope];
- línea 10: una variable local. No está expuesta a la vista V;
- líneas 12-13: uso del método [debug] del servicio [config]. Se obtiene el siguiente resultado en la consola:
En la línea 2, se obtiene la notación JSON del objeto locales['fr'].
- línea 16: el método que se ejecuta cuando el usuario hace clic en el botón [Executer];
- línea 18: muestra la hora de inicio de la ejecución del método;
- línea 22: se inicia la tarea [waitForSomeTime]. No se espera a que finalice. La ejecución continúa con la siguiente línea 24;
- líneas 24-30: se definen las funciones que se ejecutarán cuando la tarea finalice con éxito (línea 26) y en caso de error (línea 29);
- línea 26: muestra la hora de finalización de la ejecución del método;
- línea 29: muestra que la operación se ha cancelado. Esto solo ocurre cuando el usuario hace clic en el botón [Annuler]. La instrucción de la línea 41 detiene entonces la tarea asíncrona con un código de error;
- líneas 31-34: se define la función que se ejecutará tras la ejecución de una de las dos funciones anteriores;
Es importante comprender las secuencias de ejecución de este código. En el caso de que el usuario establezca un tiempo de espera de 3 segundos y no cancele la espera:
- al hacer clic en el botón [Exécuter], se ejecuta la función [$scope.execute]. Las líneas 16-34 se ejecutan sin esperar los 3 segundos. Al final de esta ejecución, la vista V se sincroniza con el modelo M. Se muestra el mensaje de espera (ng-show=$scope.waiting.visible=true, línea 20) y se oculta el formulario (ng-hide=$scope.waiting.visible=true, línea 20);
- a partir de ese momento, el usuario puede volver a interactuar con la vista. En particular, puede hacer clic en el botón [Annuler];
- si no lo hace, al cabo de 3 segundos, se ejecuta la función de [$timeout] (véanse las líneas 5-7 a continuación):
// espera
function waitForSomeTime(milliseconds) {
// espera asíncrona de milisegundos milisegundos
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// se devuelve la tarea
return task;
};
- por lo tanto, al cabo de 3 segundos, se ejecuta el código. Este código finaliza la tarea [task] con un código de éxito (resolve). Esto activará la ejecución de todos los códigos que esperaban este final (línea 4 a continuación):
// espera simulada
task = utils.waitForSomeTime($scope.waiting.time);
// fin de la espera
task.promise.then(function () {
// éxito
utils.debug('fin', new Date());
}, function () {
// fallo
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// fin de la espera en todos los casos
$scope.waiting.visible = false;
});
- Por lo tanto, se ejecutará la línea 6 anterior (finalización con éxito). A continuación, será el turno de las líneas 11-14. Una vez ejecutado este código, se vuelve a la vista V, que se sincronizará con su modelo M. El mensaje de espera se oculta (ng-show=$scope.waiting.visible=false, línea 13) y se muestra el formulario (ng-hide=$scope.waiting.visible=false, línea 13);
Las pantallas que se muestran son las siguientes:
Como se ve arriba, el intervalo de 3 segundos (06:01-05:58) entre el inicio y el final de la espera. Si, por el contrario, el usuario cancela la espera antes de los 3 segundos, se muestra lo siguiente:
Para terminar, es importante comprender que en todo momento solo hay un hilo de ejecución denominado hilo de UI (interfaz de usuario). El final de una tarea asíncrona se señala mediante un evento, exactamente igual que cuando se hace clic en un botón. Este evento no se procesa inmediatamente. Se coloca en la cola de eventos que esperan su ejecución. Cuando llega su turno, se procesa. Este procesamiento utiliza el hilo de UI y, por lo tanto, durante ese tiempo, la interfaz se congela. No reacciona a las solicitudes del usuario. Por ello, es importante que el procesamiento de un evento sea rápido. Dado que cada evento es procesado por el hilo de UI, nunca hay que resolver problemas de sincronización entre hilos que se ejecutan al mismo tiempo. En cada momento, solo se ejecuta el hilo de UI.
3.7.6. Ejemplo 6: los servicios HTTP
A continuación, presentamos el servicio [dao] que se comunica con el servidor web:
![]() |
3.7.6.1. La vista V
![]() |
Vamos a escribir un formulario para solicitar la lista de médicos:

Duplicamos [app-01.html] en [app-16.html], que luego modificamos de la siguiente manera:
<div class="container" ng-cloak="">
<h1>Rdvmedecins - v1</h1>
<!-- el mensaje 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>
<!-- la solicitud -->
<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>
<!-- la 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>
<!-- la lista de errores -->
<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>
- líneas 13-31: implementan el formulario. Este no es visible cuando se muestra el mensaje de espera (ng-hide="waiting.visible"). Cabe destacar que los cuatro campos de entrada se almacenan en (atributos ng-model) [waiting.time (ligne 16), server.url (ligne 20), server.login (ligne 24), server.password (ligne 28)];
- líneas 34-39: muestran la lista de médicos. Esta lista no siempre es visible (ng-show="medecins.show").
- línea 35: una alternativa a la sintaxis ya vista anteriormente;
- línea 36: una lista no ordenada;
- línea 37: la lista de médicos se encuentra en la plantilla [medecins.data]. La directiva Angular [ng-repeat] permite recorrer una lista. La sintaxis ng-repeat="medecin in medecins.data» indica que la etiqueta <li> se repita para cada elemento de la lista [medecins.data]. El elemento actual de la lista se denomina [medecin];
- línea 37: para cada <li>, se escribe el título, el nombre y los apellidos del médico actual designado por la variable [medecin];
- líneas 42-47: muestran la lista de errores. Esta lista no siempre está visible (ng-show="errors.show"). Esta visualización sigue el mismo patrón que la visualización de la lista de médicos. En general, para mostrar una lista de objetos, se utiliza la directiva Angular [ng-repeat];
- línea 51: el código Javascript se encuentra ahora en el archivo [rdvmedecins-04]
3.7.6.2. El controlador C y el modelo M
![]() |
El código Javascript evoluciona de la siguiente manera:

- líneas 6-9: el módulo [rdvmedecins] declara una dependencia del módulo [base64] proporcionado por la biblioteca [angular-base64], que es una de las dependencias del proyecto. Este módulo sirve para codificar en Base64 la cadena [login:password] enviada al servicio web para autenticarse;
- líneas 12-13: la función de inicialización que contiene nuestros mensajes internacionalizados. Aparecen nuevos mensajes. No los presentaremos de nuevo;
- líneas 69-70: el servicio [config] que configura nuestra aplicación. Se añaden nuevas claves de mensaje. No las presentaremos de nuevo;
- líneas 318-319: el servicio [utils], que contiene métodos de utilidad. Se añaden otros nuevos. Los presentaremos;
- líneas 385-386: el servicio [dao], encargado de las comunicaciones con el servicio web. Es en él en quien nos vamos a centrar;
- líneas 467-468: el controlador C de la vista V que acabamos de presentar. Lo presentaremos ahora, ya que es él quien dirige el conjunto y responde a las solicitudes del usuario;
3.7.6.3. El controlador C
El código del controlador es el siguiente:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- inicialización de la plantilla
// plantilla
$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;
// tarea asíncrona
var task;
// ejecución de la acción
$scope.execute = function () {
// se actualiza el 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 () {
// se solicita la lista de médicos;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
// se analiza el resultado de la llamada anterior
promise.then(function (result) {
// resultado={err: 0, data: [med1, med2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
if (result.err == 0) {
// se introducen los datos adquiridos en el modelo
$scope.medecins.data = result.data;
// se actualiza el UI
$scope.medecins.show = true;
$scope.waiting.visible = false;
} else {
// se han producido errores al obtener la lista de médicos
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
// se actualiza el UI
$scope.waiting.visible = false;
}
});
};
// cancelación en espera
function cancel() {
// se finaliza la tarea
task.reject();
// se actualiza el UI
$scope.waiting.visible = false;
$scope.medecins.show = false;
$scope.errors.show = false;
}
}
])
;
- línea 2: el controlador tiene una nueva dependencia, la del servicio [dao];
- líneas 6-13: el modelo M de la vista V se inicializa para su primera visualización;
- línea 8: [$scope.server] se utilizará para recuperar tres de los cuatro datos del formulario V, quedando el cuarto almacenado en [$scope.waiting.time] (línea 6);
- línea 9: [$scope.medecins] recopilará la información necesaria para mostrar la lista de médicos:
<!-- la 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>
El atributo [medecins.title] será el título del banner. Se define en el servicio [config]. El atributo [medecins.show] controlará si se muestra o no el banner (atributo ng-show="medecins.show"). El atributo [medecins.model] es un diccionario vacío y seguirá siéndolo. Simplemente sirve para ilustrar el uso de la variante de traducción utilizada en la línea 3. Aún sin definir, el atributo [medecins.data] contendrá la lista de médicos (línea 5).
- línea 10: [$scope.errors] recopilará la información necesaria para mostrar la lista de errores:
<!-- la lista de errores -->
<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>
El atributo [errors.title] será el título del banner. Se define en el servicio [config]. El atributo [errors.show] controlará si se muestra o no el banner (atributo ng-show="errors.show"). El atributo [errors.model] es un diccionario vacío y seguirá siéndolo. Simplemente sirve para ilustrar el uso de la variante de traducción utilizada en la línea 3. Aún sin definir, el atributo [errors.messages] contendrá la lista de mensajes de error que se mostrarán (línea 5).
- línea 16: la tarea asíncrona. El controlador iniciará sucesivamente dos tareas asíncronas. Las referencias a estas tareas sucesivas se colocarán en la variable [task]. Esto permitirá cancelarlas (línea 55);
- línea 19: el método que se ejecuta cuando el usuario hace clic en el botón [Liste des médecins]:
<button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
- líneas 21-23: se actualiza la interfaz visual: se muestra el mensaje de espera y se oculta todo lo demás;
- línea 25: se crea la tarea asíncrona de espera. Se recibirá una señal (tarea completada) una vez transcurrido el tiempo introducido por el usuario en el formulario;
- línea 26: se recupera la promesa de la tarea asíncrona. Es con ella con la que trabaja el programa que inicia la tarea. Sin embargo, es necesario disponer de la referencia de la propia tarea para poder cancelarla (línea 55);
- líneas 28-32: se define el trabajo que hay que realizar cuando finalice la espera;
- línea 30: se utiliza el método [dao.getData] para iniciar una nueva tarea asíncrona. Se le pasan los datos que necesita:
- la raíz URL del servicio web [$scope.server.url], por ejemplo, [http://localhost:8080];
- el nombre de usuario [$scope.server.login] para identificarse, por ejemplo, [admin];
- la contraseña [$scope.server.password] para identificarse, por ejemplo, [admin];
- el URL que devuelve el servicio solicitado [config.urlSvrMedecins], en este caso [/getAllMedecins]. En total, el URL completo será [http://localhost:8080/getAllMedecins];
El método [dao.getData] devuelve un resultado que puede adoptar dos formas posibles:
- (continuación)
- {err: 0, data: [med1, med2, ...]} donde [medi] es un objeto que representa a un médico (título, nombre, apellidos),
- {err: n, mensajes: [msg1, msg2, ...]} donde [msgi] es un mensaje de error y n es distinto de 0;
- línea 31: se devuelve la promesa de la tarea. Aquí hay algo que hay que entender. Tenemos dos promesas:
- promise.then(): devuelve una primera promesa [promise1];
- return task.promise: devuelve una segunda promesa [promise2];
- al final, promise=promise.then(... ;return task.promise) es una cadena de dos promesas [promise2.promise1]. [promise1] solo se evaluará cuando se obtenga la promesa [promise2], es decir, cuando finalice la tarea [dao.getData]. La promesa [promise1] no depende de ninguna tarea asíncrona. Por lo tanto, se obtendrá inmediatamente;
- líneas 34-50: de la explicación anterior se deduce que estas líneas solo se ejecutarán cuando la tarea [dao.getData] haya finalizado. El parámetro [result] pasado a la función de la línea 34 se construye mediante el método [dao.getData] y se transmite al código llamante mediante la operación [task.resolve(result)], donde [result] tiene la siguiente forma:
- {err: 0, data: [med1, med2, ...]}, donde [medi] es un objeto que representa a un médico (título, nombre, apellidos),
- {err: n, mensajes: [msg1, msg2, ...]}, donde [msgi] es un mensaje de error y n es distinto de 0;
- línea 37: se comprueba el código de error [result.err];
- líneas 38-42: si no hay error (result.err==0), se recupera la lista de médicos y se muestra;
- líneas 44-47: si, por el contrario, hay un error (result.err != 0), se recupera la lista de mensajes de error y se muestra;
- líneas 53-56: el mensaje de espera con su botón de cancelación permanece visible mientras las dos operaciones asíncronas no hayan finalizado. Veamos qué ocurre según el momento de la cancelación:
- en primer lugar, hay que entender que las líneas 19-50 se ejecutan de una sola vez. Solo se ha iniciado una tarea asíncrona, la de la línea 25,
- tras esta primera ejecución, se actualiza la vista V y, por lo tanto, se muestra la barra de espera y su botón de cancelación. Si el usuario cancela la espera antes de que finalice la tarea de la línea 25, se ejecuta el método de la línea 53 y la tarea se cancela con error (línea 55);
- líneas 56-59: se actualiza la interfaz: se vuelve a mostrar el formulario y se oculta todo lo demás,
- luego se vuelve a la vista V y el navegador procesa el siguiente evento. Dado que la tarea ha finalizado, se obtiene la promesa de esta tarea, lo que crea un evento. A continuación, se procesa;
- a continuación se ejecutan las líneas 28-32. No hay ninguna función definida para el caso de fallo, por lo que no se ejecuta ningún código. Se obtiene una nueva promesa, la que siempre devuelve [promise.then] y que siempre se obtiene,
- una vez procesado el evento, se vuelve a la vista V y el navegador procesará el siguiente evento. Dado que se ha procesado la [promise] de la línea 28, se resolverá la de la línea 34, lo que provocará un nuevo evento. A continuación, se procesa;
- las líneas 34-49 se ejecutarán a su vez, ya que se ha obtenido la promesa utilizada en la línea 34. De nuevo, como no hay ninguna función definida para el caso de fallo, no se ejecuta ningún código,
- llegamos así a la línea 50. Ya no hay ninguna tarea en espera y se muestra la nueva vista V;
- supongamos ahora que la cancelación se produce mientras se está ejecutando la segunda tarea asíncrona [dao.getData]. El razonamiento anterior se puede aplicar de nuevo. El final de la tarea provocará la ejecución de las líneas 34-50 con un final de tarea con fallo. Pronto descubriremos que el método [dao.getData] realiza una llamada asíncrona HTTP al servicio web. Esta llamada no se cancelará, pero su resultado no se utilizará.
Es importante comprender este vaivén constante entre la visualización de la vista V y el procesamiento de los eventos del navegador. Los eventos son provocados por el usuario (un clic) o por operaciones del sistema, como la finalización de una operación asíncrona. El estado de reposo del navegador es la visualización de la vista V. Sale de este reposo por un evento que se produce y que entonces procesa. Tan pronto como se ha procesado el evento, vuelve a su estado de reposo. La vista V se actualiza entonces si el evento procesado ha modificado su modelo M. El navegador sale de su estado de reposo por el siguiente evento.
Todo ocurre en un único hilo. Dos eventos nunca se procesan simultáneamente. Su ejecución es secuencial. El navegador pasa al siguiente evento solo cuando el anterior le cede el paso, por lo general porque se ha procesado por completo.
Nos queda un punto por explicar. Para mostrar los mensajes de error, escribimos:
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
La lista de mensajes la proporciona el método [utils.getErrors] definido en el servicio [utils]. Este método es el siguiente:
// análisis de errores en la respuesta del servidor JSON
function getErrors(data) {
// data {err:n, mensajes:[]}, err!=0
// errores
var errors = [];
// código de error
var err = data.err;
switch (err) {
case 2 :
// not autorizado
errors.push('not_authorized');
break;
case 3 :
// prohibido
errors.push('forbidden');
break;
case 4 :
// error local
errors.push('not_http_error');
break;
case 6 :
// documento no encontrado
errors.push('not_found');
break;
default :
// otros casos
errors = data.messages;
break;
}
// si no hay mensaje, se inserta uno
if (! errors || errors.length == 0) {
errors=['error_unknown'];
}
// se muestra la lista de errores
return errors;
}
- líneas 2-3: el parámetro [data] recibido es un objeto con dos atributos:
- [err]: un código de error;
- [messages]: una lista de mensajes;
- línea 5: vamos a crear una tabla de mensajes de error. Estos mensajes están internacionalizados. Por este motivo, no son los mensajes en sí los que se introducen en la tabla, sino sus claves de internacionalización, salvo en la línea 27. En este caso, se utiliza el atributo [messages] del parámetro [data]. Estos mensajes son mensajes reales y no claves de mensaje. Sin embargo, la vista V los tratará como claves de mensaje que, por lo tanto, no se encontrarán. En este caso, el módulo [translate] muestra la clave de mensaje que no ha encontrado, es decir, aquí un mensaje real. Este es el resultado deseado;
- líneas 32-34: tratan el caso en el que [data.messages], línea 27, es igual a null. Esto ocurre con el servicio web escrito. Habría que haber evitado este caso.
3.7.6.4. El servicio [dao]
![]() |
El servicio [dao] gestiona los intercambios HTTP con el servicio web / JSON. Su código es el siguiente:
angular.module("rdvmedecins")
.factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
function ($http, $q, config, $base64, utils) {
// registros
utils.debug("[dao] init");
// ----------------------------------métodos privados
// obtener datos del servicio web
function getData(serverUrl, username, password, urlAction, info) {
// operación asíncrona
var task = $q.defer();
// url solicitud HTTP
var url = serverUrl + urlAction;
// autenticación básica
var basic = "Basic " + $base64.encode(username + ":" + password);
// la respuesta
var réponse;
// las solicitudes http deben estar todas autenticadas
var headers = $http.defaults.headers.common;
headers.Authorization = basic;
// se realiza la solicitud 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);
// se devuelve la propia tarea para que pueda cancelarse
return task;
// éxito
function success(response) {
// response.data={status:0, data:[med1, med2, ...]} o {status:x, data:[msg1, msg2, ...]
utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
// respuesta
var payLoad = response.data;
réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
// se devuelve la respuesta
task.resolve(réponse);
}
// error
function failure(response) {
utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
// se analiza el estado
var status = response.status;
var error;
switch (status) {
case 401 :
// no autorizado
error = 2;
break;
case 403:
// prohibido
error = 3;
break;
case 404:
// not encontrado
error = 6;
break;
case 0:
// error local
error = 4;
break;
default:
// otra cosa
error = 5;
}
// se devuelve la respuesta
task.resolve({err: error, messages: [response.statusText]});
}
}
// --------------------- instancia del servicio [dao]
return {
getData: getData
}
}]);
- líneas 77-79: el servicio solo tiene un único campo: el método [getData], que permite obtener información del servicio web / JSON;
- línea 2: aparece una dependencia [$http] que aún no habíamos visto. Se trata de un servicio predefinido de Angular que permite el diálogo HTTP con una entidad remota;
- línea 6: un registro para ver en qué momento del ciclo de vida de la aplicación se ejecuta el código;
- línea 10: el método [getData] admite cinco parámetros:
- [serverUrl]: la URL raíz del servicio web (http://localhost:8080);
- [urlAction]: la ruta del servicio específico solicitado (/getAllMedecins);
- [username]: el nombre de usuario;
- [password]: su contraseña;
- [info]: objeto que recopila información complementaria cuando se solicita el URL del servicio específico solicitado mediante una operación POST. En el caso del URL (/getAllMedecins), este parámetro no se ha pasado. Por lo tanto, es [undefined];
- línea 12: se crea una tarea asíncrona;
- línea 14: el URL completa el servicio solicitado (http://localhost:8080/getAllMedecins);
- línea 16: la autenticación se realiza enviando el siguiente encabezado:
donde [code] es el código Base64 de la cadena [username:password];
La línea 16 construye la parte [Basic code] del encabezado HTTP;
- línea 18: la respuesta del servicio web;
- línea 20: los encabezados HTTP enviados por defecto por Angular en una solicitud HTTP se definen en el objeto [$http.defaults.headers.common]. El encabezado [Authorization:Basic code] no forma parte de ellos;
- línea 21: se añade a los encabezados HTTP que se deben enviar sistemáticamente. A la izquierda de la asignación, tenemos el encabezado [Authorization] que hay que inicializar y, a la derecha, el valor del encabezado, en este caso el valor definido en la línea 16. Así, si escribimos:
Angular enviará el encabezado HTTP:
- línea 23: los métodos del servicio [$http] devuelven promesas. Estas se almacenarán en la variable [promise];
- línea 27: dado que aquí el parámetro [info] tiene el valor [undefined], se ejecuta la línea 27. Se solicita URL (http://localhost:8080/getAllMedecins) con un GET. Para no esperar demasiado tiempo, se establece un tiempo de espera máximo (timeout) para obtener la respuesta del servidor. Por defecto, este tiempo es de un segundo;
- línea 29: se definen los dos métodos que se ejecutarán cuando se obtenga la promesa:
- [success]: definido en la línea 34, es el método que se ejecutará cuando se obtenga la promesa tras el éxito de la tarea;
- [failure]: definido en la línea 45, es el método que se ejecutará cuando se obtenga la promesa tras el fallo de la tarea;
- ambos métodos (deberíamos decir funciones) se definen dentro de la función [getData]. Esto es posible en Javascript. Las variables definidas en [getData] son conocidas en las dos funciones internas [success, failure];
- línea 31: se devuelve la tarea creada en la línea 12. Hay que recordar aquí el código de llamada:
promise = promise.then(function () {
// se solicita la lista de médicos;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
En la línea 3 anterior, se recupera correctamente una tarea.
- línea 34: la función [success] se ejecuta más tarde, cuando la llamada HTTP finaliza con éxito. Este concepto de éxito está relacionado con la primera línea de una respuesta HTTP. Esta tiene la forma:
El código es un texto de tres dígitos que indica si la llamada se ha realizado con éxito o no. A grandes rasgos, se puede decir que los códigos 2xx y 3xx son códigos de éxito, mientras que los demás son códigos de error. El texto es una breve explicación. A continuación se muestran dos respuestas posibles, una en caso de éxito y otra en caso de error:
- línea 36: se muestra en la consola la respuesta del servidor. En el error [404 Not Found], se obtiene algo como:
[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}
En esta respuesta, solo utilizaremos los campos [data], [status] y [statusText].
- línea 38: recuperamos el campo [data] de la respuesta. Tendrá una de las siguientes formas:
- {status: 0, data: [med1, med2, ...]} donde [medi] es un objeto que representa a un médico (título, nombre, apellidos),
- {status: n, data: [msg1, msg2, ...]} donde [msgi] es un mensaje de error y n es distinto de 0;
![]() |

- línea 39: se construye la respuesta {0,data} o {n,mensajes}. La primera respuesta contiene los médicos en el campo [data]. La segunda indica un error que se ha producido en el servidor. Este lo ha gestionado, ha generado un código de error en [err] y una lista de mensajes de error en [data]. En ambos casos, devuelve un código HTTP 200 que indica que la orden HTTP se ha procesado por completo. Por eso, ambos casos se tratan en la misma función [success];
- línea 41: la tarea ha finalizado [task.resolve] y se devuelve una de las dos respuestas:
- {err: 0, data: [med1, med2, ...]}, donde [medi] es un objeto que representa a un médico (título, nombre, apellidos),
- {err: n, messages: [msg1, msg2, ...]} donde [msgi] es un mensaje de error y n es distinto de 0;
Es necesario relacionar este código con la forma en que se recupera esta respuesta en el código de llamada del controlador:
// se analiza el resultado de la llamada anterior
promise.then(function (result) {
// resultado={err: 0, data: [med1, med2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
...
}
La respuesta de [task.resolve(réponse)] se encuentra arriba en la variable [result].
- línea 45: la función [failure] cuando la tarea asíncrona finaliza con un error. Hay dos casos posibles:
- el servidor señala este fallo devolviendo un código que no es ni 2xx ni 3xx,
- Angular cancela la llamada HTTP. En ese caso, no hay llamada. Hay una excepción de Angular, pero el servidor no devuelve ningún código de error HTTP. Este es el caso, por ejemplo, si se proporciona un URL inválido que no se puede llamar;
- línea 46: se muestra la respuesta en la consola;
- línea 48: recordamos que la respuesta del servidor tiene el formato:
{"data":"...","status":404,"config":{...},"statusText":"Not Found"}
Línea 48: se recupera el atributo [status] anterior;
- líneas 50-70: a partir del código de error HTTP, se va a generar un nuevo código de error para ocultar a los códigos llamantes la naturaleza HTTP del método [dao.getData]. Se puede comprobar que, en el controlador que utiliza este método, nada hace suponer que haya una llamada HTTP en el método;
- línea 51: el error [401] corresponde a un fallo en la autenticación (contraseña incorrecta, por ejemplo),
- línea 55: el error [403] corresponde a una llamada no autorizada. El usuario se ha autenticado correctamente, pero no tiene los derechos suficientes para solicitar el URL que ha solicitado. Esto ocurrirá con el usuario [user / user]. Este existe en la base de datos, pero no tiene permiso para utilizar la aplicación. Solo el usuario [admin / admin] tiene ese permiso;
- línea 59: el error [404] corresponde a un URL no encontrado. El error puede tener varias causas:
- el usuario ha cometido un error al introducir el URL del servicio;
- el servicio web no se ha iniciado;
- el servicio web no ha respondido con la suficiente rapidez (tiempo de espera de un segundo por defecto);
- línea 63: el código de error HTTP 0 no existe. Nos encontramos en el caso en el que Angular no ha realizado la llamada HTTP solicitada porque el URL introducido por el usuario no es válido y no se puede llamar. Más adelante nos encontraremos con otros casos en los que Angular no ejecutará la llamada HTTP solicitada;
- línea 72: se completa la tarea con éxito (task.resolve) devolviendo una respuesta del tipo {err, messages}, donde la matriz [messages] está formada únicamente por el mensaje [response.statusText]. En caso de que Angular no haya realizado la llamada HTTP solicitada, tendremos una cadena vacía;
Ahora que tenemos una visión tanto global como detallada de la aplicación, podemos comenzar con las pruebas.
3.7.6.5. Pruebas de la aplicación - 1
Empecemos con entradas válidas:

![]() |
- en [1], ponemos 0 para que no haya espera;
- en [2], aparece un mensaje de error aunque las entradas sean correctas. No hemos mostrado los diferentes mensajes de error. El que se muestra en [2] es un mensaje genérico asociado al error 0, que corresponde a una excepción de Angular. Angular ha encontrado un problema que le ha impedido realizar una llamada HTTP. En estos casos, hay que consultar los registros de la consola Javascript. Hay dos formas de hacerlo:
- ejecutar [F12] en el navegador Chrome;
- utilizar la consola de Webstorm;
En la consola de Webstorm, encontramos varios mensajes, entre ellos este:
- línea 1: Angular señala un error al que volveremos más adelante;
- línea 2: el registro del método [dao.getData]. En él encontramos datos interesantes:
- [status] es igual a 0, lo que indica que no se ha producido ninguna llamada a HTTP. En consecuencia, [statusText] está vacío,
- [url] es igual a [http://localhost:8080/getAllMedecins], lo cual es correcto;
- el encabezado HTTP de autenticación [Authorization":"Basic YWRtaW46YWRtaW4=] también es correcto;
Entonces, ¿por qué no ha funcionado? La frase clave de los registros es [No 'Access-Control-Allow-Origin' header is present]. Para entenderla, hay que dar una explicación larga. Empecemos por repasar la arquitectura general de la aplicación cliente/servidor:

- las páginas HTML / CSS / JS de la aplicación Angular provienen del servidor [1];
- en [2], el servicio [dao] realiza una solicitud a otro servidor, el servidor [2]. Pues bien, esto lo prohíbe el navegador que ejecuta la aplicación Angular porque se trata de una vulnerabilidad de seguridad. La aplicación solo puede consultar al servidor del que proviene, es decir, el servidor [1];
De hecho, no es exacto decir que el navegador prohíbe a la aplicación Angular consultar al servidor [2]. En realidad, la consulta para preguntarle si autoriza a un cliente que no proviene de su propio dominio a consultarlo. A esta técnica de intercambio se le denomina CORS (Cross-Origin Resource Sharing). El servidor [2] da su consentimiento enviando encabezados HTTP específicos. Es precisamente porque en este caso nuestro servidor [2] no los ha enviado por lo que el navegador se ha negado a realizar la llamada HTTP solicitada por la aplicación.
Veamos ahora los detalles. Analicemos los intercambios de red que tuvieron lugar durante la llamada HTTP. Para ello, en el navegador Chrome, pulsamos [F12] para acceder a las herramientas de desarrollador y seleccionamos la pestaña [Network] para ver los intercambios de red:
![]() |
- en [1], seleccionamos la pestaña [network];
- en [2], solicitamos la lista de médicos;
Obtenemos la siguiente información en la pestaña [network]:
![]() |
- en [1], la información enviada al servidor;
- en [2], la respuesta de este;
En [1] se puede ver que el navegador ha enviado una solicitud HTTP [OPTIONS] sobre la URL solicitada. [OPTIONS] es uno de los comandos HTTP posibles, junto con los más conocidos [GET] y [POST]. Permite solicitar información a un servidor, en particular sobre las opciones HTTP que admite, de ahí el nombre del comando. El servidor responde con [2]. Para indicar que acepta solicitudes de clients que no están en su dominio, debe devolver un encabezado específico llamado [Access-Control-Allow-Origin]. Y es precisamente porque no lo ha devuelto por lo que Angular no ha ejecutado la llamada HTTP solicitada y ha devuelto el error:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. No hay ningún encabezado «Access-Control-Allow-Origin» en el recurso solicitado. Por lo tanto, se permite el acceso al origen «http://localhost:63342».
Por lo tanto, debemos modificar nuestro servidor para que envíe el encabezado HTTP esperado.
3.7.6.6. Modificación del servidor web / JSON
Volvemos a Eclipse. Para conservar lo ya hecho, duplicamos el actual version del servidor web / JSON [rdvmedecins-webapi-v2] en [rdvmedecins-webapi-v3] [1]:
![]() |
Realizamos una primera modificación en [ApplicationModel], que es uno de los elementos de configuración del servicio web:
package rdvmedecins.web.models;
...
@Component
public class ApplicationModel implements IMetier {
// la capa [métier]
@Autowired
private IMetier métier;
// datos procedentes de la capa [métier]
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// datos de configuración
private boolean CORSneeded = true;
...
public boolean isCORSneeded() {
return CORSneeded;
}
}
- línea 17: creamos un valor booleano que indica si se aceptan o no los clients ajenos al dominio del servidor;
- líneas 21-23: el método de acceso a esta información;
A continuación, creamos un nuevo controlador Spring MVC [3]:
![]() |
La clase [RdvMedecinsCorsController] es la siguiente:
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;
// envío de opciones al cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// se establece el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", "*");
}
}
// lista de médicos
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
}
- líneas 28-31: definen un controlador para URL [/getAllMedecins] cuando se solicita con el comando HTTP [OPTIONS];
- línea 29: el método [getAllMedecins] admite como parámetro el objeto [HttpServletResponse], que se enviará al cliente que ha realizado la solicitud. Este objeto es inyectado por Spring;
- línea 30: se delega el tratamiento de la solicitud al método privado de las líneas 19-25;
- líneas 15-16: se inyecta el objeto [ApplicationModel];
- líneas 20-23: si el servidor está configurado para aceptar los clients ajenos a su dominio, entonces se envía el encabezado HTTP:
Access-Control-Allow-Origin: *
lo que significa que el servidor acepta los clients de cualquier dominio (*).
Ahora estamos listos para nuevas pruebas. Lanzamos la nueva version del servicio web y descubrimos que el problema persiste. No ha cambiado nada. Si en la línea 30 anterior colocamos una salida de consola, esta nunca se muestra, lo que demuestra que el método [getAllMedecins] de la línea 29 nunca se invoca.
Tras investigar un poco, descubrimos que Spring MVC procesa por sí mismo los comandos HTTP y [OPTIONS] con un procesamiento por defecto. Por lo tanto, siempre es Spring quien responde y nunca el método [getAllMedecins] de la línea 29. Este comportamiento por defecto de Spring MVC se puede modificar. Introducimos una nueva clase de configuración para configurar el nuevo comportamiento:
![]() |
La nueva clase de configuración [WebConfig] es la siguiente:
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 {
// configuración dispatcherservlet para los encabezados CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
- línea 8: la clase es una clase de configuración de Spring. Declara los beans que se colocarán en el contexto de Spring;
- línea 12: el bean [dispatcherServlet] sirve para definir el servlet que gestiona las solicitudes de clients. Es de tipo [DispatcherServlet]. Este servlet se crea normalmente de forma predeterminada. Si lo creamos nosotros mismos, podemos configurarlo;
- línea 14: se crea una instancia de tipo [DispatcherServlet];
- línea 15: se indica a la servlet que reenvíe a la aplicación los comandos HTTP y [OPTIONS];
- línea 16: configuramos el servlet de esta manera;
Ahora nos queda modificar la clase [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 {
}
- línea 11: se importa la nueva clase de configuración [WebConfig];
3.7.6.7. Pruebas de la aplicación - 2
Iniciamos la nueva version del servicio web / JSON e intentamos obtener la lista de médicos con nuestro cliente Angular. Examinamos los intercambios de red en la pestaña [Network]:
![]() |
- En [1], se puede observar que el encabezado HTTP [Access-Control-Allow-Origin: *] ya aparece en la respuesta del servidor. Y, sin embargo, sigue sin funcionar. Examinamos en [2] los registros de la consola. Encontramos el siguiente registro:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. El campo de encabezado de solicitud Authorization está permitido por Access-Control-Allow-Headers
Vemos que el navegador espera un nuevo encabezado HTTP [Access-Control-Allow-Headers] que le indique que tenemos permiso para enviarle el encabezado de autenticación:
Esto puede ser una buena señal. Es posible que Angular haya querido enviar el comando HTTP GET. Pero como este va acompañado de un encabezado de autenticación, pregunta si el servidor lo acepta.
Modificamos nuestro servidor web / JSON para enviar este encabezado. La clase [RdvMedecinsCorsController] evoluciona de la siguiente manera:
// envío de opciones al cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// se establece el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// se autoriza el encabezado [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
- las líneas 6-7 añaden el encabezado que faltaba.
Reiniciamos el servidor y volvemos a solicitar la lista de médicos con el cliente Angular:
![]() |
Esta vez, todo va bien. Los registros de la consola muestran la respuesta recibida por el 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"}
Se observa que:
- el servidor ha devuelto un código de error [status=200] con el mensaje [statusText=OK]. Por eso estamos en la función [success];
- el servidor ha devuelto un objeto [data] con dos campos:
- [status]: (no debe confundirse con el código de error HTTP [status]). Aquí, [status=0] indica que URL [/getAllMedecins] se ha procesado sin errores;
- [data]: que contiene la lista JSON de médicos;
Veamos ahora otros casos interesantes:
Hay un error en los identificadores [login, password]:
![]() |
Se inicia sesión con la identidad [user / user], que no tiene acceso a la aplicación (solo [admin] tiene acceso):
![]() |
Esta vez, el error ya no es [Erreur d'authentification], sino [Accès refusé].
3.7.7. Ejemplo 7: lista de clients
Retomamos la aplicación anterior para presentar esta vez la lista de clients en una lista desplegable de tipo [Bootstrap select]) (véase el apartado 3.6.6).
3.7.7.1. La vista V
La vista inicial será la siguiente:
![]() |
Para obtener la vista V, duplicamos el código [app-16.html] en [app-17.html] y lo modificamos de la siguiente manera:
<div class="container" >
<h1>Rdvmedecins - v1</h1>
<!-- el mensaje de espera -->
<div class="alert alert-warning" ng-show="waiting.visible" >
...
</div>
<!-- la solicitud -->
<div class="alert alert-info" ng-hide="waiting.visible" >
...
<button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
</div>
<!-- la lista de clients -->
<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>
<!-- la lista de errores -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
- líneas 5-7: la barra de espera no cambia;
- líneas 10-13: el formulario no cambia, salvo el texto del botón (línea 12);
- líneas 28-30: el banner de errores no cambia;
- líneas 16-25: la visualización de clients se realiza en una lista desplegable con el estilo del componente [Bootstrap-selectpicker] (atributos data-style, class, línea 19);
- línea 20: se utiliza la directiva [ng-repeat] para generar las diferentes opciones de la lista desplegable. Cabe señalar que el texto de un option es de tipo [Mme Julienne Tatou] y que el valor deloption es de tipo [100], donde 100 es el identificador id del cliente mostrado;
- línea 34: el código Javascript se migra a un nuevo archivo [rdvmedecins-05];
3.7.7.2. El controlador C y el modelo M
El código Javascript del archivo [rdvmedecins-05] se obtiene copiando el archivo [rdvmedecins-04]:

Casi nada cambia, salvo en el controlador, que ahora está adaptado para proporcionar la lista de clients:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- inicialización de la plantilla
// plantilla
$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;
// tarea asíncrona
var task;
// ejecución de la acción
$scope.execute = function () {
// se actualiza el 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 () {
// se solicita la lista de clients;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
return task.promise;
});
// se analiza el resultado de la llamada anterior
promise.then(function (result) {
// resultado={err: 0, data: [client1, client2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
if (result.err == 0) {
// se introducen los datos adquiridos en el modelo
$scope.clients.data = result.data;
// se actualiza el UI
$scope.clients.show = true;
$scope.waiting.visible = false;
// se diseña la lista desplegable
$('.selectpicker').selectpicker();
} else {
// se han producido errores al obtener la lista de clients
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// se actualiza el UI
$scope.waiting.visible = false;
}
});
};
// cancelación en espera
function cancel() {
// se está finalizando la tarea
task.reject();
// se actualiza el UI
$scope.waiting.visible = false;
$scope.clients.show = false;
$scope.errors.show = false;
}
}
])
;
- Casi nada cambia en el controlador. Antes proporcionaba una lista de médicos. Ahora proporciona una lista de clients;
- línea 9: [$scope.clients] será la plantilla del encabezado de los clients en la vista V;
- línea 30: ahora se utiliza el URL [/getAllClients];
- líneas 35-36: las dos formas de respuesta devueltas por el método [dao.getData]. Ahora tenemos clients en lugar de médicos;
- línea 44: una instrucción bastante poco habitual en un código Angular. Se manipula directamente el DOM (Document Object Model). Aquí queremos aplicar el método [selectpicker] (que forma parte de [bootstrap-select.min.js]) a los elementos del DOM que tienen la clase [selectpicker] [$('.selectpicker')]. Solo hay uno, el menú desplegable:
<select data-style="btn-primary" class="selectpicker" select-enable="">
....
</select>
En el apartado 3.6.6 se ha mostrado que esto aplicaba el siguiente estilo a la lista desplegable:
![]() | ![]() |
Al igual que se hizo con los médicos, también debemos modificar el servicio web.
3.7.7.3. Modificación del servicio web - 1
![]() |
La clase [RdvMedecinsController] se amplía con un nuevo método:
package rdvmedecins.web.controllers;
...
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// envío de opciones al cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// se establece el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// se autoriza el encabezado [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 clients
@RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
public void getAllClients(HttpServletResponse response) {
sendOptions(response);
}
}
- líneas 29-32: el método [getAllClients] gestionará la solicitud HTTP [OPTIONS] que le enviará el navegador;
3.7.7.4. Pruebas de la aplicación – 1
Ya estamos listos para realizar una prueba. Iniciamos el servidor web y, a continuación, introducimos valores válidos en el formulario de Angular. Obtenemos la siguiente respuesta:

Este mensaje de error aparece cuando Angular no ha podido realizar la solicitud HTTP solicitada. Por lo tanto, hay que buscar las causas en los registros de la consola. Allí encontramos el siguiente mensaje:
XMLHttpRequest cannot load http://localhost:8080/getAllClients. No hay ningún encabezado «Access-Control-Allow-Origin» en el recurso solicitado. Por lo tanto, se permite el acceso al origen «http://localhost:63342».
Un problema que creíamos resuelto. Veamos ahora los intercambios de red que se han producido:

Se observa que la operación [getAllClients] con el método HTTP [OPTIONS]ha realizado correctamente, pero que la operación [getAllClients] con el método HTTP [GET] se ha cancelado. La respuesta a la solicitud [OPTIONS] fue la siguiente:

Los encabezados HTTP del CORS están ahí. Examinemos ahora los intercambios HTTP durante el GET:

La solicitud HTTP parece correcta. Se observa, en particular, el encabezado de autenticación.
Además del mensaje de error anterior, en los registros de la consola aparece el siguiente mensaje:
[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 es el registro que genera sistemáticamente el método [dao.getData] al recibir la respuesta a su solicitud HTTP. Se pueden observar dos cosas:
- [status=0]: esto significa que fue Angular quien canceló la solicitud HTTP;
- [method=GET]: y es la solicitud GET la que se ha cancelado;
Si lo juntamos con el primer mensaje, esto significa que, para la solicitud GET también, Angular espera aquí los encabezados CORS. Sin embargo, actualmente nuestro servicio web solo los envía para la solicitud HTTP [OPTIONS]. Es muy extraño que se produzca este error ahora y no en la lista de médicos. No tengo ninguna explicación.
Por lo tanto, hay que volver a modificar el servicio web.
3.7.7.5. Modificación del servicio web – 2
![]() |
Los métodos [GET] y [POST] se procesan en la clase [RdvMedecinsController]. Debemos modificarla para que estos métodos envíen los encabezados CORS. Lo hacemos de la siguiente manera:
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// lista de clients
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients(HttpServletResponse response) {
// encabezados CORS
rdvMedecinsCorsController.getAllClients(response);
// estado de la aplicación
if (messages != null) {
return new Reponse(-1, messages);
}
// lista de clients
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
...
- línea 8: queremos reutilizar el código que hemos colocado en el controlador [RdvMedecinsCorsController]. Por lo tanto, lo insertamos aquí;
- línea 14: el método que procesa la solicitud [GET /getAllClients]. Realizamos dos modificaciones:
- línea 14: inyectamos el objeto [HttpServletResponse] en los parámetros del método,
- línea 16: utilizamos los métodos de la clase [RdvMedecinsCorsController] para introducir en este objeto los encabezados CORS;
3.7.7.6. Pruebas de la aplicación – 2
Lanzamos el nuevo version del servicio web y volvemos a solicitar la lista de clients. Obtenemos la siguiente respuesta:
![]() |
- en [1], sí que hay una respuesta, pero está vacía [2];
- en [3]: los intercambios de red se han realizado correctamente;
En los registros de la consola, el método [dao.getData] ha mostrado la respuesta que ha recibido:
[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"}
Por lo tanto, el método recibió correctamente la lista de clients. Una vez verificado el código, se sospecha de la siguiente instrucción, que no dominamos muy bien:
// se aplica estilo a la lista desplegable
$('.selectpicker').selectpicker();
Ponemos la línea 2 entre comentarios y lo intentamos de nuevo. Entonces obtenemos la siguiente respuesta:
![]() |
Así pues, hemos localizado el problema. Es la aplicación del método [selectpicker] a la lista desplegable lo que plantea el problema. Al examinar el código fuente de la página errónea, encontramos lo siguiente:
![]() |
- se observa que en [1], la lista desplegable está presente con sus elementos, pero no se muestra [style='display:none'];
- en [2], se ve el botón [bootstrap select]. Los elementos de la lista desplegable deberían aparecer en la lista <ul role='menu'>. No están ahí, por lo que tenemos una lista vacía. Parece que cuando se aplicó el método [selectpicker] a la lista desplegable, su contenido estaba vacío en ese momento;
Al buscar una solución en Internet, encontramos esta. Se sustituye el código:
// se aplica estilo a la lista desplegable
$('.selectpicker').selectpicker();
por el siguiente:
// se aplica estilo a la lista desplegable
$timeout(function(){
$('.selectpicker').selectpicker();
});
El estilo [bootstrap-select] se aplica a través de una función [$timeout]. Ya nos hemos encontrado con esta función, que permite ejecutar una función tras un cierto tiempo de espera. Aquí, la ausencia de tiempo de espera equivale a un tiempo de espera nulo. Las líneas anteriores añaden un evento a la cola de eventos del navegador. Cuando finalice el procesamiento del evento en curso (clic en el botón [Liste des clients]), se mostrará la vista V. Inmediatamente después, el navegador consultará su lista de eventos. Debido a su retardo nulo, el evento [$timeout] aparecerá en primer lugar de la lista y se procesará. A continuación, se aplica el estilo [bootstrap-select] a una lista desplegable rellenada. Veamos el resultado:
![]() |
Si volvemos a mirar el código fuente de la página mostrada, tenemos lo siguiente:
![]() |
El botón [bootstrap-select], que antes estaba vacío, ahora contiene la lista de clients.
3.7.7.7. Uso de una directiva
En el controlador C de la vista V hemos encontrado el siguiente código:
// se aplica estilo a la lista desplegable
$('.selectpicker').selectpicker();
Se manipula un objeto de DOM. Muchos desarrolladores de Angular son reacios a manipular DOM en el código de un controlador. Para ellos, esto debe hacerse en una directiva. Una directiva de Angular puede considerarse una extensión del lenguaje HTML. De este modo, es posible crear nuevos elementos o atributos HTML. Veamos un primer ejemplo:
Creamos el siguiente archivo JS [selectEnable]:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
var selectpicker = $('.selectpicker');
selectpicker.selectpicker();
});
}
};
}]);
- La directiva sigue la sintaxis del controlador a la que ya estamos acostumbrados:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)
La directiva pertenece al módulo [rvmedecins]. Es una función que admite dos parámetros:
- (continuación)
- el primero es el nombre de la directiva [selectEnable];
- el segundo es una matriz ['obj1','obj2',..., function(obj1, obj2,...)], donde los [obj] son los objetos que se deben inyectar en la función. En este caso, el único objeto inyectado es el objeto predefinido [$timeout];
- la función [directive] devuelve un objeto que puede tener diversos atributos. Aquí el único atributo es el atributo [link] (línea 3). Su valor es aquí una función que admite tres parámetros:
- scope: el modelo de la vista en la que se utiliza la directiva;
- element: el elemento de la vista, objeto de la directiva;
- attrs: los atributos de este elemento;
Veamos un ejemplo. La directiva [selectEnable] podría utilizarse en el siguiente contexto:
En el ejemplo anterior, el atributo [select-enable] aplica la directiva [selectEnable] al elemento HTML <div>. Una directiva [doSomething] puede aplicarse a cualquier elemento HTML añadiéndole el atributo [do-something]. Hay que prestar atención al cambio de escritura entre el nombre de la directiva y el atributo asociado a ella. Se pasa de una escritura [camelCase] a una escritura [camel-case].
La directiva [selectEnable] también podría utilizarse de la siguiente manera:
Aquí, la directiva [doSomething] se aplica en forma de etiqueta HTML <do-something>.
Volvamos a la escritura
y a los tres parámetros de la función [link] de la directiva, [scope, element, attrs]:
- scope: es la plantilla de la vista en la que se encuentra la <div>;
- element: es la propia <div>;
- attrs: es la matriz de atributos de la <div>. Estos pueden utilizarse para transmitir información a la directiva. En el ejemplo anterior, escribiremos attrs['selectEnable'] para obtener la información [data]. Cabe destacar el cambio de escritura [selectEnable] para designar el atributo [select-enable];
Volvamos al código de la directiva:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
$('.selectpicker').selectpicker();
});
}
};
}]);
- líneas 14-16: encontramos el código que habíamos colocado anteriormente en el controlador. Este se ejecuta al encontrar la directiva [select-enable] (en forma de elemento o de atributo) durante la visualización de la vista V.
Para implementar esta directiva, copiamos el archivo [app-17.html] en [app-17B.html] y lo modificamos de la siguiente manera:
<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>
- línea 1: se aplica la directiva [selectEnable] al elemento HTML [select]. Como no hay información que pasar a la directiva, simplemente escribimos [select-enable=""];
También modificamos el controlador duplicando el archivo JS [rdvmedecins-05.js] en [rdvmedecins-05B.js] y hacemos referencia al nuevo archivo JS en el archivo [app-17B.html] y el archivo de directiva [selectEnable.js]. No hay que olvidar este último punto. Si el archivo de directiva no existe, el atributo [select-enable=""] no se gestionará, pero Angular no señalará ningún error.
<script type="text/javascript" src="rdvmedecins-05B.js"></script>
<script type="text/javascript" src="selectEnable.js"></script>
En el archivo JS [rdvmedecins-05B.js], eliminamos del controlador las siguientes líneas:
// se aplica estilo a la lista desplegable
$timeout(function(){
$('.selectpicker').selectpicker();
});
ya que esta operación la realiza ahora la directiva.
3.7.7.8. Pruebas de la aplicación – 3
Al probar la nueva aplicación [app-17B.html], se obtiene el siguiente resultado:
![]() |
- En [1], se obtiene una lista vacía.
Los registros de la consola muestran lo siguiente:
- línea 1: inicialización del servicio [dao];
- línea 2: al mostrar inicialmente la vista V, se ejecuta la directiva [selectEnable];
- línea 3: esta línea aparece cuando el usuario hace clic en el botón [Liste des clients]. Se observa entonces que la directiva [selectEnable] no se ejecuta por segunda vez. Al final, se ejecutó cuando la lista de clients estaba vacía y, por lo tanto, tenemos una lista desplegable vacía;
Dicho de otro modo, la operación:
$('.selectpicker').selectpicker();
no se ha llevado a cabo en el momento adecuado. Se puede intentar resolver el problema de diversas maneras. Tras numerosas pruebas infructuosas, nos damos cuenta de que la operación anterior solo debe realizarse una vez y únicamente cuando la lista desplegable se haya rellenado. Para obtener este resultado, reescribimos la etiqueta <select> de la siguiente manera:
<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>
Línea 1: la etiqueta <select> solo se genera si existe [clients.data]. Este no es el caso durante la visualización inicial de la vista V. Por lo tanto, la etiqueta <select> no se generará y la directiva [selectEnable] no se evaluará. Cuando el usuario haga clic en el botón [Liste des clients], [clients.data] tendrá un nuevo valor en la plantilla M. Como la plantilla M ha cambiado, la etiqueta <select> se volverá a evaluar y, en este caso, se generará. Por lo tanto, la directiva [selectEnable] también se evaluará. Cuando se evalúa, las líneas 2-4 de la etiqueta <select> aún no se han evaluado. Por lo tanto, tenemos una lista de clients vacía. Si escribimos la directiva [selectEnable] de la siguiente manera:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable");
$('.selectpicker').selectpicker();
}
}
}]);
la línea 5 se ejecutará con una lista vacía y, por lo tanto, se mostrará una lista desplegable vacía. Por lo tanto, hay que escribir:
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 obtener el resultado esperado. Debido al [$timeout] de la línea 5, la línea 6 solo se ejecutará tras la evaluación completa de la vista V, es decir, en un momento en el que la etiqueta <select> tenga todos sus elementos.
3.7.8. Ejemplo 8: el agenda de un médico
A continuación, presentamos una aplicación que muestra el agenda de un médico.
3.7.8.1. La vista V de la aplicación
Presentaremos el siguiente formulario:
![]() |
- en [1], se solicita el agenda de la Sra. PELISSIER [2], el 25 de junio de 2014 [3];
Se obtiene el siguiente resultado: [4]:
![]() |
Analizaremos las dos vistas por separado.
3.7.8.2. El formulario
Duplicamos el archivo [app-17.html] en [app-18.html] y luego modificamos el código de la siguiente manera:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- el mensaje de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la solicitud -->
<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>
<!-- la lista de errores -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- el agenda -->
<div id="agenda" ng-show="agenda.show">
...
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
- líneas 5-7: el mensaje de espera no cambia;
- líneas 12-19: la lista de médicos de tipo [bootstrap select];
- líneas 20-26: el calendario de [ui-bootstrap] que ya hemos presentado. Cabe señalar que el día seleccionado se coloca en la plantilla [calendar.jour] (atributo ng-model);
- línea 28: el botón que solicita el agenda;
- líneas 32-34: la lista de errores no cambia;
- líneas 37-39: el agenda que presentaremos más adelante;
- línea 42: el código JS se transfiere al archivo [rdvmedecins-06.js] mediante la copia del archivo [rdvmedecins-05.js];
3.7.8.3. El controlador C
El código JS de la aplicación queda así:

Solo el servicio [utils] y el controlador [rdvMedecinsCtrl] se verán afectados por los cambios.
El controlador [rdvMedecinsCtrl] pasa a ser el siguiente:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- inicialización del modelo
// plantilla
$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()};
// estilo de la lista desplegable
$timeout(function () {
$('.selectpicker').selectpicker();
});
// configuración regional francesa para el calendario
angular.copy(config.locales['fr'], $locale);
...
}
])
;
- línea 7: se establece un tiempo de espera de 3 segundos antes de realizar la llamada HTTP;
- línea 8: se fijan de forma rígida los elementos necesarios para la conexión HTTP;
- líneas 10-17: se define de forma fija la lista de médicos;
- línea 18: la plantilla [agenda] configura la visualización de agenda en la vista;
- línea 19: la plantilla [calendar] configura la visualización del calendario en la vista. Se establece una fecha mínima [minDate] en hoy y la fecha actual también en hoy;
- líneas 21-23: la lista desplegable se diseña con el método visto anteriormente;
- línea 25: se establece la configuración regional de la aplicación en «fr». Por defecto, está en «en»;
El método que se ejecuta al solicitar el agenda es el siguiente:
// ejecución de la acción
$scope.execute = function () {
// información del formulario
var idMedecin = $('.selectpicker').selectpicker('val');
// verificación
utils.debug("[homeCtrl] idMedecin", idMedecin);
utils.debug("[homeCtrl] jour", $scope.calendar.jour);
// se pone el día en formato aaaa-MM-dd
var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
// actualización de la vista
$scope.waiting.visible = true;
$scope.errors.show = false;
$scope.agenda.show = false;
...
};
- línea 4: se recupera el atributo [value] del médico seleccionado. Aquí se vuelve a utilizar el método [selectpicker], que procede del archivo [bootstrap-select.min.js]. Hay que recordar el formato de las opciones de la lista desplegable:
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
El valor (atributo value) de option es, por tanto, el identificador [id] del médico.
- línea 11: se pone el día elegido por el usuario en el formato [aaaa-mm-jj], que es el formato de fecha esperado por el servidor web;
- líneas 13-15: cuando finalice el método [execute], se mostrará el banner de espera y se ocultará todo lo demás;
El código continúa de la siguiente manera:
// espera simulada
var task = utils.waitForSomeTime($scope.waiting.time);
// se solicita el agenda del médico
var promise = task.promise.then(function () {
// la ruta del URL de servicio
var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
// se solicita el agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// se devuelve la confirmación de finalización de la tarea
return task.promise;
});
// se analiza el resultado de la llamada al servicio [dao]
promise.then(function (result) {
// fin de la espera
$scope.waiting.visible = false;
// ¿error?
if (result.err == 0) {
// se prepara la plantilla de agenda
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// formato de la visualización de horarios
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
// se crea un evento para aplicar estilos a la tabla tras la visualización de la vista
$timeout(function () {
$("#creneaux").footable();
});
} else {
// Se han producido errores al obtener el agenda
$scope.errors = {
title: config.getAgendaErrors,
messages: utils.getErrors(result),
show: true
};
}
- línea 2: la tarea asíncrona de espera de 3 segundos;
- líneas 5-10: el código que se ejecutará cuando finalice esta espera;
- línea 6: se construye la URL consultada [/getAgendaMedecinJour/1/2014-06-25];
- línea 8: se consulta URL. Se inicia una tarea asíncrona;
- línea 10: se devuelve la promesa de esta tarea asíncrona;
- líneas 14-38: el código que se ejecutará cuando la llamada HTTP haya devuelto su respuesta;
- línea 13: [result] es la respuesta enviada por el método [dao.getData]. Aquí hay que recordar el formato de la respuesta del servidor web:
![]() |
El parámetro [result.data] de la línea 19 es el atributo [data] [1] anterior. Este atributo contiene a su vez el atributo [creneauxMedecin] [2] anterior. Este es una tabla de franjas horarias con dos datos para cada una de ellas:
- [rv]: la forma JSON de una cita o [null] si no hay ninguna cita programada en ese intervalo;
- [hDeb, mDeb, hFin, mFin]: la información horaria del intervalo;
Volvamos al código del controlador:
- línea 15: la espera ha finalizado;
- línea 19: se rellena el modelo [$scope.agenda] que controla la visualización del agenda;
- línea 20: se hace visible el agenda;
- líneas 22-24: se recorre cada uno de los elementos C de la tabla [creneauxMedecin] de la que acabamos de hablar;
- línea 23: cada elemento C tiene un atributo [creneau] que es la franja horaria. A este se le añade un atributo [text] que será la representación textual de la franja horaria en forma de [10h20:10h40];
- líneas 26-28: hacemos «responsiva» la tabla HTML utilizada para mostrar los intervalos de tiempo de agenda. Hemos visto este concepto en el apartado 3.6.7;
![]() |
- línea 27: para que la tabla sea «responsive», hay que aplicarle el método [footable]. Aquí nos encontramos con la misma dificultad que la encontrada para el componente [bootstrap-select]. Si escribimos simplemente la línea 17, vemos que la tabla no es «responsive». Este problema se resuelve de la misma manera con la función [$timeout] (línea 26);
- líneas 31-34: el caso en el que la llamada a HTTP ha fallado. En ese caso, se muestran los mensajes de error;
3.7.8.4. Visualización de agenda
Volvemos ahora al código de agenda en el archivo [app-18.html]. Es el siguiente:
<!-- el agenda -->
<div id="agenda" ng-show="agenda.show">
<!-- caso del médico sin franjas horarias de consulta -->
<h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
translate="agenda_medecinsanscreneaux"></h4>
<!-- agenda del 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>
- líneas 4-5: recordemos que [agenda.data] es el agenda, y que [agenda.data.creneauxMedecin] es una matriz de objetos de tipo [creneauMedecin]. Cada elemento de este último tipo tiene un atributo [creneauMedecin.creneau] que es un intervalo de tiempo. Cada intervalo de tiempo tiene dos elementos que nos interesan:
- [creneauMedecin.creneau.rv], que es el posible RV (rv!=null) tomado del intervalo;
- [creneauMedecin.creneau.text], que es el texto [début:fin] del intervalo de tiempo;
- línea 4: muestra un mensaje especial si el médico no tiene franjas horarias disponibles. Es poco probable, pero resulta que nuestra base de datos está incompleta y este caso se da. La generación o no del mensaje HTML viene controlada por la directiva [ng-if];

La directiva [ng-if] es diferente de las directivas [ng-show, ng-hide]. Estas últimas se limitan a ocultar un campo presente en el documento. Si se utiliza [ng-if='false'], el campo se elimina del documento. La hemos utilizado aquí a modo de ejemplo;
- línea 9: el atributo [id='creneaux'] es importante. Es el que se utiliza en la instrucción:
$("#creneaux").footable();
- líneas 10-22: muestran los encabezados de la tabla [1];
- líneas 23-45: muestran el contenido de la tabla [2];
![]() |
- línea 24: se recorre la tabla [agenda.data.creneauxMedecin];
- líneas 26-29: se escribe el texto [3]. Se utiliza la directiva [ng-class], que generará el atributo [class] del elemento. Aquí, si tenemos [creneauMedecin.rv==null], significa que la franja horaria está libre y se le pone un fondo verde al texto. De lo contrario, se le pone un fondo rojo;
- línea 32: se escribe el nombre del cliente para el que se ha reservado el RV [4]. Si es [rv==null], esta información no existe, pero Angular gestiona correctamente este caso y no declara ningún error;
- líneas 34-39: muestran uno de los dos botones [Réserver] o [Supprimer]. La existencia o no de una cita es lo que determina la elección de uno u otro botón;
3.7.8.5. Modificación del servidor web
Al igual que en los ejemplos anteriores, hay que modificar el servidor web para que URL [/getAgendaMedecinJour] envíe los encabezados CORS:
![]() |
En la clase [RdvMedecinsCorsController] se añade un nuevo método:
// agenda del médico
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getAgendaMedecinJour(HttpServletResponse response) {
sendOptions(response);
}
Este método enviará los encabezados CORS para la consulta HTTP [OPTIONS]. Debemos hacer lo mismo para la solicitud HTTP [GET] en la clase [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
// encabezados CORS
rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}
3.7.8.6. Uso de directivas
Al igual que se ha hecho anteriormente, vamos a trasladar la manipulación de DOM a unas directivas. Tenemos dos manipulaciones de DOM:
- al mostrar la vista por primera vez:
// se aplica estilo a la lista desplegable
$timeout(function () {
$('.selectpicker').selectpicker();
});
- al visualizar el agenda:
// se crea un evento para aplicar estilos a la tabla tras mostrar la vista
$timeout(function () {
$("#creneaux").footable();
});
Para el primer caso, utilizaremos la directiva [selectEnable] ya presentada. Para el segundo caso, creamos la directiva [footable] en el siguiente archivo JS [footable.js]:
angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive footable");
$timeout(function () {
$("#creneaux").footable();
})
}
}
}]);
Por lo tanto, utilizamos la misma técnica que para la directiva [selectEnable].
El código HTML [app-18.html] se duplica en [app-18B.html]. A continuación, se modifica de la siguiente manera:
<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>
- línea 1: se aplica la directiva [selectEnable] (a través del atributo [select-enable]) a la etiqueta <select> de los 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>
- línea 3: se aplica la directiva [footable] (a través del atributo [footable]) a la tabla HTML de agenda;
<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- directivas -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- líneas 3-4: se hace referencia a los archivos JS de ambas directivas;
- línea 1: el código JS de [app-18B.html] es el código JS de [app-18.html] duplicado en el archivo [rdvmedecins-06B.js];
El archivo [rdvmedecins-06B.js] es idéntico al archivo [rdvmedecins-06.js] salvo por dos detalles. Desaparecen las líneas que manipulan el DOM:
// se aplica estilo a la lista desplegable
$timeout(function () {
$('.selectpicker').selectpicker();
});
// se crea un evento para aplicar estilos a la tabla tras mostrar la vista
$timeout(function () {
$("#creneaux").footable();
});
De este modo, la ejecución de la aplicación [app-18B.html] da los mismos resultados que la de [app-18.html].
3.7.9. Ejemplo 9: crear y cancelar reservas
A continuación, presentamos una aplicación que permite crear y cancelar reservas.
3.7.9.1. La vista V de la aplicación
Presentaremos el siguiente formulario:
![]() |
- en [1], se podrá realizar una reserva. La reserva que se realice será para un cliente aleatorio;
- en [2], podremos eliminar las reservas que hayamos realizado;
Duplicamos el archivo [app-18.html] en [app-19.html] y luego modificamos el código de la siguiente manera:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- el mensaje de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la lista de errores -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- el agenda -->
<div id="agenda" ng-show="agenda.show">
..
<!-- agenda del 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>
- líneas 5-7: el mensaje de espera es el del version anterior;
- líneas 10-12: el mensaje de error es el de la version anterior;
- líneas 15-36: el agenda es el de la version anterior, salvo por dos detalles:
- línea 26: el clic en el botón [réserver] (atributo ng-click) es gestionado por el método [reserver] del modelo M de la vista V. Se le pasa el número de la franja horaria de reserva;
- línea 26: el clic en el botón [supprimer] es gestionado por el método [reserver] del modelo M de la vista V. Se le pasa el n.º de la cita que se va a eliminar;
- línea 39: el código JS que gestiona la aplicación se encuentra en el archivo [rdvmedecins-07.js];
- línea 40: el código JS de la directiva [footable] aplicada en la línea 20;
3.7.9.2. El controlador C
El código JS de [rdvmedecins-07.js] se obtiene primero copiando el archivo [rdvmedecins-06.js]. A continuación, se modifica. Seguimos teniendo los grandes bloques de código habituales. Las modificaciones se realizan principalmente en el controlador:

Vamos a describir el controlador C de la vista V en varios pasos.
3.7.9.3. Inicialización del controlador C
El código de inicialización del controlador es el siguiente:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- inicialización de la plantilla
// plantilla
$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 local francés para la fecha
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
// la tarea asíncrona global
var task;
// se solicita el agenda
getAgenda();
// ------------------------------------------------------------------ reserva
$scope.reserver = function (creneauId) {
....
};
// ------------------------------------------------------------ eliminación de RV
$scope.supprimer = function (idRv) {
...
};
// obtención del agenda
function getAgenda() {
...
}
// cancelación en espera
function cancel() {
...
}
} ]);
- línea 6: configuración del mensaje de espera. Por defecto, se esperará 3 segundos antes de realizar una llamada HTTP;
- línea 7: la información necesaria para las llamadas HTTP;
- línea 8: configuración del mensaje de error;
- líneas 9-17: los médicos fijos;
- línea 18: un médico concreto. Las reservas se realizarán para sus franjas horarias;
- líneas 19-24: los clients fijos;
- línea 26: queremos manejar fechas francesas;
- línea 27: las citas se concertarán para la fecha de hoy;
- línea 28: el servicio web de reservas espera fechas en formato «aaaa-mm-dd»;
- línea 29: la fecha de hoy en formato [jeudi 26 juin 2014];
- línea 30: configuración de agenda. El atributo [model] transporta los parámetros del mensaje internacionalizado que se va a mostrar:
agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
- línea 35: la variable global [task] representa en un momento dado la tarea asíncrona en ejecución;
- línea 37: se solicita el agenda inicial;
Esto es todo lo que se hace durante la carga inicial de la página. Si todo va bien, la vista muestra el agenda del día de la Sra. PELISSIER.

3.7.9.4. Obtención del agenda
El agenda se obtiene con el siguiente método [getAgenda]:
// obtención del agenda
function getAgenda() {
// la ruta del servicio URL
var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
// se solicita el agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// mensaje de espera
$scope.waiting.visible = true;
// se analiza el resultado de la llamada al servicio [dao]
task.promise.then(function (result) {
// fin de la espera
$scope.waiting.visible = false;
// ¿error?
if (result.err == 0) {
// se prepara la plantilla de agenda
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// formato de la visualización de los horarios
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
} else {
// se han producido errores al obtener el agenda
$scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
}
});
}
Este código es el que se estudió en la aplicación anterior. Hay dos cambios:
- no hay espera simulada antes de la llamada a HTTP;
- línea 4: se utiliza el médico creado durante la inicialización del controlador, así como el día formateado que se ha construido;
Este código se ha aislado en una función, ya que también lo utilizan las funciones [reserver] y [supprimer].
3.7.9.5. Reserva de una franja horaria
![]() | ![]() |
Se recuerda que los clients se eligen de forma aleatoria.
El código de reserva es el siguiente:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// se crea un RV con un cliente aleatorio en la franja horaria identificada 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);
// se añade la franja horaria
var promise = task.promise.then(function () {
// la ruta del URL de servicio
var path = config.urlSvrResaAdd;
// los datos que se deben transmitir al servicio
var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
// se inicia la tarea asíncrona
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// se devuelve la promesa de finalización de la tarea
return task.promise;
});
// análisis del resultado de la tarea
promise = promise.then(function (result) {
if (result.err != 0) {
// se han producido errores al validar el rv
$scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
} else {
// se solicita el nuevo agenda
getAgenda();
}
});
};
- línea 1: se recuerda que el parámetro de la función [reserver] es el n.º de la franja horaria (atributo id);
- línea 4: se elige aleatoriamente un cliente de la lista de clients definida de forma fija en el código de inicialización. Se toma su identificador [id];
- líneas 7-8: espera de 3 segundos;
- líneas 11-18: estas líneas solo se ejecutan al final de los 3 segundos;
- línea 12: el URL del servicio de reservas [/ajouterRv]. Este URL es especial en comparación con los que hemos visto hasta ahora. Se define de la siguiente manera en el servicio web:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
- (continuación)
- línea 1: el URL no tiene parámetros y se solicita con un POST;
- línea 2: los parámetros se envían en forma de un objeto JSON. Este se deserializará en el parámetro [post] (@RequestBody);
Hemos visto un ejemplo de este POST (apartado 2.12.2):
![]() |
- en [0], el URL del servicio web;
- en [1], se utiliza el método POST;
- en [2], el texto JSON de la información transmitida al servicio web en forma de {día, idClient, idCreneau};
- en [3], el cliente indica al servicio web que le envía información JSON;
Volvamos al código JS de la función [reserver]:
- línea 14: se crea el valor que se va a enviar en forma de un objeto JS. Angular lo serializará en JSON cuando se envíe;
- línea 16: se realiza la llamada a HTTP. El valor a enviar es el último parámetro de la función [dao.getData]. Cuando este parámetro está presente, la función [dao.getData] realiza un POST en lugar de un GET (véase el código del apartado 3.7.6.4);
- línea 18: se devuelve la promesa de la llamada HTTP;
- líneas 23-29: solo se ejecutan cuando la llamada HTTP ha devuelto su respuesta;
- línea 23: el parámetro [result] tiene el formato [err,data] o [err,messages], donde [err] es un código de error;
- líneas 23-26: si se han producido errores, se muestra el mensaje de error;
- línea 28: si la reserva se ha realizado correctamente, se vuelve a mostrar el nuevo agenda;
3.7.9.6. Modificación del servidor
![]() |
En la clase [RdvMedecinsCorsController], añadimos el siguiente método:
// envío de las opciones al cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// se establece el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// se autoriza el encabezado [authorization]
response.addHeader("Access-Control-Allow-Headers", "authorization");
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
public void ajouterRv(HttpServletResponse response) {
sendOptions(response);
}
La adición se realiza en las líneas 10-13. Los encabezados de las líneas 2-8 se enviarán para URL [/ajouterRv] (línea 10) y el método HTTP [OPTIONS] (línea 10).
La clase [RdvMedecinsController] se modifica de la siguiente manera:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
// encabezados CORS
rdvMedecinsCorsController.ajouterRv(response);
...
Para el método [POST] (línea 1) y el URL [/ajouterRv] (línea 1), se llama al método que acabamos de añadir en [RdvMedecinsCorsController] (línea 4), devolviendo así los mismos encabezados HTTP que para el método HTTP [OPTIONS].
3.7.9.7. Tests
Hagamos una primera prueba en la que reservamos una franja horaria cualquiera:
![]() |
Como siempre en estos casos, hay que consultar los registros de la 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":""}
El método [dao.getData] ha fallado con [status=0], lo que significa que ha sido Angular quien ha cancelado la solicitud. En los registros aparece la causa del error:
XMLHttpRequest cannot load http://localhost:8080/ajouterRv. El campo de encabezado de solicitud Content-Type es not, permitido por Access-Control-Allow-Headers.
Si observamos el tráfico de red, vemos lo siguiente:
![]() |
- en [1] y [2]: solo hubo una solicitud HTTP, la solicitud [OPTIONS];
- en [3], el cliente Angular solicita dos autorizaciones:
- la de enviar los encabezados HTTP [accept, authorization, content-type];
- la de enviar un comando POST;
- en [4]: el servidor autoriza el encabezado [authorization]. Recordemos que, en el lado del servidor, somos nosotros mismos quienes enviamos esta autorización;
La novedad es, por tanto, que en una operación POST, el cliente Angular solicita más autorizaciones al servidor. Por lo tanto, hay que modificar este último para que se las conceda:
![]() |
En la clase [RdvMedecinsCorsController], modificamos el método privado que genera los encabezados HTTP enviados para los comandos OPTIONS, GET y POST:
// envío de opciones al cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// se establece el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// se autorizan determinados encabezados
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// se autoriza el POST
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
- línea 7: se ha añadido una autorización para los encabezados HTTP y [accept, content-type];
- línea 9: se ha añadido una autorización para el método POST;
Volvemos a realizar la prueba tras reiniciar el servidor:
![]() |
Esta vez, hemos conseguido reservar.
3.7.9.8. Eliminación de una cita
![]() | ![]() |
El código de la función [supprimer] es el siguiente:
$scope.supprimer = function (idRv) {
utils.debug("suppression rv n°", idRv);
// espera simulada
$scope.waiting.visible = true;
task = utils.waitForSomeTime($scope.waiting.time);
// se añade la franja horaria
var promise = task.promise.then(function () {
// la ruta del servicio URL
var path = config.urlSvrResaRemove;
// los datos que se van a transmitir al servicio
var post = {idRv: idRv};
// se inicia la tarea asíncrona
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// se devuelve la promesa de finalización de la tarea
return task.promise;
});
// análisis del resultado de la tarea
promise = promise.then(function (result) {
if (result.err != 0) {
// se han producido errores al eliminar el rv
$scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
// se actualiza el UI
$scope.waiting.visible = false;
} else {
// se solicita el nuevo agenda
getAgenda();
}
});
};
- línea 1: hay que recordar que el parámetro de la función es el número de la cita que se va a eliminar. Aquí tenemos un código muy similar al de la reserva. Solo comentamos las diferencias;
- línea 9: el URL del servicio es aquí [/supprimerRV] y también se accede a él a través de un POST:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
El parámetro enviado se transmite aquí de nuevo en forma de JSON. En el apartado 2.12.17, mostramos la naturaleza del POST creado manualmente:
![]() |
- en [1], el URL del servicio web;
- en [2], se utiliza el método POST;
- en [3], el texto JSON de la información transmitida al servicio web en forma de {idRv};
- en [4], el cliente indica al servicio web que le envía información JSON;
Volvamos al código JS de la función [supprimer]:
- línea 11: se crea el objeto enviado. Angular lo serializará automáticamente en JSON;
El resto del código es similar al de la reserva.
3.7.9.9. Modificación del servidor
En el lado del servidor, realizamos las siguientes modificaciones:
![]() |
En la clase [RdvMedecinsCorsController], añadimos el siguiente método:
// envío de las opciones al cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// se fija el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// se autorizan determinados encabezados
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// se autoriza el POST
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
...
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(HttpServletResponse response) {
sendOptions(response);
}
La adición se realiza en las líneas 13-16. Los encabezados de las líneas 2-10 se enviarán a URL [/supprimerRv] (línea 13) y al método HTTP [OPTIONS] (línea 13).
La clase [RdvMedecinsController] se modifica de la siguiente manera:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
// encabezados CORS
rdvMedecinsCorsController.supprimerRv(response);
...
Para el método [POST] (línea 1) y el URL [/supprimerRv] (línea 1), se llama al método que acabamos de añadir en [RdvMedecinsCorsController] (línea 4), devolviendo así los mismos encabezados HTTP que para el método HTTP [OPTIONS].
3.7.10. Ejemplo 10: crear y cancelar reservas - 2
Ahora presentamos la misma aplicación que antes, pero en lugar de reservar para un cliente aleatorio, este se seleccionará en una lista desplegable.
3.7.10.1. La vista V de la aplicación
Presentaremos el siguiente formulario:
![]() |
Los clients se seleccionarán en [1].
El código es similar al de la aplicación anterior, por lo que solo presentaremos las principales diferencias.
Duplicamos el archivo [app-19.html] en [app-20.html] y, a continuación, creamos el código de la lista desplegable de clients [1]:
<!-- la lista de clients -->
<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>
- líneas 8-12: la lista desplegable se implementará con el componente [bootstrap-select];
- línea 1: la directiva [selectEnable] se aplica a través del atributo [select-enable];
- línea 1: la etiqueta <select> solo se genera si existe [clients.data] (# null, undefined). Este punto es importante y se ha explicado en el apartado 3.7.7.8;
Además, importamos nuevos archivos JS:
<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- directrices -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- línea 1: el archivo [rdvmedecins-08.js] se obtiene copiando el archivo [rdvmedecins-0.js];
- líneas 3-4: se importan los archivos de ambas directivas;
3.7.10.2. El controlador C
El código del controlador C evoluciona de la siguiente manera:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- inicialización del modelo
...
// los clients
$scope.clients = {title: config.listClients, show: false, model: {}};
//------------------------------------------- inicialización de vista
// la tarea asíncrona global
var task;
// se solicitan los clients y luego el agenda
getClients().then(function () {
getAgenda();
});
...
// ejecución de la acción
function getClients() {
....
};
} ]);
- línea 8: el objeto [$scope.clients] configura la lista desplegable de los clients en la vista V;
- líneas 14-16: de forma asíncrona, primero se solicita la lista de clients y, una vez obtenida, se solicita el agenda de la Sra. PELISSIER para el día de hoy. La sintaxis utilizada aquí solo funciona porque la función [getClients] devuelve una promesa (promise);
El método [getClients] solicita la lista de clients:
function getClients() {
// se actualiza el UI
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// se solicita la lista de clients;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// se analiza el resultado de la llamada anterior
promise = promise.then(function (result) {
// resultado={err: 0, data: [client1, client2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
if (result.err == 0) {
// se introducen los datos adquiridos en el modelo
$scope.clients.data = result.data;
// se actualiza el UI
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// se han producido errores al obtener la lista de clients
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// se actualiza el UI
$scope.waiting.visible = false;
}
});
// se cumple la promesa
return promise;
};
Es un código que ya hemos visto y comentado. El elemento importante a destacar es la línea 31:
- línea 27: se devuelve la promesa de la línea 10, es decir, la última promesa obtenida en el código. Esta promesa solo se obtendrá cuando la llamada HTTP haya devuelto su respuesta;
El método [reserver] evoluciona ligeramente:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// se crea un RV para el cliente seleccionado
var idClient = $(".selectpicker").selectpicker('val');
...
});
- línea 4: ya no se reserva para un cliente aleatorio, sino para el cliente seleccionado en la lista de clients.
3.7.11. Ejemplo 11: una directiva [selectEnable2]
Este ejemplo repasa las directivas.
3.7.11.1. La vista V
La aplicación muestra la siguiente vista:
![]() |
3.7.11.2. El código HTML de la vista
El código HTML de la vista [app-21.html] es el siguiente:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- el mensaje de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la lista de errores -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- la lista de clients -->
<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>
<!-- la 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>
<!-- directrices -->
<script type="text/javascript" src="selectEnable2.js"></script>
- líneas 19-23: la lista desplegable de clients;
- línea 19: se aplica la directiva [selectEnable2] (atributo [select-enable2]);
- línea 19: solo si [clients.data] no está vacío;
- línea 19: la lista desplegable se identifica mediante el atributo [id="selectpickerClients"];
- líneas 33-37: la lista desplegable de médicos;
- línea 33: se aplica la directiva [selectEnable2] (atributo [select-enable2]);
- línea 33: solo si [medecins.data] no está vacío;
- línea 33: la lista desplegable se identifica mediante el atributo [id="selectpickerMedecins"];
- línea 43: se importa un nuevo archivo JS [rdvmedecins-09.js];
- línea 45: se importa el archivo JS de la nueva directiva;
3.7.11.3. La directiva [selectEnable2]
El código de la directiva [selectEnable2] es el siguiente:
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();
})
}
}
}]);
- línea 4: se muestra el valor del parámetro [attrs] para comprender el funcionamiento del código. Veremos que attrs['id']='selectpickerClients' para la lista de clients;
- línea 6: para localizar en DOM un elemento de [id='x'], escribimos [$('#x')]. Por lo tanto, hay que escribir [$('#selectpickerClients')] para localizar la lista de clients. Esto se consigue con la sintaxis [$('#' + attrs['id'])];
La directiva [selectEnable2] utiliza, por tanto, la información transportada por uno de los atributos del elemento HTML al que se aplica.
3.7.11.4. El controlador C
El controlador C se encuentra en el archivo JS [rdvmedecins-09.js] y tiene la siguiente estructura:
// controlador
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- inicialización de la plantilla
// el mensaje de espera
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
// la información de conexión
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
// los errores
$scope.errors = {show: false, model: {}};
// los médicos
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
// los clients
$scope.clients = {title: config.listClients, show: false, model: {}};
// la tarea asíncrona global
var task;
// ---------------------------------------------------- Inicialización de la vista
// se actualiza el UI
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.medecins.show = false;
$scope.errors.show = false;
// se solicitan los clients y luego los médicos
getClients().then(function () {
getMedecins();
});
// lista de clients
function getClients() {
...
}
// lista de médicos
function getMedecins() {
...
}
// cancelación en espera
function cancel() {
...
}
} ]);
- líneas 26-28: primero se solicitan los clients y luego los médicos;
3.7.11.5. Las pruebas
Pruebe este nuevo version.
3.7.12. Ejemplo 12: una directiva [list]
Retomamos el mismo ejemplo que antes, pero queremos aligerar el código HTML utilizando una directiva. De hecho, actualmente tenemos el siguiente código HTML:
<!-- la lista de clients -->
<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>
Las líneas 14-26 son idénticas a las líneas 1-13. Se aplican a médicos en lugar de a clients. Nos gustaría poder escribir lo siguiente:
<!-- la lista de clients -->
<list model="clients" ng-if="clients.show"></list>
<!-- la lista de médicos -->
<list model="medecins" ng-if="medecins.show"></list>
Este código implica una nueva directiva [list] que vamos a crear ahora.
3.7.12.1. La directiva [list]
La directiva [list] se coloca en el archivo JS [list.js]. Su código es el siguiente:
angular.module("rdvmedecins")
.directive("list", ['utils', '$timeout', function (utils, $timeout) {
// instancia de la directiva devuelta
return {
// elemento HTML
restrict: "E",
// url del fragmento
templateUrl: "list.html",
// ámbito único para cada instancia de la directiva
scope: true,
// función de enlace con el 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();
})
}
}
}]);
- línea 2: define una directiva denominada «list»;
- línea 6: el atributo [restrict] establece las formas de utilizar la directiva. [restrict: "E"] significa que la directiva [list] se puede utilizar como elemento HTML <list ...>...</list>. [restrict: "A"] significa que la directiva [list] se puede utilizar como atributo, por ejemplo <div ... list='...'>. [restrict: "AE"] significa que la directiva [list] se puede utilizar como atributo y como elemento;
- línea 8: el atributo [templateUrl] indica el nombre del fragmento HTML que se utilizará al encontrar la etiqueta. Este fragmento será el cuerpo de la etiqueta;
- línea 10: el atributo [scope] establece el ámbito de la plantilla de la directiva. [scope: true] significa que dos elementos de tipo <list> tendrán cada uno su propia plantilla. Por defecto (ámbito no inicializado), comparten sus plantillas;
- línea 12: la función [link] que ya hemos utilizado varias veces;
Para entender el código anterior, hay que recordar el uso que se le va a dar a la directiva:
<!-- la lista de clients -->
<list model="clients" ng-if="clients.show"></list>
<!-- la lista de médicos -->
<list model="medecins" ng-if="medecins.show"></list>
La directiva [list] se utiliza como elemento HTML <list>. Este elemento tiene dos atributos:
- [model]: cuyo valor será el elemento del modelo M de la vista V en la que se encuentra la directiva [list]. Este elemento alimentará el modelo de la directiva;
- [ng-if]: que se encargará de que el código HTML de la directiva no se genere si no hay nada que visualizar;
Volvamos al código de la función [link] de la directiva:
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();
})
}
Asociemos este código JS con el código HTML que utiliza la directiva:
<list model="clients" ng-if="clients.show"></list>
- línea 3: attrs['model'] tiene aquí el valor «clients»;
- línea 3: scope[attrs['model']] tiene como valor scope['clients'] y representa entonces [$scope.clients], es decir, el campo [clients] del modelo de la vista. Este campo tendrá como valor {id :'...', data:[client1, client2, ...], show : ..., title :'...'} ;
- línea 3: se añade un campo [model] al modelo de la directiva. Esta ha heredado el modelo de la vista en la que se encuentra. Por lo tanto, hay que evitar colisiones con un posible campo [model] que también podría tener la vista. En este caso, no habrá colisión;
- línea 4: se muestra [scope.model] para comprender mejor el código;
- líneas 5-7: volvemos a encontrar un código ya visto. La diferencia es que el id del componente se incluía antes en un atributo attrs['id']. Aquí se incluirá en [scope.model.id];
Ahora, veamos el código HTML generado por la directiva. Debido al atributo [templateUrl: "list.html"] de la directiva, hay que buscarlo en el archivo [list.html]:
<!-- una lista de clients o 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>
- Lo primero que hay que recordar para leer este código es que la directiva ha creado un objeto [scope.model] con la forma [{id :'...', data:[client1, client2, ...], show : ..., title :'...'}]. Este objeto [model] (scope está implícito en el código HTML) es utilizado por el código HTML de la directiva;
- línea 2: uso de [model.show] para mostrar/ocultar la vista generada por la directiva;
- línea 5: uso de [model.title] para establecer un título;
- línea 6: uso de [model.id] para insertar un id en la etiqueta <select>. Este id es utilizado por el código JS de la directiva;
- línea 6: uso de [model.data] para generar el <select> solo si hay datos que mostrar;
- líneas 7-9: uso de [model.data] para generar los elementos de la lista desplegable;
3.7.12.2. El código HTML
El código HTML de la aplicación [app-22.html] es el siguiente:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- el mensaje de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la lista de errores -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- la lista de clients -->
<list model="clients" ng-if="clients.show"></list>
<!-- la lista de médicos -->
<list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- instrucciones -->
<script type="text/javascript" src="list.js"></script>
- línea 22: no hay que olvidar incluir el código JS de la directiva;
3.7.12.3. El controlador C
El controlador C cambia muy poco:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- inicialización de la plantilla
...
// los médicos
$scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
// los clients
$scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
- líneas 7 y 9, añadimos el atributo [id] a los modelos de los médicos y el clients;
3.7.12.4. Las pruebas
Las pruebas dan los mismos resultados que en el ejemplo anterior.
3.7.13. Ejemplo 13: actualización de la plantilla de una directiva
Seguimos con el estudio de las directivas y mantenemos el ejemplo de la lista desplegable. Aquí queremos estudiar el comportamiento de la directiva [list] cuando cambia el contenido de la lista desplegable.
3.7.13.1. Las vistas V
Las diferentes vistas son las siguientes:
![]() |
- en [1], se solicita por primera vez la lista de clients;
![]() |
- en [2], se solicita por segunda vez la lista de clients. Esta segunda lista se suma entonces a la primera [3]. Lo que queremos estudiar en este ejemplo es la actualización del componente [Bootstrap select].
3.7.13.2. La página HTML
La página HTML [app-23.html] se obtiene copiando [app-22.html] y modificándola de la siguiente manera:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- el mensaje de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la lista de errores -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- el botón -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la lista de clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- directivas -->
<script type="text/javascript" src="list2.js"></script>
Los cambios con respecto a la aplicación anterior son los siguientes:
- líneas 15-17: se ha añadido un botón;
- línea 20: uso de una nueva directiva [list2];
- línea 23: uso de un nuevo archivo JS;
- línea 25: importación del archivo JS de la directiva [list2];
3.7.13.3. La directiva [list2]
La directiva [list2] en [list2.js] es la siguiente:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instancia de la directiva devuelta
return {
// elemento HTML
restrict: "E",
// url del fragmento
templateUrl: "list.html",
// ámbito único para cada instancia de la directiva
scope: true,
// función de enlace con el documento
link: function (scope, element, attrs) {
utils.debug('directive list2');
scope.model = scope[attrs['model']];
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
}
}
}]);
La única diferencia con la directiva [list] está en la línea 16: con el método [selectpicker('refresh')], se solicita al componente [Bootstrap-select] que se actualice. La idea detrás de esto es que cada vez que el usuario solicite una nueva lista de clients, se actualizará la lista desplegable. No va a funcionar, pero esa es la idea básica.
3.7.13.4. El controlador C
El controlador se encuentra en el archivo [rdvmedecins-11.js], obtenido al copiar el archivo [rdvmedecins-10.js]:
// los clients
$scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
// lista de clients
$scope.getClients = function getClients() {
// se actualiza el UI
$scope.waiting.visible = true;
$scope.errors.show = false;
// se solicita la lista de clients;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// se analiza el resultado de la llamada anterior
promise = promise.then(function (result) {
// resultado={err: 0, data: [client1, client2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
if (result.err == 0) {
// se introducen los datos obtenidos en un nuevo modelo para forzar la actualización de la vista
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
// se actualiza el UI
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// se han producido errores al obtener la lista de clients
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// se actualiza el UI
$scope.waiting.visible = false;
}
});
}
- línea 1: para permitir la concatenación de tablas en [clients.data], este objeto se inicializa con una tabla vacía;
- línea 18: se concatena la nueva lista de clients con las que ya están presentes en la matriz [clients.data];
Antes habíamos escrito:
Ahora escribimos:
Para entender este código, hay que recordar cómo se utiliza el modelo M en la vista V en el caso de la directiva [list2]:
<!-- la lista de clients -->
<list2 model="clients" ng-if="clients.show"></list2>
La plantilla utilizada por la directiva [list2] es [clients]. Solo se volverá a evaluar en la vista V si [clients] cambia en la plantilla M de la vista. La primera idea que se nos ocurre para la modificación es escribir:
para tener en cuenta que la nueva lista de clients debe añadirse a las anteriores. Al hacerlo, se modifica [clients.data], pero no [clients]. No conozco los entresijos de Javascript, pero no sería de extrañar que [clients] fuera un puntero, al igual que [clients.data]. El puntero [clients] no cambia cuando se cambia el puntero [clients.data]. La directiva [list2] no se reevalúa entonces. Esto es precisamente lo que se observa al depurar la aplicación (F12 en Chrome).
Al escribir:
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
Nos aseguramos de que [$scope.clients] reciba efectivamente un nuevo valor. El puntero [$scope.clients] apunta a un nuevo objeto. La directiva [list2] debería entonces reevaluarse. Sin embargo, no se obtiene el resultado esperado. Examinemos las capturas de pantalla cuando se solicita dos veces la lista de clients:
![]() |
- en [1], solo hay cuatro elementos en lugar de ocho;
- en [2], estos cuatro elementos se encuentran en un [select], pero este está oculto (style='display : none');
![]() |
- en [3], encontramos los cuatro clients en otra arquitectura HTML y es esta la que ve el usuario cuando hace clic en la lista desplegable;
Por último, los registros de la consola indican lo siguiente:
- línea 1: se instancia el servicio [dao];
- línea 2: el servicio [dao] obtiene una primera lista de clients;
- línea 3: se ejecuta la directiva [list2];
- línea 4: el servicio [dao] obtiene una segunda lista de clients;
La visualización de la línea 2 proviene del siguiente código de la directiva:
link: function (scope, element, attrs) {
utils.debug('directive list2');
...
}
Examinemos el ciclo de vida de la directiva [list2]:
- entre las líneas 1 y 2, no está activada aunque la vista se haya mostrado por primera vez. Esto se debe a su atributo [ng-if="clients.show"] en la vista V:
<list2 model="clients" ng-if="clients.show"></list2>
- línea 3: tras obtener la primera lista de médicos, [clients.show] pasa a true y la directiva se activa;
- tras obtener la segunda lista de clients, vemos que no se llama al código de la directiva [list2]. Por eso no vemos la segunda lista;
Para resolver este problema, modificamos la directiva [list2] de la siguiente manera:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instancia de la directiva devuelta
return {
// elemento HTML
restrict: "E",
// url del fragmento
templateUrl: "list.html",
// ámbito único para cada instancia de la directiva
scope: true,
// función de enlace con el documento
link: function (scope, element, attrs) {
// cada vez que cambia attrs["model"], la plantilla de la directiva también debe cambiar
scope.$watch(attrs["model"], function (newValue) {
utils.debug("directive list2 newValue", newValue);
// se actualiza la plantilla de la directiva
scope.model = newValue;
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
});
}
}
}]);
- línea 14: la función [scope.$watch] permite observar un valor del modelo. Su sintaxis es [scope.$watch('var'), f], donde [var] es el identificador de una variable del modelo y f la función que se debe ejecutar cuando esta variable cambia de valor. En este caso, queremos observar la variable [clients]. Por lo tanto, debemos escribir [scope.$watch('clients')]. Como tenemos attrs['model']='clients', escribimos [scope.$watch(attrs["model"], function (newValue)] ;
- línea 14: el segundo parámetro de la función [scope.$watch] es la función que se ejecutará cuando la variable observada cambie de valor. El parámetro [newValue] es el nuevo valor de la variable, es decir, para nosotros, el nuevo valor de la variable [clients] del modelo;
- línea 17: este nuevo valor se asigna al campo [model] del modelo de la directiva;
Una vez realizada esta modificación, los registros cambian:
![]() |
En el ejemplo anterior, vemos que, tras obtener la segunda lista de clients, la directiva [list2] se vuelve a ejecutar correctamente, lo que confirma el resultado [2].
3.7.14. Ejemplo 14: las directivas [waiting] y [errors]
Volvamos al código HTML de la aplicación anterior:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- el mensaje de espera -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la lista de errores -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- el botón -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la lista de clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
- líneas 5-7: el mensaje de espera;
- líneas 10-12: el mensaje de error;
Decidimos poner los códigos HTML de estos dos mensajes en directivas.
3.7.14.1. El nuevo código HTML
El nuevo código HTML [app-24.html] es el siguiente:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- el mensaje de espera -->
<waiting model="waiting"></waiting>
<!-- la lista de errores -->
<errors model="errors"></errors>
<!-- el botón -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la lista de clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- directivas -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
- línea 5: la directiva para el mensaje de espera;
- línea 8: la directiva para el mensaje de error;
- línea 19: el nuevo archivo JS asociado a la aplicación;
- líneas 21-23: los archivos JS de las tres directivas;
3.7.14.2. La directiva [waiting]
El código JS de la directiva [waiting] se encuentra en el siguiente archivo [waiting.js]:
angular.module("rdvmedecins")
.directive("waiting", ['utils', function (utils) {
// instancia de la directiva devuelta
return {
// elemento HTML
restrict: "E",
// url del fragmento
templateUrl: "waiting.html",
// ámbito único para cada instancia de la directiva
scope: true,
// función de enlace con el documento
link: function (scope, element, attrs) {
// cada vez que cambie attr["model"], la plantilla de la página también debe cambiar
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[waiting] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Este código sigue la misma lógica que el de la directiva [list2] ya estudiada.
En la línea 8, se hace referencia al siguiente archivo [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>
En el código JS de la aplicación, la plantilla [$scope.waiting] de este código HTML se definirá de la siguiente manera:
// el mensaje de espera
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time: 3000};
3.7.14.3. La directiva [errors]
El código JS de la directiva [errors] se encuentra en el siguiente archivo [errors.js]:
angular.module("rdvmedecins")
.directive("errors", ['utils', function (utils) {
// instancia de la directiva devuelta
return {
// elemento HTML
restrict: "E",
// url del fragmento
templateUrl: "errors.html",
// ámbito único para cada instancia de la directiva
scope: true,
// función de enlace con el documento
link: function (scope, element, attrs) {
// cada vez que cambia attr["model"], el modelo de la página también debe cambiar
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[errors] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Este código sigue la misma lógica que el de la directiva [list2] ya estudiada.
En la línea 8, se hace referencia al siguiente archivo [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>
En el código JS de la aplicación, la plantilla [$scope.errors] de este código HTML se definirá de la siguiente manera:
// se han producido errores al obtener la lista de clients
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};
3.7.15. Ejemplo 15: navigation
Hasta ahora, hemos utilizado aplicaciones de una sola página. En este ejemplo, abordamos las aplicaciones de varias páginas y el navigation entre ellas.
3.7.15.1. Las vistas V de la aplicación
![]() |
- en [1], el URL de la vista n.º 1;
- en [2], su contenido;
- en [3], se pasa a la página 2;
- en [4], la vista n.º 2;
- en [5], se pasa a la página 3;
![]() |
- en [6], la vista n.º 3;
- en [7], se pasa a la página 1;
- en [8], se vuelve a la vista n.º 1;
3.7.15.2. Organización del código
Comenzamos una nueva organización del código:
![]() |
- las vistas de la aplicación se colocarán en la carpeta [views];
- el módulo de la aplicación se colocará en la carpeta [modules];
- los controladores de la aplicación se colocarán en la carpeta [controllers];
Del mismo modo, en la version final:
- los servicios se colocarán en la carpeta [services];
- las directivas se colocarán en la carpeta [directives];
3.7.15.3. El contenedor de vistas
Las vistas de la carpeta [views] se mostrarán en el siguiente contenedor [app-25.html]:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container" ng-controller="mainCtrl">
<!-- la barra de navigation -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- la vista actual -->
<ng-view></ng-view>
</div>
...
<!-- el módulo -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- los 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>
- línea 7: el cuerpo del contenedor está controlado por [mainCtrl];
- línea 9: la directiva [ng-include] permite incluir un archivo externo HTML, en este caso una barra de navigation;
- línea 12: las diferentes vistas mostradas por el contenedor se muestran dentro de la directiva [ng-view]. Al final, tenemos un contenedor que muestra:
- siempre la misma barra de navigation (línea 9);
- diferentes vistas en la línea 12;
- líneas 16-22: se importan los archivos JS del módulo de la aplicación [rdvmedecins-13.js] y de sus controladores;
3.7.15.4. El módulo de la aplicación
El archivo [rdvmedecins-13.js] define el módulo de la aplicación y el enrutamiento entre vistas:
// --------------------- módulo Angular
angular.module("rdvmedecins", [ 'ngRoute' ]);
angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ enrutamiento
$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"
});
}]);
- línea 1: se define el módulo [rdvmedecins]. Depende del módulo [ngRoute] proporcionado por la biblioteca [angular-route.min.js]. Este módulo es el que permite el enrutamiento definido en las líneas 6-24;
- línea 4: define la función [config] del módulo [rdvmedecins]. Cabe recordar que esta función se ejecuta antes de cualquier instanciación del servicio. Se trata de una función de configuración del módulo. En este caso, se configura su enrutamiento. Esto se realiza mediante el objeto [$routeProvider] proporcionado por el módulo [ngRoute];
- líneas 6-10: definen la vista que se mostrará cuando el usuario solicite el URL [/page1]. Se trata de un enrutamiento interno de la aplicación. El URL es, en realidad, [/rdvmedecins-angular-v1/app-21.html#/page1]. Se observa que siempre se utiliza el URL del contenedor [/rdvmedecins-angular-v1/app-21.html], pero con información adicional detrás de un carácter #. Es esta información adicional la que gestiona el enrutamiento de Angular;
- línea 8: indica el fragmento HTML que se debe insertar en la directiva [ng-view] del contenedor:
- línea 9: indica el nombre del controlador de este fragmento;
- líneas 11-15: definen la vista que se mostrará cuando el usuario solicite el URL [/page2];
- líneas 16-20: definen la vista que se mostrará cuando el usuario solicite el URL [/page3];
- líneas 21-24: definen el enrutamiento que se debe aplicar cuando el URL solicitado no es uno de los tres anteriores (otherwise, línea 21);
- línea 23: redireccionamiento a la URL [/page1], es decir, a la vista definida en las líneas 6-10;
3.7.15.5. El controlador del contenedor de vistas
Hemos visto que el contenedor de vistas declaraba un controlador:
<div class="container" ng-controller="mainCtrl">
El controlador [mainCtrl] se define en el archivo [mainController.js]:
// controlador
angular.module("rdvmedecins")
.controller('mainCtrl', ['$scope', '$location',
function ($scope, $location) {
// plantillas de las páginas
$scope.page1 = {};
$scope.page2 = {};
$scope.page3 = {};
// plantilla global
var main = $scope.main = {};
main.text = "[Modèle global]";
// métodos expuestos a la vista
main.showPage1 = function () {
$location.path("/page1");
};
main.showPage2 = function () {
$location.path("/page2");
};
main.showPage3 = function () {
$location.path("/page3");
}
}]);
- línea 3: el controlador [mainCtrl] necesita el objeto [$location] proporcionado por el módulo de enrutamiento [ngRoute]. Este objeto permite cambiar de vista (líneas 16, 19, 22);
Volvamos al código del contenedor:
<div class="container" ng-controller="mainCtrl">
<!-- la barra de navigation -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- la vista actual -->
<ng-view></ng-view>
</div>
- el controlador [mainCtrl] construye el modelo de la zona 1-7;
- la vista incluida en la línea 6 también tiene un controlador. Por ejemplo, la vista [page1] tiene el controlador [page1Ctrl]. Este construye el modelo de la zona mostrada en la línea 6. Así, en esta zona tenemos dos modelos:
- el modelo creado por el controlador [mainCtrl];
- el modelo creado por el controlador [page1Ctrl];
Existe herencia de modelos. En la vista mostrada en la línea 6, se ven ambos modelos de los controladores [mainCtrl] y [pagexCtrl]. Si dos variables de estos modelos tienen el mismo nombre, una ocultará a la otra. Para evitar esta colisión de nombres, creamos cuatro modelos con cuatro nombres:
contenedor | mainCtrl | mano | 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 |
- línea 12: define un elemento [text] en el modelo [main];
Las líneas 7-11 tienen una consecuencia muy particular: definen el [$scope] del controlador [mainCtrl] y, dentro de este, crean cuatro variables [main, page1, page2, page3]. Estas cuatro variables se utilizarán como plantillas respectivas del contenedor y de las tres vistas que este contendrá sucesivamente.
3.7.15.6. La barra de navigation
La barra de navigation se define de la siguiente manera en el contenedor:
<div class="container" ng-controller="mainCtrl">
<!-- la barra de navigation -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- la vista actual -->
<ng-view></ng-view>
</div>
La barra de navigation se define en la línea 3. Esto significa que solo conoce el modelo [main]. Su código es el siguiente:
<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>
- en las líneas 16, 21 y 26 se utilizan métodos del modelo [main];
- línea 16: al hacer clic en el enlace [Page1] se iniciará la ejecución del método [$scope.main.showPage1]. Este se define en el controlador [mainCtrl] de la siguiente manera:
// modelo global
var main = $scope.main = {};
main.text = "[Modèle global]";
// métodos expuestos en la vista
main.showPage1 = function () {
$location.path("/page1");
};
- línea 6: del código anterior se observa que el método [main.showPage1] es, en realidad, el método [$scope.main.showPage1]. Por lo tanto, es este el que se ejecutará;
- línea 7: cambiamos el URL de la aplicación, que pasa a ser [/page1]. Volvamos al enrutamiento que se ha definido en el módulo principal:
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
vemos que el fragmento [views/page1.html] se insertará en el contenedor y que su controlador es [page1Ctrl].
3.7.15.7. La vista [/page1] y su controlador
El fragmento [views/page1.html] es el siguiente:
<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>
Recordemos que en la vista insertada en el contenedor, el modelo [main] es visible. Esto es lo que queremos comprobar en la línea 4. Por otra parte, el controlador [page1Ctrl] del fragmento [views/page1.html] define un modelo [page1]. Este es el que se utiliza en la línea 5.
El código del controlador [page1Ctrl] es el siguiente:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope',
function ($scope) {
// plantilla de la página 1
var page1=$scope.page1;
page1.text="[Modèle local dans page 1]";
}]);
- línea 2: el [$scope] inyectado aquí no está vacío. Dado que el controlador [page1Ctrl] controla un área insertada en un contenedor controlado por [mainCtrl], el [$scope] de la línea 2 contiene los elementos del [$scope] definido por el controlador [mainCtrl]. Es importante comprender esto. El [$scope] definido por el controlador [mainCtrl] contiene los siguientes elementos [main, page1, page2, page3]. Esto significa que se tiene acceso a los modelos de todas las vistas. No es necesariamente deseable, pero es el caso aquí. En el version final del cliente Angular, utilizaremos esta particularidad para almacenar en el modelo [main] la información que debe compartirse entre vistas. Tendremos aquí un concepto análogo al concepto de «sesión» del lado del servidor;
- línea 6: recuperamos en el [$scope] el modelo [page1] de la página 1 y luego trabajamos con él (línea 7). Obtenemos entonces la siguiente visualización:
![]() |
Las vistas [/page2] y [/page3] se basan en el mismo modelo que la vista [/page1] (véanse las capturas de pantalla de la página 240).
3.7.15.8. Comprobación de la vista navigation
Ahora queremos controlar la navigation de la siguiente manera: [page1 --> page2 --> page3 --> page1]. Así, si el usuario se encuentra en la página 1 [/page1] y escribe en su navegador laURL [/page3], entonces este navigation no debe aceptarse y debemos permanecer en la página 1.
Para conseguir este resultado, modificamos los controladores de las páginas de la siguiente manera:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope', '$location',
function ($scope, $location) {
// ¿navigation autorizada?
var main = $scope.main;
if (main.lastUrl && main.lastUrl != '/page3') {
// volvemos a la última URL
$location.path(main.lastUrl);
return;
}
// se guarda el URL de la página
main.lastUrl = '/page1';
// plantilla de la página
var page1 = $scope.page1;
page1.text = "[Modèle local dans page 1]";
}]);
- línea 12: cuando se muestre una página, memorizaremos su URL en el modelo [main.lastUrl]. Aquí utilizamos el concepto del que hemos hablado anteriormente: utilizar el modelo [main] para almacenar información compartida por todas las vistas. En este caso, se trata del último URL consultado;
- el código de las líneas 4-12 se duplica y se adapta a las tres vistas. Aquí estamos en la vista [/page1];
- línea 5: se recupera el modelo [main];
- línea 6: si la plantilla [main.lastUrl] existe y es diferente de [/page3], entonces se prohíbe la navigation (la última URL visitada existe y no es /page3);
- línea 8: entonces volvemos a la última URL visitada;
Hagamos una prueba:
![]() |
- en [1], estamos en la página 1 y escribimos el URL de la página 3 en [2];
- en [3], el navigation no se ha producido y hemos vuelto al URL de la página 1;
3.7.16. Conclusión
Hemos repasado todos los casos de uso que encontraremos en el version final del cliente Angular. Cuando lo presentemos, comentaremos más las funcionalidades de la aplicación que sus detalles de implementación. En cuanto a estos últimos, nos limitaremos a hacer referencia al ejemplo que ilustra el caso de uso que se ha estudiado.
3.8. El cliente final Angular
3.8.1. Estructura del proyecto
El proyecto final tiene el siguiente aspecto:
![]() |
![]() |
- en [1], el proyecto completo. [app.html] es la página maestra de la aplicación;
- en [2], los controladores;
- en [3], las directivas;
- en [4], los servicios y el módulo Angular [main.js] de la aplicación;
- en [5], las diferentes vistas que se insertan en la página maestra [app.html];
3.8.2. Las dependencias del proyecto
Las dependencias del proyecto son las siguientes:
![]() |
La función de estos diferentes elementos se ha explicado en el apartado 3.4, página 134.
3.8.3. La página maestra [app.html]
La página maestra es la siguiente:
<!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é">
<!-- el 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>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- módulos -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- servicios -->
<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>
<!-- directivas -->
<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>
- línea 18: cabe señalar que [appCtrl] es el controlador de la página maestra;
- líneas 19-21: el contenido de la página maestra;
Este contenido es el siguiente:
<div class="container">
<!-- las barras de navigation -->
<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>
<!-- el jumbotron -->
<ng-include src="'views/jumbotron.html'"></ng-include>
<!-- el título de la página -->
<div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
translate-values="{{app.titre.model}}"></div>
<!-- los errores de la página -->
<errors model="app.errors" ng-show="app.errors.show"></errors>
<!-- el mensaje de espera -->
<waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
<!-- la vista actual -->
<ng-view></ng-view>
<!-- debug -->
<debug model="app" ng-show="app.debug.on"></debug>
</div>
Sea cual sea la vista mostrada, siempre tendrá los siguientes elementos:
- líneas 3-4: una barra de comandos. Las dos barras de las líneas 3 y 4 son mutuamente excluyentes;
![]()
![]()
- línea 6: un logotipo o texto de la aplicación:

- línea 8: un título

- línea 11: un mensaje de error:

- línea 13: un mensaje de espera:

- línea 17: información de depuración:

Todos los elementos anteriores están controlados por una directiva [ng-show / ng-hide] que hace que, aunque estén presentes, no sean necesariamente visibles.
3.8.4. Las vistas de la aplicación
En el código de la página maestra, tenemos:
<div class="container">
...
<!-- vista actual -->
<ng-view></ng-view>
...
</div>
La línea 4 recibe las diferentes vistas de la aplicación. Estas se definen en el módulo [main.js]:

La función de la configuración de las diferentes rutas se ha explicado en el apartado 3.7.15.4, página 242.
La vista [login.html] está vacía, es decir, no añade ningún elemento a los que ya están presentes en la página maestra.
La vista [home.html] añade el siguiente elemento a la página maestra:

La vista [agenda.html] añade el siguiente elemento a la página maestra:

La vista [resa.html] añade el siguiente elemento a la página maestra:

3.8.5. Funcionalidades de la aplicación
Las vistas del cliente Angular ya se han presentado en el apartado 1.3.3, página 7. Para facilitar la lectura de este nuevo capítulo, las volvemos a incluir aquí. La primera vista es la siguiente:
![]() |
- en [6], la página de inicio de la aplicación. Se trata de una aplicación para concertar citas con médicos;
- en [7], una casilla de selección que permite estar o no en modo [debug]. Este último se caracteriza por la presencia del marco [8] que muestra la plantilla de la vista actual;
- en [9], un tiempo de espera artificial en milisegundos. Su valor por defecto es 0 (sin espera). Si N es el valor de este tiempo de espera, cualquier acción del usuario se ejecutará tras un tiempo de espera de N milisegundos. Esto permite ver la gestión de la espera implementada por la aplicación;
- en [10], el URL del servidor Spring 4. Si seguimos lo anterior, es [http://localhost:8080];
- en [11] y [12], el identificador y la contraseña de quien desea utilizar la aplicación. Hay dos usuarios: admin/admin (login/contraseña) con un rol (ADMIN) y user/user con un rol (USER). Solo el rol ADMIN tiene permiso para utilizar la aplicación. El rol USER solo está ahí para mostrar lo que responde el servidor en este caso de uso;
- en [13], el botón que permite conectarse al servidor;
- en [14], el idioma de la aplicación. Hay dos: el francés por defecto y el inglés.
![]() |
- en [1], se establece la conexión;
![]() |
- una vez conectado, se puede elegir el médico con el que se desea concertar una cita [2] y el día de la misma [3];
- se solicita en [4] ver el agenda del médico elegido para el día elegido;
![]() |
- una vez obtenido el agenda del médico, se puede reservar una franja horaria [5];
![]() |
- en [6], se selecciona al paciente para la cita y se valida esta selección en [7];
![]() |
Una vez validada la cita, se vuelve automáticamente a agenda, donde ya figura la nueva cita. Esta cita podrá eliminarse posteriormente en [7].
Se han descrito las principales funcionalidades. Son sencillas. Las que no se han descrito son funciones de navigation para volver a una vista anterior. Terminemos con la gestión del idioma:
![]() |
- en [1], se cambia del francés al inglés;
![]() |
- en [2], la vista pasa a inglés, incluido el calendario;
3.8.6. El módulo [main.js]
El módulo [main.js] define el módulo Angular que controlará la aplicación:
![]() |
- línea 4: el módulo se llama [rdvmedecins];
- línea 5: el módulo [ngRoute] se utiliza para el enrutamiento de URL;
- línea 6: el módulo [translate] se utiliza para la internacionalización de los textos;
- línea 7: el módulo [base64] se utiliza para codificar en Base64 la cadena «login:password»;
- línea 8: el módulo [ngLocale] se utiliza para internacionalizar el calendario;
- línea 9: el módulo [ui.bootstrap] se utiliza para el calendario;
- línea 12: la configuración de las rutas;
- línea 40: la internacionalización de los mensajes;
3.8.7. El controlador de la página maestra
Recordemos el código HTML de la página maestra [app.html]:
<body ng-controller="appCtrl">
<div class="container">
...
Línea 1: todo el cuerpo (body) de la página maestra está controlado por el controlador [appCtrl]. Por su posición, esto lo convierte en un controlador general y principal de la aplicación. Como se ha explicado en el apartado 3.7.15, el modelo construido por este controlador es heredado por todas las vistas que se insertarán en la página maestra.
Su código es el siguiente:
angular.module("rdvmedecins")
.controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
function ($scope, config, utils, $location, $locale) {
// debug
utils.debug("[app] init");
// ----------------------------------------inicialización de la página
// las plantillas de las # páginas
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// plantilla de la página actual
var app = $scope.app;
...
// ---------------------------------- métodos
// cancelación de la tarea actual
app.cancel = function () {
...
};
// desconexión
app.deconnecter = function () {
...
};
// este código debe permanecer aquí porque hace referencia a la función [cancel] que precede
app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
}])
;
Las líneas 10-14 definen los cinco modelos que se utilizan en la aplicación:
app.html | appCtrl | |
login.html | loginCtrl | |
home.html | homeCtrl | |
resa.html | resaCtrl | |
agenda.html | agendaCtrl |
Lo importante es comprender que el objeto [$scope], al ser la plantilla del controlador de la página maestra, es heredado por todas las vistas y controladores. Por lo tanto, el controlador [loginCtrl] tiene acceso a los elementos de [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda]. Dicho de otro modo, un controlador tiene acceso a los modelos de otros controladores. La aplicación que nos ocupa evita cuidadosamente utilizar esta posibilidad. Así, por ejemplo, el controlador [loginCtrl] trabaja únicamente con dos modelos:
- el suyo, [$scope.login];
- y el del controlador padre [$scope.app];
Lo mismo ocurre con todos los demás controladores. El modelo [$scope.app] se utilizará como memoria compartida entre los distintos controladores. Cuando un controlador C1 tenga que transmitir información al controlador C2, se procederá de la siguiente manera:
En [C1]:
En [C2]:
En ambos casos, $scope se hereda del controlador [appCtrl] y, por lo tanto, es idéntico (es un puntero) en [C1] y [C2]. El objeto [$scope.app], que sirve de memoria compartida entre los controladores, se denominará a menudo session en los comentarios, por analogía con la sesión utilizada en las aplicaciones web clásicas, que designa la memoria compartida entre solicitudes HTTP sucesivas.
Volvamos al código del controlador [appCtrl]:
// las plantillas de las # páginas
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// plantilla de la página actual
var app = $scope.app;
// [app.debug] y [utils.verbose] deben estar siempre sincronizados
app.debug = utils.verbose;
app.debug.on = config.debug;
// sin título de página por el momento
app.titre = {show: false};
// sin barras de navigation
app.navbarrun = {show: false};
app.navbarstart = {show: false};
// sin errores
app.errors = {show: false};
// lokal por defecto
angular.copy(config.locales['fr'], $locale);
// vista actual
app.view = {url: undefined, model: {}, done: false};
// la tarea actual
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
- línea 8: [$scope.app] será la plantilla de la página maestra. También será la memoria compartida entre los diferentes controladores. En lugar de escribir [$scope.app.champ=value] en todas partes, el puntero [$scope.app] se asigna a la variable [app] y entonces se escribirá [app.champ=value]. Solo hay que recordar que [app] es el modelo expuesto en la página maestra;
- línea 11: [app.debug.on] es un valor booleano que controla el modo debug de la aplicación. Por defecto está en true. Su valor está vinculado a la casilla de verificación [debug] de las barras de navigation;
- línea 15: [app.navbarrun.show] controla la visualización de la siguiente barra de navigation:
![]()
- línea 16: [app.navbarstart.show] controla la visualización de la barra de navigation siguiente:
![]()
- línea 18: [app.errors] es la plantilla del banner de errores;

- línea 22: [app.view] contendrá información sobre la vista actual, la que se muestra actualmente mediante la etiqueta [ng-view] de la página maestra. En ella anotaremos la siguiente información:
- [url]: el URL de la vista actual, por ejemplo, [/agenda];
- [model]: la plantilla de la vista actual, por ejemplo, [$scope.agenda];
- [done]: a vrai indica que la vista actual ha terminado su trabajo y que se está pasando a otra vista;
Esta información sirve para el control de navigation.
- línea 24: inicia una tarea asíncrona, una espera simulada. La tarea asíncrona se referencia mediante dos punteros [app.view.model.task.action] y [app.task];
Se han factorizado dos métodos en el controlador [appCtrl]:
// cancelar tarea actual
app.cancel = function () {
...
};
// cierre de sesión
app.deconnecter = function () {
...
};
- línea 2: la función [app.cancel] sirve para cancelar la tarea actual para la que se muestra actualmente un mensaje de espera. Todas las vistas muestran este mensaje, por lo que la cancelación de la tarea se realizará aquí;
- línea 7: la función [app.deconnecter] devuelve al usuario a la página de autenticación. Todas las vistas, excepto la vista [/login], ofrecen esta posibilidad;
La función [app.deconnecter] es la siguiente:
// desconexión
app.deconnecter = function () {
// volvemos a la página de inicio de sesión
$location.path(config.urlLogin);
};
- línea 4: se vuelve a la página de inicio de sesión de URL [/login];
3.8.8. Gestión de la tarea asíncrona
En nuestra aplicación, en un momento dado, solo se ejecutará una tarea asíncrona. Es posible tener varias. Por ejemplo, al iniciar la aplicación, esta solicita al servicio web la lista de médicos y, a continuación, la de clients con dos solicitudes HTTP sucesivas. Se podría hacer lo mismo con dos solicitudes HTTP simultáneas. Angular ofrece las herramientas para esta gestión. En este caso, no hemos optado por ello.
La tarea en ejecución se cancela con el siguiente código en el controlador [appCtrl]:
// cancelación de la tarea actual
app.cancel = function () {
utils.debug("[app] cancel task");
// se cancela la tarea asíncrona de la vista actual
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
...
};
- línea 5: se busca la tarea en [app.view.model.task]. Además, todos los controladores se asegurarán de que sus tareas asíncronas sean referenciadas por este objeto;
- línea 6: para indicar que la tarea ha finalizado;
- línea 7: para finalizar la tarea con un error. Esta notación es diferente de la utilizada en los ejemplos de Angular estudiados:
- en los ejemplos, el objeto [task] era un objeto [$q.defer()] que se podía finalizar;
- en el version final, el objeto [task] es un objeto con los campos [action, isFinished], donde [action] es el objeto [$q.defer()] quese puede completar y [isFinished] un booleano que indica que la acción ha finalizado;
Analicemos el ciclo de vida del objeto [task] con un ejemplo. Al inicio, tras el controlador [appCtrl], es el controlador [loginCtrl] el que toma el relevo para mostrar la vista [views/login.html]. Su código de inicialización es el siguiente:
// se recupera el modelo padre
var login = $scope.login;
var app = $scope.app;
// vista actual
app.view = {url: config.urlLogin, model: login, done: false};
En la línea 5, tenemos [model=login]. Esto significa que cuando se modifica el objeto [login], se modifica el objeto [app.view.model] y, por lo tanto, [$scope.app.view.model]. Cuando en el controlador [loginCtrl] queremos realizar una espera simulada, escribimos:
// espera simulada
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
Al añadir el campo [task] al objeto [login], se ha añadido al objeto [$scope.app.view.model]. Si el usuario cancela la espera, el código en [appCtrl.cancel]:
// plantilla de la página actual
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
finalizará correctamente la espera simulada (líneas 4-6).
3.8.9. Control de navigation
Las reglas de navigation utilizadas en la aplicación son las siguientes:
cualquiera | sí | |
/login | sí si el controlador [loginCtrl] ha indicado que ha terminado su trabajo | |
/home | sí | |
/agenda | sí | |
/home | sí si el controlador [homeCtrl] ha indicado que ha terminado su trabajo | |
/resa | sí | |
/agenda | sí | |
/agenda | sí si el controlador [homeCtrl] ha indicado que ha terminado su trabajo | |
/resa | sí |
Esto se implementa con el siguiente código:
Para [agendaCtrl]:

- líneas 11-20: implementación de la regla de navigation;
- línea 26: nueva vista actual;
Para [resaCtrl]:

- líneas 12-20: implementación de la regla de navigation:
- línea 27: nueva vista actual;
Para [loginCtrl]:

- aquí no hay ningún control de navigation, ya que la regla establece que se puede acceder a URL [/login] desde cualquier lugar. Por lo tanto, si el usuario escribe este URL en su navegador, funcionará independientemente de la vista actual en ese momento;
- línea 16: la nueva vista actual;
El código para el controlador [homeCtrl] se ha proporcionado en el apartado 3.8.7.
Por último, para una regla como:
/home | sí, si el controlador [homeCtrl] ha indicado que ha terminado su trabajo |
he aquí un ejemplo de código que pasa de URL [/home] a URL [/agenda]:
![]() |
En el ejemplo anterior, nos encontramos en el método [afficherAgenda] del controlador [homeCtrl]. El usuario ha solicitado el agenda de un médico.
- línea 107: la promesa de la tarea HTTP;
- línea 109: la variable [app] se ha inicializado con [$scope.app]. Este último objeto se utiliza, como hemos visto, como modelo de la vista [app.html]. Esta plantilla [$scope.app] también se utiliza para almacenar la información que debe compartirse entre las vistas;
- línea 111: se analiza el código de error devuelto por la tarea;
- línea 113: el resultado [result.data] se coloca en la plantilla [app];
- línea 116: el controlador [homeCtrl] cederá el control al controlador [agendaCtrl]. Le indica que ha terminado su trabajo con el código de la línea 115. Este código será utilizado por el controlador [agendaCtrl] de la siguiente manera:

- línea 11: se recupera el objeto [$scope.app.view];
- línea 15: procesamiento del campo [$scope.app.view.done] inicializado por [homeCtrl];
3.8.10. Los servicios
![]() |
Los servicios [config, utils, dao] son los ya descritos en la presentación de Angular:
- el servicio [config] se presentó en el apartado 3.7.4;
- el servicio [utils] se presentó en el apartado 3.7.5;
- el servicio [dao] se presentó en el apartado 3.7.6;
A modo de recordatorio, se detalla la estructura de estos servicios:
Servicio [config]
![]() |
- en [1]: se observa que el código tiene unas 250 líneas. La parte esencial de este código es la externalización de las claves de los mensajes internacionalizados [2]. Se evita incluir estas claves de forma fija en el código;
Servicio [utils]
![]() |
- línea 8: aún no habíamos encontrado la variable [verbose]. Esta controla la función [debug] de la siguiente manera:
![]() |
- líneas 22-25: la función [utils.debug] no hace nada si [verbose.on] se evalúa como false. Esta variable está vinculada a una variable del controlador [appCtrl]:
![]() |
- línea 21: [app.debug] toma el valor del puntero [utils.verbose]. Por lo tanto, cualquier modificación realizada en [app.debug] se aplicará también a [utils.verbose];
- línea 22: el valor inicial de [app.debug.on] se toma del archivo de configuración. Por defecto, es el valor true.. Este valor puede cambiar con el tiempo. De hecho, el usuario tiene la posibilidad de cambiarlo en las barras de navigation:
![]() |
- línea 45: una casilla de verificación (type=checkbox) permite cambiar el valor de [app.debug.on] (atributo ng-model);
Servicio [dao]
![]() |
3.8.11. Las directivas
![]() |
Las directivas [errors, footable, list, waiting] son las ya descritas en la presentación de Angular:
- la directiva [footable] se introdujo en el apartado 3.7.8.6;
- la directiva [list] se introdujo en el apartado 3.7.12;
- las directivas [errors] y [waiting] se introdujeron en el apartado 3.7.14;
No habíamos encontrado la directiva [debug]. Es la siguiente:
![]() |
El archivo [debug.html] al que se hace referencia en la línea 11 es el siguiente:
- línea 2: la directiva [debug] muestra su plantilla en formato JSON en un banner de Bootstrap (línea 1);
Esta directiva solo se utiliza en la página maestra [app.html]:
![]() |
- La directiva [debug] se utiliza en la línea 35. Por lo tanto, muestra la forma JSON de la plantilla [$scope.app] cuando se está en modo debug (atributo ng-show). Esto da resultados como este:
![]() |
Esto requiere un buen conocimiento del código para poder interpretarlo, pero una vez adquirido, la información anterior resulta útil para la depuración. Aquí se han resaltado los elementos del modelo [$scope.app] mostrado. Recordemos que [$scope.app] es la memoria compartida por los controladores;
- [waitingBeforeTask]: el tiempo de espera simulado antes de cualquier solicitud HTTP;
- [debug]: el modo debug; es necesariamente true si se muestra este banner;
- [navbarrun]: valor booleano que controla la visualización de la barra de navigation siguiente:
![]()
- [navbarstart]: valor booleano que controla la visualización de la barra de navigation siguiente:
![]()
- [errors]: plantilla de la directiva [errors];
- [view]: encapsula información sobre la vista que se muestra actualmente;
- [waiting]: plantilla de la directiva [waiting];
- [serverUrl, username, password]: información de conexión al servicio web;
- [medecins]: plantilla para la directiva [list] aplicada a los médicos;
- [clients]: lo mismo que para clients;
- [menu]: controla las opciones de menú mostradas. Estas se definen en [navbar-run.html]:

Las opciones del menú se encuentran en las líneas 16, 23, 29 y 36.
- [formattedJour]: el día seleccionado en el calendario en formato «aaaa-mm-dd»;
- [agenda]: el agenda del médico. En este, hay franjas horarias libres (rv==null) y reservadas. Para estas últimas, aparece el nombre del cliente que ha reservado;
- [selectedCreneau]: la franja horaria elegida para realizar una reserva;
3.8.12. El controlador [loginCtrl]
![]() |
El controlador [loginCtrl] está asociado a la vista [views/login.html], que, combinada con la página maestra, genera la página siguiente:

El controlador [loginCtrl] es el siguiente:

- línea 13: [login] será la plantilla de la vista actual;
- línea 14: [app] es la memoria compartida entre los controladores;
- línea 16: se rellena [app.view] con la información de la vista actual;
Este código de inicialización se encontrará en cada controlador. Para el controlador C1 de una vista V1 que tiene el modelo M1, tendremos el siguiente código de inicialización:
- línea 18: quizá recordemos que [appCtrl] ha iniciado una espera simulada referenciada por el objeto [app.task.action]. Se utiliza el [promise] de esta tarea para esperar a que finalice;
- línea 39: el método [login.setLang] gestiona el cambio de idioma;
- línea 47: el método [login.authenticate] gestiona la autenticación del usuario;
Veamos los pasos principales del método de autenticación:

- líneas 50-51: [app.waiting] es la plantilla del banner de espera;
- línea 53: [app.errors] es la plantilla del banner de error;
- línea 55: se inicia una espera simulada. El objeto [action, isFinished] es referenciado por [login.task] y, por lo tanto, dado que [app.view.model=login], por [app.view.model.task]. Recordemos que esta es la condición para que la tarea pueda cancelarse;
- línea 57: una vez finalizada la espera simulada, se cargan los médicos;
- línea 62: cuando se ha obtenido la solicitud de los médicos, se analiza dicha solicitud. Si se han obtenido los médicos, se solicitan entonces los clients;
- línea 83: se analiza la respuesta obtenida y se muestra la vista final. Esto se hace con el siguiente código:

- línea 87: el valor booleano [task.isFinished] se establece en true en los siguientes casos:
- el usuario ha cancelado la espera;
- la solicitud de los médicos ha finalizado con un error;
- líneas 91-98: el caso en el que se ha obtenido clients;
- línea 93: [app.clients] es la plantilla de la directiva [list] que mostrará los clients en una lista desplegable;
- líneas 97-98: nos preparamos para cambiar de vista (línea 98), pero antes indicamos que el controlador ha terminado su trabajo (línea 97). Recordemos que [$scope.app.view.done] se utiliza para el control de navigation;
Lo importante aquí es que los médicos y los clients se han almacenado en la caché del navegador. A partir de ahora ya no se solicitarán al servicio web.
3.8.13. El controlador [homeCtrl]
![]() |
El controlador [homeCtrl] está asociado a la vista [views/home.html], que, combinada con la página maestra, genera la siguiente página:

La estructura del controlador [homeCtrl] es la siguiente:

- líneas 12-20: es el control de navigation. Todos los controladores lo tienen excepto [loginCtrl], ya que se puede acceder a la página [/login.html] sin condiciones;

- líneas 25-28: aquí encontramos líneas similares a las del controlador [loginCtrl]. [home] es, por tanto, la plantilla de la vista asociada al controlador;
- línea 33: un atributo que aún no habíamos visto. Es la plantilla de la barra de título de la vista:
![]()
- línea 36: [home.datepicker] es la plantilla del calendario;
- línea 38: [app.menu] es el modelo del menú de la barra de navigation. Aquí estará presente option [Agenda]. Es esta la que permite solicitar el agenda de un médico;
Por último, el controlador dispone de dos métodos:

La visualización del agenda (línea 51) se ha tratado en el apartado 3.7.8.
3.8.14. El controlador [agendaCtrl]
![]() |
El controlador [agendaCtrl] está asociado a la vista [views/agenda.html], que, combinada con la página maestra, genera la página siguiente:

La estructura del controlador [agendaCtrl] es la siguiente:

- las líneas 10-20 controlan navigation;

- líneas 23-26: [agenda] será la plantilla de la vista asociada al controlador [agendaCtrl];
- líneas 36-44: [app.titre] es la plantilla del siguiente banner de título:

- línea 46: el menú tendrá el option [Home / Accueil]:
![]()
Los métodos del controlador son los siguientes:

- línea 95: el método [agenda.supprimer] se ha tratado en el apartado 3.7.9;
El método [agenda.home] es un método de puro navigation:

El método [agenda.reserver] es el siguiente:

- línea 73: el parámetro de la función [reserver] es el n.º de la franja horaria (id);
- líneas 77-86: tienen como objetivo encontrar la franja horaria con este identificador;
- línea 82: el intervalo encontrado se almacena en la memoria compartida [app]. El controlador [resaCtrl], que tomará el control (línea 90), utilizará esta información para mostrar su banner de título;
- líneas 89-90: navigation a [/resa.html];
3.8.15. El controlador [resaCtrl]
![]() |
El controlador [resaCtrl] está asociado a la vista [views/resa.html], que, combinada con la página maestra, genera la página siguiente:

La estructura del controlador [resaCtrl] es la siguiente:

- líneas 12-20: el control de navigation;

- líneas 24-27: [resa] será la plantilla de la vista actual;
- líneas 38-45: [app.titre] es la plantilla del siguiente encabezado:

- línea 47: se muestran dos opciones de menú:
![]()
Los métodos del controlador son los siguientes:

El método [resa.valider] se ha analizado en el apartado 3.7.9.
3.8.16. Gestión de idiomas
Todos los controladores ofrecen el siguiente método [setLang]:

Podría haberse factorizado en el controlador [appCtrl].




























































































































