3. El cliente Angular JS
3.1. Referencias del marco de trabajo Angular JS
Al inicio de este documento se proporcionaron dos referencias sobre el marco de trabajo 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 el 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 propio. El de Adam Freeman tiene más de 600 páginas y ninguna de ellas es innecesaria. Describiremos una aplicación de Angular y, a lo largo de esta descripción, abordaremos los fundamentos de este marco de trabajo. Sin embargo, nos limitaremos únicamente a las explicaciones necesarias para comprender la solución propuesta. Angular es un marco de trabajo extremadamente completo y existen numerosas soluciones para llegar al mismo resultado. Esto supone una dificultad, ya que al comenzar no se sabe si se está utilizando una solución mejor o peor que otra. Este es el caso de la solución que se propone aquí. Podría escribirse de otra manera y tal vez siguiendo 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 MVC, con algunas diferencias. Una aplicación web Spring MVC, por ejemplo, tiene la siguiente arquitectura:
![]() |
El procesamiento de una solicitud de un cliente se lleva a cabo de la siguiente manera:
- solicitud: los URL solicitados tienen el formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&.... La clase [Dispatcher Servlet] de Spring es la que procesa los URL entrantes. Esta «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 encontró 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 provenir de varias fuentes:
- de la ruta [/param1/param2/...] de la URL,
- los parámetros [p1=v1&p2=v2] del URL,
- de los parámetros enviados por el navegador junto con su solicitud;
- al procesar la solicitud del usuario, la acción puede requerir 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 pudo procesar correctamente
- una página de confirmación en caso contrario
- la acción solicita que se muestre una vista específica: [3]. Esta vista mostrará datos que se denominan el modelo de la vista. Este es el 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 una sola página (APU) o Single Page Application (SPA):

- El usuario solicita la página inicial de la aplicación (URL) de la siguiente forma: http://machine:port/contexte. El navegador consultará a un servidor web para obtener el documento solicitado. Este es una página HTML con estilo definido mediante CSS y dinamizada mediante JavaScript;
- luego, 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. Estas son manejadas por el JavaScript integrado;
- aquellas que requieren datos provenientes de un servicio web remoto. Estos se recuperarán mediante una llamada AJAX (Asynchronous JavaScript and XML), se construirá un modelo y se mostrará una vista;
- aquellas que requieren una vista distinta a la inicial. Se solicitará mediante una llamada Ajax al servidor que proporcionó la página inicial. Luego, se repetirá el proceso anterior. La página obtenida se almacenará en la caché del navegador. En la próxima 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, dirigidas al servidor de páginas HTML o a servicios web remotos, las realiza el JavaScript integrado en las páginas.
Ahora 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. Para facilitar la explicación, podemos considerar que todas ellas se encuentran 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 que este muestra;
- en [3], se buscan los datos en la red, a menudo a través de servicios web;
El usuario interactúa con las vistas: llena formularios y los valida. Explicaremos este proceso con la vista V1 anterior. Supondremos que es la vista inicial de la aplicación. Se obtuvo de la siguiente manera:
- el usuario solicita la vista inicial de la aplicación, URL, en el formato: http://machine:port/contexte;
- el navegador solicitó el documento asociado a esta URL. Recibió la página HTML / CSS / JS desde la vista V1;
- el JavaScript integrado en la página tomó entonces el control y se lo cedió al controlador C1 [5];
- este último construyó el modelo M1 [8] [9] de la vista V1. La creación de este modelo pudo haber requerido 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 llena y luego 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 implica un simple cambio en la vista V1 (ocultar o mostrar campos), el controlador C1 modificará el modelo M1 de la vista V1 y luego 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 a AJAX para obtenerlos;
- en [8] y [9], se modificará el modelo 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 vista 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 procesado por un enrutador configurado de tal manera que a cada URL interno le corresponde una vista V y su controlador C. El enrutador entonces hace que se muestre la nueva vista Vn. Antes de la visualización, su controlador Cn toma el control, construye el modelo Mn y luego hace que se muestre 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 manejadores 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, por lo general se utiliza la siguiente arquitectura en capas:
![]() |
En el ejemplo anterior, la capa [web] se comunica con la capa [DAO] únicamente 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 no lo hacemos.
Con Angular, no nos lo prohibimos. La arquitectura queda entonces así:
![]() |
- en [1], la capa [présentation] puede comunicarse directamente con cualquier servicio;
- en [2], los servicios se reconocen entre sí. Un servicio puede utilizar uno o varios otros.
3.3. Las vistas del cliente Angular
Las vistas del cliente Angular ya se presentaron en el párrafo 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 programar citas con médicos;
- en [7], una casilla de selección que permite estar o no en el 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 predeterminado 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 observar 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 nombre de usuario y la contraseña de quien desea utilizar la aplicación. Hay dos usuarios: admin/admin (nombre de usuario/contraseña) con un rol (ADMIN) y user/user con un rol (USER). Solo el rol ADMIN tiene permiso para usar la aplicación. El rol USER solo está ahí para mostrar qué 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, que es el predeterminado, y el inglés.
![]() |
- en [1], nos conectamos;
![]() |
- una vez que inicias sesión, puedes elegir el médico con el que quieres una cita [2] y el día de la misma [3];
- en [4], se solicita ver la agenda del médico elegido para el día seleccionado;
![]() |
- una vez obtenida la agenda del médico, se puede reservar un horario [5];
![]() |
- en [6], se selecciona al paciente para la cita y se confirma esta selección en [7];
![]() |
Una vez validada la cita, se regresa automáticamente al calendario, donde ya aparece registrada la nueva cita. Esta cita se podrá eliminar posteriormente en [7].
Ya se han descrito las principales funciones. Son sencillas. Las que no se han descrito son funciones de navegación para regresar a una vista anterior. Terminemos con la gestión del idioma:
![]() |
- en [1], se cambia del francés al inglés;
2

- a [2], la vista pasa a inglés, incluido el calendario;
3.4. Configuración del proyecto Angular
Vamos a desarrollar nuestro cliente Angular de manera gradual. 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 creamos;
- en [3], obtenemos un proyecto vacío de WebStorm;
![]() |
- en [4], la configuración del proyecto se realiza mediante la opción [File / Settings];
- En [5] y [6], se configura la propiedad [Spelling], que controla la revisión ortográfica. Por defecto, esta función está activa. Como 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], se modifica el archivo generado tal como se muestra en [11];
- en [12], guardamos 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 de JavaScript que necesitamos;
- en [15], selecciona el archivo [bower.json] que acabamos de crear;
![]() |
- en [16], agreguemos una biblioteca de JavaScript;
- en [17] se muestran todas las bibliotecas de JavaScript que se pueden descargar;
- en [18], podemos establecer un filtro para refinar la lista de [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 versión 1.2.18 de Angular;
- en [20], se está descargando;
![]() |
- en [21], se ve que ya se ha descargado;
- en [22], se ve la versión descargada. Por lo tanto, en realidad es la 1.2.19;
- en [23], se ve la última versión 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í se utilizará su calendario; | ||
el marco de trabajo 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 instalaron en la carpeta [bower_components];
- en [26], se observa que se ha descargado la biblioteca JQuery. Esto se debe a que Bootstrap la utiliza. El sistema de instalación de las dependencias de JavaScript de un proyecto es similar al de Maven en el mundo de 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 una primera versión 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 por 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>
<!-- Núcleo de Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
- 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 de Bootstrap;
- línea 21: los componentes de Bootstrap funcionan con 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 las aplicaciones Angular;
- línea 30: el archivo JS del módulo [angular-base64];
Se puede verificar la validez del archivo [app-01.html]:
![]() |
- en [1], se solicita la revisión del código;
- en [2], el resultado cuando todo está bien;
Se recomienda esta inspección sistemática del código antes de su ejecución. En este caso, esta detección permite identificar 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] fue 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í también se muestran 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 esos cambios en el navegador sin tener que recargar la página. Así, si agregamos la línea 3 a continuación:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2>Version 1</h2>
</div>
y regresamos al navegador, observamos que la página ha cambiado:
![]() |
3.6. Introducción a Bootstrap
Ahora vamos a ilustrar algunas de las características de Bootstrap utilizadas en la aplicación. Solo tengo un conocimiento limitado de este marco de trabajo, obtenido al copiar y pegar códigos que encontré 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 recaban información del exterior son asincrónicas. Esto significa que la operación se inicia y hay 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, a su vez, puede enriquecer la vista actual o cambiarla. Si la operación puede tardar mucho tiempo, es útil ofrecer al usuario la posibilidad de cancelarla. Se la ofreceremos sistemáticamente. Para ello, utilizaremos un banner de Bootstrap:

Para lograr 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 con 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 asigna un color específico. 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 lograr este resultado, duplicamos [app-01.html] en [app-03.html] y modificamos las siguientes líneas:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- Jumbotron de Bootstrap -->
<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 contarán con 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 de depuración -->
<label style="width: 100px">
<input type="checkbox">
<span style="color: white">Debug</span>
</label>
<!-- formulario de inicio de sesió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] definirá el estilo de la barra de navegación. La clase [navbar-inverse] le da un fondo negro. La clase [navbar-fixed-top] se encargará de que, al desplazarse por la página que muestra el navegador, la barra de navegación 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 un área «adaptable» de la barra de comandos. En un teléfono inteligente, esta área desaparece y se integra en un menú;
- línea 16: la clase [navbar-form] da estilo a un formulario de la barra de comandos. La clase [navbar-right] lo desplaza a la derecha de este;
- 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 una lista desplegable:

Para obtener este resultado, duplicamos [app-01.html] en [app-05.html] y agregamos 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 agregadas son las líneas 4 a 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 su 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 solo 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 del menú desplegable son elementos de una lista no ordenada;
3.6.5. Ejemplo 5
Para enviar un formulario o navegar, el usuario tendrá en la barra de control opciones o botones como los que se muestran a continuación:
![]() |
Se han implementado opciones de menú en [1]. Para lograr este resultado, duplicamos [app-01.html] en [app-06.html] y agregamos 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 del menú se obtienen en las líneas 8 a 29. Se trata, una vez más, de elementos de una lista <ul>. La clase [active] hace que el texto aparezca resaltado, lo que indica que se puede hacer clic en la opción.
3.6.6. Ejemplo 6
Presentaremos a los médicos y a los clientes 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 lograr este resultado, duplicamos [app-01.html] en [app-07.html] y agregamos 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 aplicar un 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 clásicas HTML;
- línea 22: hay que importar el JS desde [bootstrap-select];
- líneas 24-26: un script JS que se ejecuta al finalizar 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 la agenda de un médico, utilizaremos una tabla «responsiva» 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 siguiente línea. A esto se le llama un componente «responsivo» o, simplemente, adaptable.
Duplicamos [app-01.html] en [app-08.html] y agregamos 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-*] provienen 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 expande o contrae 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 un teléfono. También se puede utilizar el valor «tablet»;
3.6.8. Ejemplo 8
Para ayudar al usuario, crearemos burbujas de ayuda (tooltip) alrededor de los componentes principales de las vistas:
![]() |
Para lograr este resultado, duplicamos [app-01.html] en [app-09.html] y agregamos 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>
<!-- Núcleo de Bootstrap JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- script local -->
<script>
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>
Las burbujas de ayuda son proporcionadas por la biblioteca [angular-ui-bootstrap], que a su vez se basa en la biblioteca [angular]. La línea 50 importa la biblioteca [angular-ui-bootstrap]. Para implementar los componentes de la biblioteca [angular-ui-bootstrap], debemos crear un módulo de Angular. Esto se hace en las líneas 52 a 55. Estas líneas definen un módulo de Angular llamado [rdvmedecins] (primer parámetro). Un módulo de Angular puede utilizar otros módulos de Angular. A esto se le llama dependencias del módulo. Se proporcionan en una matriz como segundo parámetro de la función [angular.module]. En este caso, el módulo llamado [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 de Angular. Por defecto, esto no tiene ningún efecto en la página. Se indica que la página debe ser administrada por Angular, vinculándola a un módulo de 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 agrega 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 agregar 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 agregamos 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á vinculada a un módulo de 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 el modelo 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 modifique las fechas, estos cambios se mostrarán de inmediato 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 hay 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 llamado [date]. Este filtro viene predefinido en Angular. Sirve para dar formato a las fechas. Admite parámetros que especifican el formato deseado. Así, la expresión {{día | fecha:'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. Próximamente abordaremos su internacionalización.
3.6.10. Conclusión
Hemos presentado los elementos del marco de trabajo CSS Bootstrap que utilizaremos. 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ó del lado del servidor, ciertos marcos de trabajo son imprescindibles del lado del cliente. Este es el caso del marco de trabajo Angular JS, que trae consigo una nueva forma de abordar el desarrollo de aplicaciones de JavaScript ejecutadas por un navegador. Lo presentaremos ahora.
3.7. Descubriendo Angular JS
Ahora ilustraremos algunas de las características del marco 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 observa el modelo M y actualiza automáticamente la vista V cuando se produce un cambio en su modelo M. El valor de una variable del modelo M se muestra en la vista V mediante:
Comenzaremos profundizando 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 creado por el controlador C1. Este último contiene no solo el modelo M1, sino también los manejadores de eventos de la vista V1. Nos encontramos en el ciclo 5, 8, 9:
- [5]: se produce un evento en la vista V1. Este es procesado por el controlador C1;
- este realiza su tarea [6-7] y luego construye el modelo M1 [8];
- [9]: la vista V1 muestra el nuevo modelo M1. Como ya dijimos, este último paso es automático. No hay, como en otros marcos de trabajo, un «push» explícito (C1 envía la plantilla M1 a V1) ni un «pull» explícito (la vista V1 recupera el modelo M1 de C1). Existe un envío 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 vimos 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 ellos el atributo [min-date], que establece la fecha mínima que se puede seleccionar en el calendario. Esto nos será ú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 el 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>
<!-- núcleo de Bootstrap 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>
Analicemos primero el script local de las líneas 26 a 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 a 35: creación de un controlador. Este será el que albergue la plantilla de nuestra página. Aquí no habrá ningún manejador de eventos;
- líneas 30-31: el controlador [rdvMedecinsCtrl] pertenece al módulo [rdvmedecins]. Se pueden agregar tantos controladores como se desee a un módulo. En nuestra aplicación tendremos:
- un módulo de administración de la aplicación;
- un controlador por vista;
- el segundo parámetro de la función [controller] es un array con 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 de Angular:
![]() |
En el ejemplo anterior, el controlador C1 contiene todos los manejadores de eventos de la vista V1, así como el modelo M1 de esta última. Los manejadores 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 un único ejemplar de cada uno. Se identifican por 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, ese es el 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 en el proceso de minificación. Cuando Angular construya el controlador con la nueva matriz, reemplazará 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 agregar 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 manejadores de eventos del controlador c1;
- el contenido de [div11] (líneas 3-4) muestra la plantilla M11 administrada por el controlador c11, pero también la plantilla M1. Existe herencia de plantillas. Las etiquetas de esta zona pueden hacer referencia tanto a los administradores de eventos del controlador c11 como a los del controlador c1. No pueden hacer referencia ni a la plantilla M12 del controlador c12 ni a los administradores de eventos de este. De hecho, el controlador c12 no se conoce entre las líneas 3 y 5;
- líneas 7-9: se puede aplicar un razonamiento similar 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] del modelo. 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 sirve de mucho, ya que es un calendario inglés. Es posible adaptarlo a la configuración regional:
![]() |
- en [1], tenemos un calendario en francés;
- en [2], lo cambiamos a 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 agregaron las líneas 21 a 31 de la lista desplegable de idiomas. Por primera vez, encontramos un manejador de eventos en las líneas 27 y 28:
- línea 27: el atributo [ng-click] es un atributo de Angular que indica el controlador 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')], la cual configurará el calendario en francés;
- línea 28: aquí configuramos el calendario en inglés;
- línea 35: dado que el código JavaScript del controlador es bastante extenso, lo colocamos en un archivo llamado [rdvmedecins.js];
Angular maneja la localización de las vistas con un módulo llamado [ngLocale]. La definición de nuestro módulo [rdvmedecins] será, por lo tanto, la siguiente:
// --------------------- módulo Angular
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);
En la 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, particularmente difícil de detectar. Aquí tenemos una nueva dependencia del módulo [ngLocale].
Por defecto, Angular solo maneja la localización de fechas, números, etc., que tienen variantes locales. No maneja 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 haya 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;}
});
}]);
Aquí 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 el inglés de USA (en-us).
El código anterior no es muy fácil de leer. Al leerlo con atención, nos damos cuenta de que todo este código define la variable [$locale] de la línea 4. Al cambiar el valor de esta variable se logra 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 al importar 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 buscar en la web una solución a este problema, no encontré ninguna. Aquí propongo una: [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. La idea es colocar las diferentes configuraciones regionales que necesitamos en un diccionario. Ahí es donde las buscaremos cuando sea necesario 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 a 215 más 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 maneja 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, la versión en francés de la configuración regional. No se puede escribir simplemente [$locale=locales['fr']], ya que eso asignaría 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] del modelo se inicializa con la fecha de hoy. Esto hace que el calendario se muestre con esa fecha seleccionada;
- líneas 223-230: definen el manejador de eventos que se invoca al cambiar el idioma. Cabe destacar la sintaxis:
para definir un manejador de eventos que se llamaría [nom_fonction] y que aceptaría los parámetros [param1, param2, ...];
Recordemos el código HTML de la lista desplegable:
<!-- 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>
- línea 8: al seleccionar el francés, se llama a [setLang('fr')];
- línea 9: al seleccionar el inglés, se invoca [setLang('en')];
- línea 3: el atributo [is-open] es un valor booleano que controla si la lista desplegable está abierta (true) o cerrada (false). Se inicializa con la variable [isopen] del modelo de la vista;
Volvamos al código de [rdvmedecins.js]:
- línea 225: se cambia el valor de la variable [$locale] por el valor correspondiente del diccionario [locales];
- línea 227: se ha establecido 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 cambió el valor de la variable [$locale], que no forma parte del modelo M mostrado por la vista V. Es necesario encontrar una forma de cambiar este modelo M para que el calendario se actualice y utilice su nueva configuración regional. Aquí, modificamos la variable [jour] del modelo del calendario. La inicializamos 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 que muestra el calendario. Con este número, reconstruimos una nueva fecha. Por supuesto, obtendremos la misma fecha y el calendario seguirá 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 ser false, lo que provocará que se cierre 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 de [Calendrier, Langues]. Por defecto, Angular no ofrece una herramienta para la internacionalización de mensajes. Aquí utilizaremos la biblioteca [angular-translate] (https://github.com/angular-translate/angular-translate).
Desarrollaremos 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 de i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// mensajes en francés
$translateProvider.translations("fr", {
'msg_header': 'Consultorio médico <br/> Les Médecins Associés',
'msg_langues': 'Idiomas',
'msg_agenda': 'Agenda de {{título}} {{nombre}} {{apellido}}<br/>el {{día}}',
'msg_calendrier': 'Calendario',
'msg_jour': 'Día seleccionado: ',
'msg_meteo': "Hoy va a llover..."
});
// mensajes en inglés
$translateProvider.translations("en", {
'msg_header': 'The Associated Doctors',
'msg_langues': 'Idiomas',
'msg_agenda': "Diario de {{título}} {{nombre}} {{apellido}}<br/> el {{día}}",
'msg_calendrier': 'Calendario',
'msg_jour': 'Día seleccionado: ',
'msg_meteo': 'Hoy lloverá...'
});
// idioma predeterminado
$translateProvider.preferredLanguage("fr");
}]);
- línea 2: la primera modificación es agregar 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 de Angular, el marco de trabajo instancia todos los servicios necesarios para la aplicación, tanto los predefinidos por 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 que se instancie cualquier servicio. Se puede utilizar para definir información de configuración de los servicios que se instanciarán posteriormente. En este caso, 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 un array ['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í se ha 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 {'clave1':'mensaje1', 'clave2':'mensaje2', ...};
- 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 predeterminado. Su parámetro es uno de los argumentos utilizados como primer parámetro de la función [$translateProvider.translations], por lo que aquí puede 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), es 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:
Consultorio médico<br/>Los Médicos Asociados | Los Médicos Asociados | |
Calendario | Calendario | |
Idiomas | Idiomas | |
Día seleccionado: | Día seleccionado: |
Ahora veamos 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 configurado que se debe utilizar;
- msg.model: es el diccionario que proporciona los valores de los parámetros;
Los nombres de los campos [text, model] pueden ser cualesquiera. 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;
- Diario de {{título}} {{nombre}} {{apellido}}<br/> el {{día}} en el diccionario de inglés;
Por lo tanto, el mensaje que se 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. Existe una dificultad con el parámetro [jour]. Queremos mostrar el nombre completo del día. Este varía según sea en francés o en inglés. Por lo tanto, utilizamos el filtro [date], que ya se utilizó 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' es 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 realizaron en la vista mediante atributos del módulo [pascalprecht.translate]. También se puede optar por realizar esta traducción del 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» también es un filtro. Aquí se solicita el mensaje de clave «msg_meteo».
Analicemos el mecanismo de cambio de idiomas. Ya vimos que la función [config] de configuración del módulo [rdvmedecins] había designado el francés como idioma predeterminado (línea 9 a continuación):
// configuración de 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 existe ninguna relación entre la internacionalización de los mensajes que implementa 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 momento de ver qué sucede 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; modifican la variable [$locale] del calendario. Esto no afecta en absoluto 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, ya sea '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. Este no será el caso de la expresión {{msg2}}, que no cuenta con 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 mantuvo 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 encontrar explicaciones antes de buscar 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 mantuvo 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, se puede forzar esta reevaluación en el controlador:
// ------------------- administrador de eventos
// cambio de idioma
$scope.setLang = function (lang) {
...
// se actualiza msg2
$scope.msg2 = $filter('translate')('msg_meteo');
// y la fecha del mensaje
$scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};
Cada vez que se cambia de idioma, la línea 8 anterior vuelve a evaluar el día que se muestra. Esto resuelve efectivamente el primer problema, pero no el segundo (el día que se muestra 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 al hacer clic en el calendario se debe ejecutar la función [$scope.calendarClick]. Esta es la siguiente:

- línea 267: el controlador 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 de Angular puede convertirse en un servicio:
- un servicio sigue una sintaxis específica. Tiene un nombre y Angular lo reconoce por 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. Como un servicio puede ser inyectado en otro servicio, resulta tentador realizar la configuración en un servicio al que llamaremos [config] e inyectar este último en los servicios y controladores que se deben 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 selección que controla si se muestra o no el mensaje de espera de las líneas 9-15. El valor de la casilla de selección se almacena 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 le damos el valor «true» a la variable [waiting.visible], la casilla quedará marcada. Existe 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:
- se asigna el valor true 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á visible (ng-show, línea 9);
- el razonamiento es similar 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 tenía 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 breve 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);
Por lo 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 traducirán;
- 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 tardar mucho tiempo. Aquí se establece en 1 segundo el tiempo máximo de espera para la respuesta del servicio web. Pasado 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 establece aquí en milisegundos. Una espera de 0 significa que no hay espera. La aplicación se diseñará de tal manera 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 establecerí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] ahora se encuentra 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 cancelar, se invoca el método [$scope.waiting.cancel()]. Al final, es la función privada cancel de la línea 316 la que se ejecuta. Esta simplemente oculta el mensaje de espera al establecer en «false» la variable del modelo [waiting.visible] (línea 318);
3.7.5. Ejemplo 5: programación asíncrona
Ahora presentamos 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 de utilidad. Presentaremos dos de ellos;
- [dao]: el servicio de acceso al servicio web de programación de citas. Lo presentaremos próximamente;
Vamos a escribir la siguiente aplicación:
![]() |
![]() |
- Se trata de mostrar el banner [2] durante un tiempo establecido 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 de Angular. Esto evita que el campo se muestre brevemente antes de que se evalúe el atributo [ng-show], el cual, de hecho, provocará que se oculte;
- 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 de Angular que administra 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 desarrollar;
- línea 315: el controlador [rdvmedecinsCtrl] que vamos a desarrollar;
Agregamos 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': "Tiempo de espera: "
});
// mensajes en inglés
$translateProvider.translations("en", {
...
'msg_waiting_time_text': "Tiempo de espera:"
});
// idioma predeterminado
$translateProvider.preferredLanguage("fr");
}]);
Agregamos 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 después de que haya transcurrido un cierto tiempo. El servicio [$q] permite crear tareas asincrónicas;
- 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 asignado 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, si corresponde, 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 termina mediante uno de los dos métodos:
- [task.resolve(value)]: que finaliza la tarea con éxito y devuelve el valor [value] a quienes esperan que termine la tarea;
- [task.reject(value)]: que finaliza la tarea con error y devuelve el valor [value] a quienes esperan 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 que termine la tarea. La tarea continúa ejecutándose;
Quienes desean esperar a que termine 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 manejar 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 fracaso. Se puede omitir la función de fracaso. La función [successCallback] solo se ejecutará al finalizar la tarea [task] con éxito [task.resolve()]. La función [errorCallBack] solo se ejecutará al finalizar la tarea [task] con un error en [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 ambas 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 después de un cierto 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: al finalizar el tiempo de espera de [milliseconds], la tarea se completa 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 a 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 () {
// fallo
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 agregado 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 que muestra 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 termine. 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 en que finaliza la ejecución del método;
- línea 29: muestra que la operación se ha cancelado. Esto ocurre únicamente 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á después de que se haya ejecutado 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 a 34 se ejecutan sin esperar los 3 segundos. Al finalizar 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 esta finalización (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 exitosa). Luego será el turno de las líneas 11 a 14. Una vez ejecutado este código, se regresa a la vista V, la cual 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 ven son las siguientes:
Como se observa arriba, hay un lapso 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, aparece lo siguiente:
Para terminar, es importante comprender que en todo momento solo hay un hilo de ejecución llamado el 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 de inmediato. Se coloca en la fila 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 responde a las acciones del usuario. Por eso, 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 todo momento, solo se ejecuta el hilo de UI.
3.7.6. Ejemplo 6: los servicios HTTP
Ahora 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 está visible (ng-show="medecins.show").
- línea 35: una alternativa a la sintaxis <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}"> que ya hemos visto;
- línea 36: una lista no ordenada;
- línea 37: la lista de médicos se encuentra en la plantilla [medecins.data]. La directiva de 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 escriben el título, el nombre y el apellido 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 de Angular [ng-repeat];
- línea 51: el código JavaScript ahora se encuentra en el archivo [rdvmedecins-04]
3.7.6.2. El controlador C y el modelo M
![]() |
El código de JavaScript cambia 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. Ya no los presentaremos;
- líneas 69-70: el servicio [config] que configura nuestra aplicación. Se añaden nuevas claves de mensaje. Ya no las presentaremos;
- líneas 318-319: el servicio [utils], que contiene métodos de utilidad. Se añaden nuevos métodos. 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 porque es él quien coordina todo 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, datos: [med1, med2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
if (result.err == 0) {
// se introducen los datos obtenidos en el modelo
$scope.medecins.data = result.data;
// se actualiza el UI
$scope.medecins.show = true;
$scope.waiting.visible = false;
} else {
// Se produjeron 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; el cuarto se almacena 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 no está definido el atributo [medecins.data], que 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 no está definido el atributo [errors.messages], que contendrá la lista de mensajes de error que se mostrarán (línea 5).
- línea 16: la tarea asincrónica. El controlador iniciará sucesivamente dos tareas asincrónicas. Las referencias a estas tareas sucesivas se almacenará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) al cabo del tiempo ingresado 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 tener la referencia de la tarea misma para poder cancelarla (línea 55);
- líneas 28-32: se define el trabajo que se debe realizar una vez que 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 iniciar sesión, por ejemplo, [admin];
- el URL que proporciona 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, apellido),
- {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];
- por lo tanto, 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á de inmediato;
- líneas 34-50: de la explicación anterior se desprende 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 es construido por el método [dao.getData] y transmitido 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, apellido),
- {err: n, mensajes: [msg1, msg2, ...]}, donde [msgi] es un mensaje de error y n es distinto de 0;
- línea 37: se revisa el código de error [result.err];
- líneas 38-42: si no hay error (result.err == 0), entonces se recupera la lista de médicos y se muestra;
- líneas 44-47: si, por el contrario, hay un error (result.err ≠ 0), entonces 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 asincrónicas no hayan finalizado. Veamos qué sucede 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. En ese momento, solo se ha iniciado una tarea asincrónica, la de la línea 25;
- tras esta primera ejecución, se actualiza la vista V y, por lo tanto, se muestra el banner de espera y su botón de cancelación. Si el usuario cancela la espera antes de que la tarea de la línea 25 haya finalizado, 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 todo lo demás se oculta,
- luego se regresa a la vista V y el navegador procesa el siguiente evento. Dado que la tarea ha finalizado, se obtiene la promesa de dicha tarea, lo que genera un evento. Este evento se procesa;
- luego se ejecutan las líneas 28-32. No hay ninguna función definida para el caso de error, 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 regresa a la vista V y el navegador procesará el siguiente evento. Dado que la promesa [promise] de la línea 28 ya se ha procesado, se resolverá la de la línea 34, lo que provocará un nuevo evento. Este se procesa;
- las líneas 34 a 49 se ejecutarán a su vez, ya que se ha cumplido la promesa utilizada en la línea 34. Una vez más, 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 ocurre 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 a 50 con un final de tarea fallido. Pronto veremos 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 asincrónica. El estado de reposo del navegador es la visualización de la vista V. Sale de este estado de reposo cuando ocurre un evento, el cual procesa a continuación. Tan pronto como se ha procesado el evento, regresa 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 con el siguiente evento.
Todo ocurre en un único hilo. Nunca se procesan dos eventos 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 ya 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 los errores en la respuesta del servidor JSON
function getErrors(data) {
// datos {err:n, mensajes:[]}, err!=0
// errores
var errors = [];
// código de error
var err = data.err;
switch (err) {
case 2 :
// no 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 genera 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 esta razón, no son los mensajes en sí los que se colocan en la tabla, sino sus claves de internacionalización, excepto 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, las cuales no se encontrarán. En este caso, el módulo [translate] muestra la clave de mensaje que no encontró, es decir, en este caso, un mensaje real. Este es el resultado deseado;
- líneas 32-34: tratan el caso en el que [data.messages], en la línea 27, es igual a null. Esto ocurre con el servicio web escrito. Se debería 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 de la solicitud HTTP
var url = serverUrl + urlAction;
// autenticación básica
var basic = "Basic " + $base64.encode(username + ":" + password);
// la respuesta
var réponse;
// todas las solicitudes HTTP deben estar 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);
}
// fallo
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:
// no encontrado
error = 6;
break;
case 0:
// error local
error = 4;
break;
default:
// otra cosa
error = 5;
}
// se está generando 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 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 la comunicación 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 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 reúne 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 que Angular envía por defecto 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 agrega a los encabezados HTTP que se enviarán sistemáticamente. A la izquierda de la asignación, se encuentra el encabezado [Authorization] que se debe 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 el 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]: definida en la línea 34, es el método que se debe ejecutar cuando se cumple la promesa tras el éxito de la tarea;
- [failure]: definido en la línea 45, es el método que se debe ejecutar cuando se obtiene la promesa tras el fracaso de la tarea;
- ambos métodos (se debería decir funciones) están definidos 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. Aquí hay que recordar el código que realiza la 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 adelante, cuando la llamada a 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 realizó 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: se recupera 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, apellido),
- {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 ocurrió en el servidor. Este lo gestionó, generó 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 procesan 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, apellido),
- {err: n, mensajes: [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 que llama al controlador:
// se analiza el resultado de la llamada anterior
promise.then(function (result) {
// resultado={err: 0, datos: [med1, med2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
...
}
La respuesta de [task.resolve(réponse)] se encuentra más arriba en la variable [result].
- línea 45: la función [failure] cuando la tarea asíncrona termina con un error. Hay dos casos posibles:
- el servidor indica este error devolviendo un código que no es ni 2xx ni 3xx,
- Angular cancela la llamada HTTP. En ese caso, no se realiza ninguna llamada. Hay una excepción de Angular, pero el servidor no devuelve un código de error HTTP. Este es el caso, por ejemplo, si se proporciona un URL inválido que no se puede invocar;
- 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"}
En la línea 48, se recupera el atributo [status] mencionado anteriormente;
- líneas 50-70: a partir del código de error HTTP, se generará un nuevo código de error para ocultar a los códigos que lo invocan la naturaleza HTTP del método [dao.getData]. Se puede verificar que, en el controlador que utiliza este método, nada sugiere que haya una llamada HTTP en el método;
- línea 51: el error [401] corresponde a un fallo en la autenticación (por ejemplo, contraseña incorrecta),
- línea 55: el error [403] corresponde a una llamada no autorizada. El usuario se ha autenticado correctamente, pero no cuenta con los permisos suficientes para solicitar el URL que ha solicitado. Esto ocurrirá con el usuario [user / user]. Este usuario sí 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 que no se encontró. El error puede tener varias causas:
- el usuario cometió un error al ingresar el URL del servicio;
- el servicio web no se ha iniciado;
- el servicio web no respondió lo suficientemente rápido (tiempo de espera predeterminado de un segundo);
- línea 63: el código de error HTTP 0 no existe. Nos encontramos en el caso en que Angular no realizó la llamada HTTP solicitada porque el URL ingresado 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, mensajes}, donde el array [messages] está formado únicamente por el mensaje [response.statusText]. En caso de que Angular no haya realizado la llamada solicitada HTTP, 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
Comencemos 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 revisar los registros de la consola de 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 reporta 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 hubo una llamada a HTTP. En consecuencia, [statusText] está vacío,
- [url] equivale a [http://localhost:8080/getAllMedecins], lo cual es correcto;
- el encabezado de autenticación HTTP de [Authorization":"Basic YWRtaW46YWRtaW4=] también es correcto;
Bueno, entonces, ¿por qué no funcionó? La frase clave de los registros es [No 'Access-Control-Allow-Origin' header is present]. Para entenderla, se necesita una explicación detallada. Comencemos 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 constituye una falla 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 le prohíbe a la aplicación Angular consultar al servidor [2]. En realidad, la aplicación lo consulta para preguntarle si autoriza que un cliente que no proviene de su propio dominio lo consulte. A esta técnica de intercambio se le llama 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 envió que el navegador se negó a realizar la llamada HTTP solicitada por la aplicación.
Ahora veamos los detalles. Analicemos los intercambios de red que tuvieron lugar durante la llamada HTTP. Para ello, en el navegador Chrome, presionamos [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 envió 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 clientes que no están en su dominio, debe devolver un encabezado específico llamado [Access-Control-Allow-Origin]. Y es precisamente porque no lo devolvió que Angular no ejecutó la llamada HTTP solicitada y devolvió el error:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. El recurso solicitado no cuenta con el encabezado «Access-Control-Allow-Origin». Por lo tanto, no se permite el acceso desde el origen «http://localhost:63342».
Por lo tanto, debemos modificar nuestro servidor para que envíe el encabezado esperado HTTP.
3.7.6.6. Modificación del servidor web / JSON
Volvemos a Eclipse. Para conservar lo logrado, duplicamos la versión actual 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 clientes ajenos al dominio del servidor;
- líneas 21-23: el método de acceso a esta información;
Luego 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 realizó la solicitud. Este objeto es inyectado por Spring;
- línea 30: se delega el procesamiento 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 clientes ajenos a su dominio, entonces se envía el encabezado HTTP:
Access-Control-Allow-Origin: *
lo que significa que el servidor acepta clientes de cualquier dominio (*).
Ahora estamos listos para nuevas pruebas. Lanzamos la nueva versión del servicio web y descubrimos que el problema persiste. No ha cambiado nada. Si en la línea 30 anterior colocamos una salida a la consola, esta nunca se muestra, lo que indica 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 predeterminado. Por lo tanto, siempre es Spring quien responde y nunca el método [getAllMedecins] de la línea 29. Este comportamiento predeterminado 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 de 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 los clientes. Es de tipo [DispatcherServlet]. Este servlet normalmente se crea de forma predeterminada. Si lo creamos nosotros mismos, entonces podemos configurarlo;
- línea 14: se crea una instancia de tipo [DispatcherServlet];
- línea 15: se le indica a la servlet que reenvíe a la aplicación los comandos HTTP y [OPTIONS];
- línea 16: devolvemos el servlet así configurado;
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
Lanzamos la nueva versión del servicio web /JSON e intentamos obtener la lista de médicos con nuestro cliente Angular. Revisamos los intercambios de red en la pestaña [Network]:
![]() |
- En [1], se puede observar que el encabezado HTTP [Access-Control-Allow-Origin: *] ahora está presente en la respuesta del servidor. Sin embargo, sigue sin funcionar. Revisamos 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» no está permitido por «Access-Control-Allow-Headers»
Se observa 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 viene 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] queda así:
// 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");
}
- Las líneas 6 y 7 agregan el encabezado que faltaba.
Reiniciamos el servidor y volvemos a solicitar la lista de médicos con el cliente Angular:
![]() |
Esta vez sí funciona. 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 devolvió un código de error [status=200] con el mensaje [statusText=OK]. Por eso estamos en la función [success];
- el servidor devolvió un objeto [data] con dos campos:
- [status]: (no debe confundirse con el código de error HTTP [status]). Aquí, [status=0] indica que URL y [/getAllMedecins] se procesaron sin errores;
- [data]: que contiene la lista JSON de médicos;
Veamos ahora otros casos interesantes:
Hay un error en los identificadores [login, password]:
![]() |
Nos conectamos 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 clientes
Retomamos la aplicación anterior para, en esta ocasión, presentar la lista de clientes en una lista desplegable de tipo [Bootstrap select] (véase el párrafo 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 clientes -->
<div class="row" style="margin-top: 20px" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
<!-- 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: el mensaje 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 mensaje de error no cambia;
- líneas 16-25: la visualización de los clientes se realiza en una lista desplegable con estilo definido por el 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 una opción es de tipo [Mme Julienne Tatou] y que el valor de la opción es de tipo [100], donde 100 es el identificador (id) del cliente que se muestra;
- línea 34: el código JavaScript se traslada 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, excepto en el controlador, que ahora está adaptado para proporcionar la lista de clientes:
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 clientes;
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, datos: [client1, client2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
if (result.err == 0) {
// se introducen los datos obtenidos en el modelo
$scope.clients.data = result.data;
// se actualiza el UI
$scope.clients.show = true;
$scope.waiting.visible = false;
// se aplica el estilo a la lista desplegable
$('.selectpicker').selectpicker();
} else {
// Se produjeron errores al obtener la lista de clientes
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// se actualiza el UI
$scope.waiting.visible = false;
}
});
};
// Se espera la cancelación
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 clientes;
- línea 9: [$scope.clients] será la plantilla del banner de clientes en la vista V;
- línea 30: ahora se utiliza URL [/getAllClients];
- líneas 35-36: las dos formas de respuesta que devuelve el método [dao.getData]. Ahora tenemos clientes en lugar de médicos;
- línea 44: una instrucción bastante poco común en un código de Angular. Se manipula directamente el DOM (Modelo de Objetos de Documento). Aquí se quiere aplicar el método [selectpicker] (que forma parte de [bootstrap-select.min.js]) a los elementos de DOM que tienen la clase [selectpicker] [$('.selectpicker')]. Solo hay uno: la lista desplegable:
<select data-style="btn-primary" class="selectpicker" select-enable="">
....
</select>
En el párrafo 3.6.6, se mostró que esto aplicaba el estilo a la lista desplegable de la siguiente manera:
![]() | ![]() |
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 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");
}
}
// lista de médicos
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
// lista de clientes
@RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
public void getAllClients(HttpServletResponse response) {
sendOptions(response);
}
}
- 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
Ahora estamos listos para una prueba. Iniciamos el servidor web y luego ingresamos valores válidos en el formulario de Angular. Obtenemos la siguiente respuesta:

Este mensaje de error aparece cuando Angular no pudo realizar la solicitud HTTP solicitada. En ese caso, hay que buscar las causas en los registros de la consola. Allí se encuentra el siguiente mensaje:
XMLHttpRequest cannot load http://localhost:8080/getAllClients. No se encuentra el encabezado «Access-Control-Allow-Origin» en el recurso solicitado. Por lo tanto, no se permite el acceso desde el origen «http://localhost:63342».
Un problema que creíamos resuelto. Veamos entonces los intercambios de red que se produjeron:

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

Los encabezados HTTP del CORS están presentes. Ahora analicemos 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 canceló;
Si lo relacionamos 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 las solicitudes HTTP y [OPTIONS]. Es muy extraño que este error aparezca ahora y no en la lista de médicos. No tengo una explicación.
Por lo tanto, hay que modificar nuevamente 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 clientes
@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 clientes
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 colocamos 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 insertar en este objeto los encabezados CORS;
3.7.7.6. Pruebas de la aplicación – 2
Lanzamos la nueva versión del servicio web y volvemos a solicitar la lista de clientes. Obtenemos la siguiente respuesta:
![]() |
- en [1], sí recibimos una respuesta, pero está vacía [2];
- en [3]: las comunicaciones de red se realizaron correctamente;
En los registros de la consola, el método [dao.getData] mostró la respuesta que recibió:
[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 clientes. Una vez verificado el código, se sospecha que el problema está en la siguiente instrucción, que no dominamos muy bien:
// Estilo de la lista desplegable
$('.selectpicker').selectpicker();
Comentamos la línea 2 y lo intentamos de nuevo. Entonces obtenemos la siguiente respuesta:
![]() |
Así que hemos localizado el problema. Es la aplicación del método [selectpicker] a la lista desplegable lo que causa el problema. Al revisar el código fuente de la página con error, encontramos lo siguiente:
![]() |
- se observa que en [1], la lista desplegable sí 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í y, por lo tanto, 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. Reemplazamos 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 hemos visto esta función, que permite ejecutar una función después de un cierto tiempo. Aquí, la ausencia de tiempo de espera equivale a un tiempo de espera nulo. Las líneas anteriores agregan un evento a la lista de espera de eventos del navegador. Cuando termine el procesamiento del evento actual (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á al principio de la lista y será procesado. El estilo [bootstrap-select] se aplica entonces a una lista desplegable ya llena. Veamos el resultado:
![]() |
Si volvemos a revisar el código fuente de la página que se muestra, encontramos lo siguiente:
![]() |
El botón [bootstrap-select], que antes estaba vacío, ahora contiene la lista de clientes.
3.7.7.7. Uso de una directiva
En el controlador C de la vista V, encontramos el siguiente código:
// se aplica estilo a la lista desplegable
$('.selectpicker').selectpicker();
Se está manipulando 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 verse como una extensión del lenguaje HTML. De esta manera, 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 acepta dos parámetros:
- (continuación)
- el primero es el nombre de la directiva [selectEnable];
- el segundo es un array ['obj1','obj2',..., function(obj1, obj2,...)], donde los [obj] son los objetos que se deben inyectar en la función. Aquí, 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: la plantilla 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]. Se debe prestar atención a la diferencia ortográfica entre el nombre de la directiva y el atributo asociado a ella. Se pasa de la forma [camelCase] a la forma [camel-case].
La directiva [selectEnable] también podría utilizarse de la siguiente manera:
Aquí, la directiva [doSomething] se aplica en forma de una 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, se escribirá attrs['selectEnable'] para obtener la información [data]. Tenga en cuenta el cambio en la notación [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: aquí encontramos el código que habíamos colocado anteriormente en el controlador. Este se ejecuta al encontrar la directiva [select-enable] (ya sea como elemento o como atributo) al mostrar 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 de directiva [app-17B.html] y el archivo [selectEnable.js] de la directiva. No hay que olvidar este último punto. Si falta el archivo de la directiva, el atributo [select-enable=""] no se gestionará, pero Angular no reportará 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 ahora la realiza 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 clientes estaba vacía y, por lo tanto, tenemos una lista desplegable vacía;
En otras palabras, la operación:
$('.selectpicker').selectpicker();
no se llevó 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 llenado. Para lograr 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>
En la 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. Debido a que 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á. Al evaluarse, las líneas 2 a 4 de la etiqueta <select> aún no se han evaluado. Por lo tanto, tenemos una lista de clientes 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 a la directiva [$timeout] de la línea 5, la línea 6 solo se ejecutará después de la evaluación completa de la vista V, es decir, en un momento en que la etiqueta <select> cuente con todos sus elementos.
3.7.8. Ejemplo 8: la agenda de un médico
Ahora presentamos una aplicación que muestra la agenda de un médico.
3.7.8.1. La vista V de la aplicación
Presentaremos el siguiente formulario:
![]() |
- en [1], se solicita la agenda de la Sra. PELISSIER [2], del 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>
<!-- la 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 del 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 el modelo [calendar.jour] (atributo ng-model);
- línea 28: el botón que solicita la agenda;
- líneas 32-34: la lista de errores no cambia;
- líneas 37-39: el calendario 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] queda así:
// 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 de la plantilla
// plantilla
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', nombre de usuario: 'admin', contraseña: '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()};
// estilamos la lista desplegable
$timeout(function () {
$('.selectpicker').selectpicker();
});
// configuración regional en francés 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 a HTTP;
- línea 8: se establecen de manera fija los elementos necesarios para la conexión HTTP;
- líneas 10-17: se define de manera fija la lista de médicos;
- línea 18: la plantilla [agenda] configura la visualización del calendario en la vista;
- línea 19: el modelo [calendar] configura la visualización del calendario en la vista. Se establece una fecha mínima [minDate] para hoy y la fecha actual también para hoy;
- líneas 21-23: se aplica estilo a la lista desplegable 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 calendario es el siguiente:
// ejecución de la acción
$scope.execute = function () {
// la 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 el 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 utiliza nuevamente el método [selectpicker], que proviene del archivo [bootstrap-select.min.js]. Es importante 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 la opción es, por lo tanto, el identificador [id] del médico.
- línea 11: se convierte el día elegido por el usuario al formato [aaaa-mm-jj], que es el formato de fecha que espera el servidor web;
- líneas 13-15: cuando el método [execute] haya finalizado, 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 la agenda del médico
var promise = task.promise.then(function () {
// la ruta del URL de servicio
var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
// se solicita la 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 del calendario
$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);
});
// se crea un evento para aplicar estilos a la tabla después de mostrar la vista
$timeout(function () {
$("#creneaux").footable();
});
} else {
// Se produjeron errores al obtener el calendario
$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 el URL consultando el [/getAgendaMedecinJour/1/2014-06-25];
- línea 8: se consulta URL. Se inicia una tarea asíncrona;
- línea 10: se resuelve 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] mencionado anteriormente. Este atributo contiene, a su vez, el atributo [creneauxMedecin] [2] mencionado anteriormente. Este último es una tabla de franjas horarias en la que cada una incluye dos datos:
- [rv]: el formato 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 terminado;
- línea 19: se completa el modelo [$scope.agenda] que controla la visualización de la agenda;
- línea 20: se hace visible la 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 representa el horario. A este se le agrega un atributo [text] que será la representación textual del horario en el formato [10h20:10h40];
- líneas 26-28: hacemos que la tabla HTML, utilizada para mostrar los intervalos de la agenda, sea «responsiva». Ya vimos este concepto en el párrafo 3.6.7;
![]() |
- línea 27: para que la tabla sea «adaptable», hay que aplicarle el método [footable]. Aquí nos encontramos con la misma dificultad que con el componente [bootstrap-select]. Si simplemente escribimos la línea 17, vemos que la tabla no es «adaptable». Este problema se resuelve de la misma manera con la función [$timeout] (línea 26);
- líneas 31-34: el caso en que la llamada a HTTP falló. En ese caso, se muestran los mensajes de error;
3.7.8.4. Visualización de la agenda
Ahora volvemos al código de la agenda en el archivo [app-18.html]. Es el siguiente:
<!-- el calendario -->
<div id="agenda" ng-show="agenda.show">
<!-- caso del médico sin horarios 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 la agenda, y que [agenda.data.creneauxMedecin] es una matriz de objetos del tipo [creneauMedecin]. Cada elemento de este último tipo tiene un atributo [creneauMedecin.creneau] que es un horario. Cada horario tiene dos elementos que nos interesan:
- [creneauMedecin.creneau.rv], que es el posible RV (rv!=null) asociado a la franja horaria;
- [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. Es poco probable, pero resulta que nuestra base de datos está incompleta y este caso existe. La generación o no del mensaje HTML está controlada por la directiva [ng-if];

La directiva [ng-if] es diferente de las directivas [ng-show, ng-hide]. Estas últimas simplemente ocultan un campo presente en el documento. Si se utiliza [ng-if='false'], entonces 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 el horario está disponible y se le aplica un fondo verde al texto. De lo contrario, se le aplica un fondo rojo;
- línea 32: se escribe el nombre del cliente para quien se reservó el RV [4]. Si es [rv==null], esta información no existe, pero Angular maneja correctamente este caso y no reporta ningún error;
- líneas 34-39: muestran uno de los dos botones: [Réserver] o [Supprimer]. La existencia o no de una cita determina cuál de los dos botones se elige;
3.7.8.5. Modificación del servidor web
Al igual que en los ejemplos anteriores, se debe modificar el servidor web para que el URL [/getAgendaMedecinJour] envíe los encabezados CORS:
![]() |
En la clase [RdvMedecinsCorsController] se agrega 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 las consultas HTTP y [OPTIONS]. Debemos hacer lo mismo para las solicitudes HTTP y [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 hizo anteriormente, vamos a trasladar el procesamiento de DOM a directivas. Tenemos dos procesamientos de DOM:
- al mostrar la vista por primera vez:
// se aplica estilo a la lista desplegable
$timeout(function () {
$('.selectpicker').selectpicker();
});
- al mostrar la agenda:
// se crea un evento para aplicar estilo a la tabla después de 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]. Luego 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 la agenda;
<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- directrices -->
<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 manejan el DOM:
// se aplica estilo a la lista desplegable
$timeout(function () {
$('.selectpicker').selectpicker();
});
// Se crea un evento para aplicar estilos a la tabla después de que se muestre la vista
$timeout(function () {
$("#creneaux").footable();
});
De este modo, al ejecutar la aplicación [app-18B.html] se obtienen los mismos resultados que al ejecutar [app-18.html].
3.7.9. Ejemplo 9: crear y cancelar reservaciones
Ahora presentamos una aplicación que permite crear y cancelar reservaciones.
3.7.9.1. La vista V de la aplicación
Presentaremos el siguiente formulario:
![]() |
- en [1], se podrá hacer una reserva. La reserva que se realice será para un cliente al azar;
- en [2], podremos eliminar las reservaciones 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>
<!-- la 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 de la versión anterior;
- líneas 10-12: el mensaje de error es el de la versión anterior;
- líneas 15-36: el calendario es el de la versión anterior, salvo por dos detalles:
- línea 26: al hacer clic en el botón [réserver] (atributo ng-click), se gestiona mediante el método [reserver] del modelo M de la vista V. Se le pasa el número del horario de reserva;
- línea 26: al hacer clic en el botón [supprimer], se gestiona mediante el método [reserver] del modelo M de la vista V. Se le pasa el número de la cita que se va a eliminar;
- línea 39: el código JS que administra 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]. Luego se modifica. Se mantienen los grandes bloques de código habituales. Las modificaciones se realizan principalmente en el controlador:

Describiremos 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', nombre de usuario: 'admin', contraseña: '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 la agenda
getAgenda();
// ------------------------------------------------------------------ reserva
$scope.reserver = function (creneauId) {
....
};
// ------------------------------------------------------------ eliminación RV
$scope.supprimer = function (idRv) {
...
};
// obtención de la 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 en formato fijo;
- línea 18: un médico particular. Las citas se harán para sus horarios disponibles;
- líneas 19-24: los clientes fijos;
- línea 26: queremos manejar fechas francesas;
- línea 27: las citas se programarán para la fecha de hoy;
- línea 28: el servicio web de reservaciones espera fechas en el formato «aaaa-mm-dd»;
- línea 29: la fecha de hoy en el formato [jeudi 26 juin 2014];
- línea 30: configuración de la agenda. El atributo [model] contiene los parámetros del mensaje internacionalizado que se 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 que se está ejecutando;
- línea 37: se solicita la agenda inicial;
Eso es todo lo que se hace durante la carga inicial de la página. Si todo sale bien, la vista muestra la agenda del día de la Sra. PELISSIER.

3.7.9.4. Obtención de la agenda
La agenda se obtiene mediante el siguiente método [getAgenda]:
// obtención de la agenda
function getAgenda() {
// la ruta del servicio URL
var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
// se solicita la 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 la 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 produjeron errores al obtener la agenda
$scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
}
});
}
Este código es el que se analizó en la aplicación anterior. Hay dos cambios:
- no hay una 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 un horario
![]() | ![]() |
Cabe recordar que los clientes se seleccionan al azar.
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 el horario identificado por [id]
var idClient = clients[Math.floor(Math.random() * clients.length)].id;
utils.debug("réservation du créneau pour le client", idClient);
// espera simulada
$scope.waiting.visible = true;
var task = utils.waitForSomeTime($scope.waiting.time);
// se agrega 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) {
// Hubo errores al validar el rv
$scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
} else {
// se solicita la nueva agenda
getAgenda();
}
});
};
- línea 1: se recuerda que el parámetro de la función [reserver] es el número de la franja horaria (atributo id);
- línea 4: se selecciona un cliente al azar de la lista de clientes definida de manera fija en el código de inicialización. Se toma su identificador [id];
- líneas 7-8: la espera de 3 segundos;
- líneas 11-18: estas líneas solo se ejecutan al finalizar los 3 segundos;
- línea 12: el URL del servicio de reservación [/ajouterRv]. Este URL es diferente a 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 junto 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 (párrafo 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 el formato {día, idClient, idCreneau};
- en [3], el cliente le 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 enviará en forma de un objeto JS. Angular lo serializará como JSON cuando se envíe;
- línea 16: se realiza la llamada a HTTP. El valor que se va a enviar es el último parámetro de la función [dao.getData]. Cuando este parámetro está presente, la función [dao.getData] genera un POST en lugar de un GET (consulte el código en el párrafo 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 reservación se ha realizado correctamente, se vuelve a mostrar la nueva agenda;
3.7.9.6. Modificación del servidor
![]() |
En la clase [RdvMedecinsCorsController], agregamos 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 a 13. Los encabezados de las líneas 2 a 8 se enviarán para URL [/ajouterRv] (línea 10) y el método HTTP [OPTIONS] (línea 10).
¿Se modifica la clase [RdvMedecinsController] 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 los métodos URL y [/ajouterRv] (línea 1), se invoca el método que acabamos de agregar en [RdvMedecinsCorsController] (línea 4), por lo que devuelve los mismos encabezados HTTP que para los métodos HTTP y [OPTIONS].
3.7.9.7. Tests
Hagamos una primera prueba en la que reservemos un horario cualquiera:
![]() |
Como siempre en estos casos, hay que revisar 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] falló con [status=0], lo que significa que fue Angular quien canceló la solicitud. La causa del error aparece en los registros:
XMLHttpRequest cannot load http://localhost:8080/ajouterRv. El campo de encabezado de solicitud Content-Type no está 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 y [accept, authorization, content-type];
- la de enviar un comando POST;
- en [4]: el servidor autoriza el encabezado [authorization]. Recordemos que, del lado del servidor, somos nosotros mismos quienes enviamos esta autorización;
La novedad es, por lo 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 otorgue:
![]() |
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 las opciones al cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// se establece el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// se autorizan ciertos 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 agregado una autorización para los encabezados HTTP y [accept, content-type];
- línea 9: se ha agregado una autorización para el método POST;
Volvemos a realizar la prueba después de reiniciar el servidor:
![]() |
Esta vez, logramos hacer la reserva.
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 agrega la franja horaria
var promise = task.promise.then(function () {
// la ruta del servicio URL
var path = config.urlSvrResaRemove;
// los datos que se transmitirán 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 produjeron 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 la nueva 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 reservación. Solo comentaremos 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í nuevamente en formato JSON. En el párrafo 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 el formato {idRv};
- en [4], el cliente le 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 que se enviará. Angular lo serializará automáticamente en JSON;
El resto del código es similar al de la reservación.
3.7.9.9. Modificación del servidor
Del lado del servidor, realizamos las siguientes modificaciones:
![]() |
En la clase [RdvMedecinsCorsController], agregamos el siguiente método:
// envío de las opciones al cliente
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// se corrige el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// se autorizan ciertos 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 a 16. Los encabezados de las líneas 2 a 10 se enviarán a URL y [/supprimerRv] (línea 13), así como al método HTTP y [OPTIONS] (línea 13).
La clase [RdvMedecinsController], por su parte, 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 los métodos URL y [/supprimerRv] (línea 1), se invoca el método que acabamos de agregar en [RdvMedecinsCorsController] (línea 4), por lo que devuelve los mismos encabezados HTTP que para los métodos HTTP y [OPTIONS].
3.7.10. Ejemplo 10: crear y cancelar reservaciones - 2
Ahora presentamos la misma aplicación que antes, pero en lugar de hacer una reserva para un cliente al azar, este se seleccionará de una lista desplegable.
3.7.10.1. La vista V de la aplicación
Presentaremos el siguiente formulario:
![]() |
Los clientes se seleccionarán en [1].
El código es similar al de la aplicación anterior, por lo que solo presentaremos las diferencias principales.
Duplicamos el archivo [app-19.html] en [app-20.html] y luego creamos el código de la lista desplegable de clientes [1]:
<!-- la lista de clientes -->
<div class="alert alert-info">
<h3>{{agenda.title|translate:agenda.model}}</h3>
<div class="row" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
- líneas 8-12: la lista desplegable se implementará con el componente [bootstrap-select];
- línea 1: la directiva [selectEnable] se aplica mediante el 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 párrafo 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 clientes
$scope.clients = {title: config.listClients, show: false, model: {}};
//------------------------------------------- inicialización de la vista
// la tarea asíncrona global
var task;
// se solicitan los clientes y luego la 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 clientes en la vista V;
- líneas 14-16: de manera asíncrona, primero se solicita la lista de clientes y, una vez obtenida, se solicita la 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 clientes:
function getClients() {
// se actualiza el UI
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// se solicita la lista de clientes;
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, datos: [client1, client2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
if (result.err == 0) {
// se introducen los datos obtenidos en el modelo
$scope.clients.data = result.data;
// se actualiza el UI
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// Se produjeron errores al obtener la lista de clientes
$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;
};
Este 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] presenta una ligera modificación:
$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 de la lista de clientes.
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 clientes -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- 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>
<!-- instrucciones -->
<script type="text/javascript" src="selectEnable2.js"></script>
- líneas 19-23: la lista desplegable de clientes;
- 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 entender el funcionamiento del código. Veremos que attrs['id']='selectpickerClients' para la lista de clientes;
- línea 6: para localizar en DOM un elemento de [id='x'], se escribe [$('#x')]. Por lo tanto, debemos escribir [$('#selectpickerClients')] para localizar la lista de clientes. Esto se logra con la sintaxis [$('#' + attrs['id'])];
La directiva [selectEnable2] utiliza, por lo tanto, la información contenida en 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 del modelo
// el mensaje de espera
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
// la información de inicio de sesión
$scope.server = {url: 'http://localhost:8080', nombre de usuario: 'admin', contraseña: 'admin'};
// los errores
$scope.errors = {show: false, model: {}};
// los médicos
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
// los clientes
$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 clientes y luego los médicos
getClients().then(function () {
getMedecins();
});
// lista de clientes
function getClients() {
...
}
// lista de médicos
function getMedecins() {
...
}
// cancelación de espera
function cancel() {
...
}
} ]);
- líneas 26-28: primero se solicitan los clientes y luego los médicos;
3.7.11.5. Las pruebas
Prueba esta nueva versión.
3.7.12. Ejemplo 12: una directiva [list]
Retomamos el mismo ejemplo que antes, pero queremos simplificar el código HTML utilizando una directiva. De hecho, actualmente tenemos el siguiente código HTML:
<!-- la lista de clientes -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- lista de médicos -->
<div class="alert alert-info">
<div class="row" ng-show="medecins.show">
<div class="col-md-4">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
</div>
</div>
Las líneas 14 a 26 son idénticas a las líneas 1 a 13. Se aplican a médicos en lugar de a clientes. Nos gustaría poder escribir lo siguiente:
<!-- la lista de clientes -->
<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 encuentra 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 llamada «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 alcance de la plantilla de la directiva. [scope: true] significa que dos elementos de tipo <list> tendrán cada uno su propia plantilla. Por defecto (alcance 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 para qué se utilizará la directiva:
<!-- la lista de clientes -->
<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 «clientes»;
- línea 3: scope[attrs['model']] tiene como valor scope['clients'] y, por lo tanto, representa a [$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 agrega 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 pudiera 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 que ya hemos visto. La diferencia es que el id del componente antes se incluía 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 clientes 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 el formato [{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 asignar un identificador a 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> únicamente 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 clientes -->
<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 clientes
$scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
- En las líneas 7 y 9, agregamos el atributo [id] a los modelos de los médicos y los clientes;
3.7.12.4. Las pruebas
Las pruebas arrojan los mismos resultados que en el ejemplo anterior.
3.7.13. Ejemplo 13: actualización de la plantilla de una directiva
Seguimos con el análisis de las directivas y mantenemos el ejemplo de la lista desplegable. Aquí queremos analizar 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 clientes;
![]() |
- en [2], se solicita por segunda vez la lista de clientes. Esta segunda lista se suma entonces a la primera, [3]. Es la actualización del componente [Bootstrap select] lo que queremos analizar en este ejemplo.
3.7.13.2. La página HTML
La página HTML [app-23.html] se obtiene al copiar [app-22.html] y luego se modifica 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 clientes -->
<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 agrega 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 desde 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 le pide 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 clientes, 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 clientes
$scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
// lista de clientes
$scope.getClients = function getClients() {
// se actualiza el UI
$scope.waiting.visible = true;
$scope.errors.show = false;
// se solicita la lista de clientes;
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, datos: [client1, client2, ...]}
// resultado={err: n, mensajes: [msg1, msg2, ...]}
if (result.err == 0) {
// se colocan los datos obtenidos en un nuevo modelo para forzar que la vista se actualice
$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 produjeron errores al obtener la lista de clientes
$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 clientes con las que ya están presentes en el array [clients.data];
Antes habíamos escrito:
Ahora se escribe:
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 clientes -->
<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 clientes debe agregarse 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] sea un puntero, al igual que [clients.data]. El puntero [clients] no cambia cuando se modifica el puntero [clients.data]. La directiva [list2], por lo tanto, no se reevalúa. 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, por lo tanto, volver a evaluarse. Sin embargo, no se obtiene el resultado esperado. Analicemos las capturas de pantalla cuando se solicita dos veces la lista de clientes:
![]() |
- 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 a los cuatro clientes en otra arquitectura, HTML, y es esta la que ve el usuario al hacer 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 clientes;
- línea 3: se ejecuta la directiva [list2];
- línea 4: el servicio [dao] obtiene una segunda lista de clientes;
La visualización de la línea 2 proviene del siguiente código en la directiva:
link: function (scope, element, attrs) {
utils.debug('directive list2');
...
}
Analicemos 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: después de obtener la primera lista de médicos, [clients.show] pasa a true y la directiva se activa;
- tras obtener la segunda lista de clientes, se observa que no se invoca el código de la directiva [list2]. Por eso no se ve 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 cambie 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 es la función que se ejecutará cuando esa variable cambie de valor. En este caso, queremos observar la variable [clients]. Por lo tanto, debemos escribir [scope.$watch('clients')]. Como tenemos attrs['model']='clientes', 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 lo anterior, se observa que, tras obtener la segunda lista de clientes, la directiva [list2] se ejecuta nuevamente, lo cual 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 clientes -->
<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 colocar 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 clientes -->
<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 analizada.
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 cambie attr["model"], la plantilla 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] que ya hemos analizado.
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 produjeron errores al obtener la lista de clientes
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};
3.7.15. Ejemplo 15: navegación
Hasta ahora, hemos utilizado aplicaciones de una sola página. En este ejemplo, abordaremos las aplicaciones de varias páginas y la navegación 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 regresa 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 versión 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 las 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 navegación -->
<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 navegación;
- línea 12: las diferentes vistas que muestra el contenedor se presentan dentro de la directiva [ng-view]. Al final, tenemos un contenedor que muestra:
- siempre la misma barra de navegación (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]. Este 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 a 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. Aquí 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, el [/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 a 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 ninguna de las tres anteriores (otherwise, línea 21);
- línea 23: redirección hacia URL [/page1], es decir, hacia 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 navegación -->
<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 del área que se muestra en la línea 6. Así, en esta área tenemos dos modelos:
- el modelo creado por el controlador [mainCtrl];
- el modelo creado por el controlador [page1Ctrl];
Existe una herencia de modelos. En la vista que se muestra en la línea 6, se pueden ver los 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 distintos:
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 a 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 navegación
La barra de navegación se define de la siguiente manera en el contenedor:
<div class="container" ng-controller="mainCtrl">
<!-- la barra de navegación -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- la vista actual -->
<ng-view></ng-view>
</div>
La barra de navegación se define en la línea 3. Esto significa que solo reconoce 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 último el que se ejecutará;
- línea 7: cambiamos el URL de la aplicación, que pasa a ser [/page1]. Volvamos al enrutamiento que se definió en el módulo principal:
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
se observa 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 verificar 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) {
// modelo 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 entender esto. El [$scope], definido por el controlador [mainCtrl], contiene los siguientes elementos: [main, page1, page2, page3]. Esto significa que tenemos acceso a los modelos de todas las vistas. No es necesariamente deseable, pero en este caso sí lo es. En la versión final del cliente Angular, utilizaremos esta característica para almacenar en el modelo [main] la información que debe compartirse entre vistas. Aquí tendremos 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). Entonces obtenemos la siguiente visualización:
![]() |
Las vistas [/page2] y [/page3] se construyen sobre el mismo modelo que la vista [/page1] (véanse las capturas de pantalla en la página 240).
3.7.15.8. Control de la navegación
Ahora queremos controlar la navegación de la siguiente manera: [page1 --> page2 --> page3 --> page1]. De este modo, si el usuario se encuentra en la página 1 ([/page1]) y escribe en su navegador URL o [/page3], dicha navegación no debe aceptarse y debe permanecer en la página 1.
Para lograr este resultado, modificamos los controladores de las páginas de la siguiente manera:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope', '$location',
function ($scope, $location) {
// ¿Navegación autorizada?
var main = $scope.main;
if (main.lastUrl && main.lastUrl != '/page3') {
// ¿Se regresa 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, almacenaremos su URL en el modelo [main.lastUrl]. Aquí utilizamos el concepto del que hablamos anteriormente: usar la plantilla [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 a 12 se duplica y se adapta a las tres vistas. Aquí nos encontramos 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 no se permite la navegación (la última URL visitada existe y no es /page3);
- línea 8: entonces se regresa a la última URL visitada;
Probemos lo siguiente:
![]() |
- En [1], estamos en la página 1 y escribimos el URL de la página 3 en [2];
- en [3], no se realizó la navegación y regresamos a URL de la página 1;
3.7.16. Conclusión
Hemos repasado todos los casos de uso que encontraremos en la versión 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 analizado.
3.8. El cliente final de 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 distintos elementos se ha explicado en el párrafo 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>
<!-- Núcleo de Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- módulos -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- 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 navegación -->
<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>
<!-- depuración -->
<debug model="app" ng-show="app.debug.on"></debug>
</div>
Independientemente de la vista que se muestre, siempre contará con 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], lo 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">
...
<!-- la 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 distintas rutas se explicó en el párrafo 3.7.15.4, página 242.
La vista [login.html] está vacía, es decir, no agrega ningún elemento a los que ya están presentes en la página maestra.
La vista [home.html] agrega el siguiente elemento a la página maestra:

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

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

3.8.5. Funcionalidades de la aplicación
Las vistas del cliente Angular ya se presentaron en el párrafo 1.3.3, página 7. 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 programar citas médicas;
- en [7], una casilla de selección que permite estar o no en el 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 predeterminado 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 observar 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 (nombre de usuario/contraseña) con un rol (ADMIN) y user/user con un rol (USER). Solo el rol ADMIN tiene permiso para usar la aplicación. El rol USER solo está ahí para mostrar qué 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 inicia sesión;
![]() |
- una vez que inicias sesión, puedes elegir el médico con el que quieres una cita [2] y el día de la misma [3];
- en [4], se solicita ver la agenda del médico elegido para el día seleccionado;
![]() |
- una vez obtenida la agenda del médico, se puede reservar un horario [5];
![]() |
- en [6], se selecciona al paciente para la cita y se confirma esta selección en [7];
![]() |
Una vez validada la cita, se regresa automáticamente al calendario, donde ya aparece registrada la nueva cita. Esta cita se podrá eliminar posteriormente en [7].
Ya se han descrito las principales funciones. Son sencillas. Las que no se han descrito son funciones de navegación para regresar a una vista anterior. Terminemos con la gestión del idioma:
![]() |
- en [1], se cambia del francés al inglés;
![]() |
- a [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 los 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">
...
En la línea 1, todo el cuerpo (body) de la página maestra es controlado por el controlador [appCtrl]. Por su posición, esto lo convierte en un controlador general y principal de la aplicación. Como se explicó en el párrafo 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) {
// depuración
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
// cancelar 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 lo precede
app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
}])
;
Las líneas 10 a 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 que es importante entender es que el objeto [$scope], al ser el modelo del controlador de la página maestra, es heredado por todas las vistas y controladores. De este modo, el controlador [loginCtrl] tiene acceso a los elementos [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda]. Dicho de otra manera, un controlador tiene acceso a los modelos de otros controladores. La aplicación que estamos analizando 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 deba 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 como memoria compartida entre los controladores, a menudo se denominará session en los comentarios, a modo de imitación de 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] siempre deben estar 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 navegación
app.navbarrun = {show: false};
app.navbarstart = {show: false};
// sin errores
app.errors = {show: false};
// configuración regional predeterminada
angular.copy(config.locales['fr'], $locale);
// la 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 distintos controladores. En lugar de escribir [$scope.app.champ=value] en todas partes, el puntero [$scope.app] se asigna a la variable [app] y, por lo tanto, se escribirá [app.champ=value]. Solo hay que recordar que [app] es la plantilla expuesta 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 selección [debug] de las barras de navegación;
- línea 15: [app.navbarrun.show] controla la visualización de la siguiente barra de navegación:
![]()
- línea 16: [app.navbarstart.show] controla la visualización de la siguiente barra de navegación:
![]()
- 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 se incluirá 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 controlar la navegación.
- línea 24: inicia una tarea asíncrona, una espera simulada. La tarea asíncrona es referenciada por dos punteros: [app.view.model.task.action] y [app.task];
Se han factorizado dos métodos en el controlador [appCtrl]:
// cancelar la tarea actual
app.cancel = function () {
...
};
// desconexión
app.deconnecter = function () {
...
};
- línea 2: la función [app.cancel] sirve para cancelar la tarea actual para la cual se muestra actualmente un mensaje de espera. Todas las vistas muestran este mensaje y, por lo tanto, 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 () {
// se regresa a la página de inicio de sesión
$location.path(config.urlLogin);
};
- línea 4: se regresa 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 luego la de clientes 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 esa opción.
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 asincrónicas hagan referencia a este objeto;
- línea 6: para indicar que la tarea ha finalizado;
- línea 7: para dar por terminada la tarea con un error. Esta notación es diferente a la utilizada en los ejemplos de Angular analizados:
- en los ejemplos, el objeto [task] era un objeto [$q.defer()] que se podía dar por finalizado;
- en la versión final, el objeto [task] es un objeto con los campos [action, isFinished], donde [action] es el objeto [$q.defer()] quese puede completar y [isFinished] es un valor booleano que indica que la acción se ha completado;
Analicemos el ciclo de vida del objeto [task] con un ejemplo. Al inicio, después del controlador [appCtrl], el controlador [loginCtrl] toma el control 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, el [$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 agregar el campo [task] al objeto [login], en realidad se agregó al objeto [$scope.app.view.model]. Si el usuario cancela la espera, el código en [appCtrl.cancel]:
// modelo de la página actual
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
sí terminará la espera simulada (líneas 4-6).
3.8.9. Control de la navegación
Las reglas de navegación 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í | |
/inicio | 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 navegación;
- línea 26: nueva vista actual;
Para [resaCtrl]:

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

- aquí no hay ningún control de navegación, ya que la regla establece que se puede llegar a URL [/login] desde cualquier lugar. Por lo tanto, si el usuario escribe esta URL en su navegador, funcionará independientemente de cuál sea la vista actual en ese momento;
- línea 16: la nueva vista actual;
El código para el controlador [homeCtrl] se proporcionó en el párrafo 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 cambia de URL [/home] a URL [/agenda]:
![]() |
En el ejemplo anterior, nos encontramos en el método [afficherAgenda] del controlador [homeCtrl]. El usuario solicitó la 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, como ya vimos, se utiliza 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] le 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á procesado 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 que ya se describieron en la presentación de Angular:
- el servicio [config] se presentó en el párrafo 3.7.4;
- el servicio [utils] se presentó en el párrafo 3.7.5;
- el servicio [dao] se presentó en el párrafo 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 aproximadamente 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 estática 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 que se realice 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 modificarlo en las barras de navegación:
![]() |
- línea 45: una casilla de selecció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 que ya se describieron en la presentación de Angular:
- la directiva [footable] se introdujo en el párrafo 3.7.8.6;
- la directiva [list] se introdujo en el párrafo 3.7.12;
- las directivas [errors] y [waiting] se introdujeron en el párrafo 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 de depuración (atributo ng-show). Esto da como resultado cosas como esta:
![]() |
Esto requiere un buen conocimiento del código para poder interpretarlo, pero una vez que se adquiere dicho conocimiento, la información anterior resulta útil para la depuración. Aquí se han resaltado los elementos del modelo [$scope.app] que se muestra. Recordemos que [$scope.app] es la memoria compartida por los controladores;
- [waitingBeforeTask]: el tiempo de espera simulado antes de cualquier solicitud a HTTP;
- [debug]: el modo de depuración; es necesariamente true si se muestra esta barra;
- [navbarrun]: valor booleano que controla la visualización de la siguiente barra de navegación:
![]()
- [navbarstart]: valor booleano que controla la visualización de la siguiente barra de navegación:
![]()
- [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]: la información de conexión al servicio web;
- [medecins]: plantilla para la directiva [list] aplicada a los médicos;
- [clients]: lo mismo para los clientes;
- [menu]: controla las opciones del menú que se muestran. 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]: la agenda del médico. En ella hay horarios disponibles (rv==null) y reservados. Para estos últimos, aparece el nombre del cliente que hizo la reserva;
- [selectedCreneau]: el horario elegido para hacer una reservación;
3.8.12. El controlador [loginCtrl]
![]() |
El controlador [loginCtrl] está asociado a la vista [views/login.html], la cual, al combinarse con la página maestra, genera la siguiente página:

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 completa [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: tal vez recordemos que [appCtrl] inició una espera simulada a la que hace referencia el objeto [app.task.action]. Se utiliza el [promise] de esta tarea para esperar a que termine;
- 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 errores;
- 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]. Cabe recordar que esta es la condición para que la tarea pueda cancelarse;
- línea 57: tras finalizar la espera simulada, se cargan los médicos;
- línea 62: una vez obtenida la solicitud de médicos, se analiza dicha solicitud. Si se han obtenido los médicos, entonces se solicitan los clientes;
- 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 terminó con un error;
- líneas 91-98: el caso en el que se han tenido clientes;
- línea 93: [app.clients] es la plantilla de la directiva [list] que mostrará a los clientes 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 navegación;
Lo importante que hay que destacar aquí es que los médicos y los clientes 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], la cual, al combinarse 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 navegación. 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 que aparecieron en el controlador [loginCtrl]. [home] es, por lo 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 la plantilla del menú de la barra de navegación. Aquí aparecerá la opción [Agenda]. Es esta la que permite consultar la agenda de un médico;
Por último, el controlador cuenta con dos métodos:

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

La estructura del controlador [agendaCtrl] es la siguiente:

- las líneas 10-20 se encargan del control de navegación;

- 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á la opción [Home / Accueil]:
![]()
Los métodos del controlador son los siguientes:

- línea 95: el método [agenda.supprimer] se ha tratado en el párrafo 3.7.9;
El método [agenda.home] es un método de navegación pura:

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

- línea 73: el parámetro de la función [reserver] es el número de la franja horaria (id);
- líneas 77-86: tienen como objetivo encontrar el horario con ese 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: navegación hacia [/resa.html];
3.8.15. El controlador [resaCtrl]
![]() |
El controlador [resaCtrl] está asociado a la vista [views/resa.html], la cual, al combinarse con la página maestra, genera la siguiente página:

La estructura del controlador [resaCtrl] es la siguiente:

- líneas 12-20: el control de navegación;

- 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 analizó en el párrafo 3.7.9.
3.8.16. La gestión de idiomas
Todos los controladores ofrecen el siguiente método [setLang]:

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




























































































































