Skip to content

3. El cliente Angular JS

3.1. Referencias para el marco Angular JS

Al principio de este documento se proporcionaron dos referencias para el marco Angular JS. Las volvemos a enumerar aquí:

AngularJS merece un libro propio. El libro de Adam Freeman tiene más de 600 páginas, y ni una sola de ellas está de más. Describiremos una aplicación Angular y, a lo largo de esta descripción, analizaremos los fundamentos de este marco. Sin embargo, nos limitaremos únicamente a las explicaciones necesarias para comprender la solución propuesta. Angular es un marco extremadamente completo y hay muchas formas de lograr el mismo resultado. Esto puede suponer un reto, ya que cuando se está empezando, 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 presenta aquí. Podría haberse escrito de otra manera y quizá utilizando mejores prácticas.

3.2. Arquitectura del cliente de Angular

La arquitectura del cliente de Angular se asemeja a la de una aplicación web MVC clásica, con algunas diferencias. Una aplicación web Spring MVC, por ejemplo, tiene la siguiente arquitectura:

El procesamiento de una solicitud del cliente se lleva a cabo de la siguiente manera:

  1. solicitud: las URL solicitadas tienen el formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... El [Dispatcher Servlet] es la clase de Spring que gestiona las URL entrantes. «Enruta» la URL a la acción que debe gestionarla. Estas acciones son métodos de clases específicas denominadas [Controladores]. La C de MVC aquí es la cadena [Servlet Dispatcher, Controlador, Acción]. Si no se ha configurado ninguna acción para gestionar la URL entrante, el [Servlet Dispatcher] responderá que no se ha encontrado la URL solicitada (error 404 NOT FOUND);
  1. el procesamiento
  • la acción seleccionada puede utilizar los parámetros que el [Servlet Dispatcher] le ha pasado. Estos pueden provenir de varias fuentes:
    • la ruta [/param1/param2/...] de la URL,
    • los parámetros de la URL [p1=v1&p2=v2],
    • de los parámetros enviados por el navegador con su solicitud;
  • al procesar la solicitud del usuario, la acción puede necesitar la capa [de negocio] [2b]. Una vez procesada la solicitud del cliente, puede desencadenar diversas respuestas. Un ejemplo clásico es:
    • una página de error si la solicitud no se ha podido procesar correctamente
    • una página de confirmación en caso contrario
  • la acción indica que se muestre una vista específica [3]. Esta vista mostrará datos conocidos como el modelo de vista. Esta es la M de MVC. La acción creará este modelo M [2c] e indicará que se muestre una vista V [3];
  1. respuesta: la vista V seleccionada utiliza el modelo M construido 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 esta respuesta.

La arquitectura de nuestro cliente Angular será similar, con una terminología ligeramente diferente. En primer lugar, las aplicaciones Angular son generalmente aplicaciones web de una sola página (SPA):

Image

  • el usuario solicita la URL inicial de la aplicación en el formato: http://machine:port/contexte. El navegador consulta a un servidor web para recuperar el documento solicitado. Se trata de una página HTML con estilo CSS y dinamizada mediante JavaScript;
  • luego, el usuario interactúa con las vistas que se le presentan. Podemos distinguir varios tipos de interacciones:
    • las que no requieren ninguna interacción con fuentes externas, como ocultar o mostrar elementos de la vista. Estas se gestionan mediante JavaScript incrustado;
    • las que requieren datos de un servicio web remoto. Estos datos se recuperarán mediante una solicitud AJAX (Asynchronous JavaScript and XML), se construirá un modelo y se mostrará una vista;
    • aquellas que requieren una vista distinta de la vista inicial. Se solicitará mediante una llamada AJAX al servidor que sirvió la página inicial. A continuación, se repetirá el proceso anterior. La página resultante se almacenará en la caché del navegador. En la siguiente solicitud, no se recuperará del servidor HTML remoto;

En definitiva, el navegador solo realiza una única solicitud HTTP: la que recarga la página inicial. Las solicitudes HTTP posteriores al servidor de la página HTML o a los servicios web remotos las realiza el JavaScript integrado en las páginas.

A continuación, presentaremos la arquitectura de la aplicación dentro del navegador. No tendremos en cuenta el servidor HTML que proporciona las páginas HTML de la aplicación. A efectos de explicación, podemos suponer que todas ellas se encuentran en la caché del navegador.

En primer lugar, debemos situar esta arquitectura:

  • en [1], estamos en un navegador;
  • en [2], un usuario interactúa con las vistas mostradas por el navegador;
  • en [3], se recuperan datos de la red, a menudo de servicios web;

El usuario interactúa con las vistas: rellena formularios y los envía. Explicaremos este proceso utilizando la vista V1 anterior. Supondremos que esta es la vista inicial de la aplicación. Se obtuvo de la siguiente manera:

  • el usuario solicita la URL inicial de la aplicación en el formato: http://machine:port/contexte;
  • el navegador solicitó el documento asociado a esta URL. Recibió la página HTML/CSS/JS para la vista V1;
  • el JavaScript incrustado en la página tomó el control y lo cedió al controlador C1 [5];
  • el controlador construyó el modelo M1 [8] [9] para la vista V1. La construcción de este modelo pudo haber requerido el uso de servicios internos [6] y la consulta de servicios externos [7];

El usuario tiene ahora una vista V1 ante sí. Imaginemos que se trata de un formulario. Lo rellena y, a continuación, lo envía:

  • en [4], el usuario envía el formulario;
  • en [5], este evento será gestionado por uno de los métodos del controlador C1;

Si el evento solo da lugar a un simple cambio en la vista V1 (ocultar/mostrar campos), el controlador C1 modificará el modelo M1 de la vista V1 y, a continuación, volverá a mostrar la vista V1. Para ello, es posible que necesite uno de los servicios de la capa [servicios] [6].

Si el evento requiere datos externos:

  • en [6], el controlador C1 solicitará a la capa [DAO] que los recupere;
  • en [7], la capa [DAO] realizará una o más llamadas AJAX para recuperarlos;
  • en [8] y [9], se modificará el modelo M1 y se mostrará la vista V1;

Si el evento desencadena un cambio de vista, en ambos casos anteriores, en lugar de mostrar la vista V1, el controlador C1 solicitará una nueva URL [10]. Se trata de una URL interna dentro del navegador. No da lugar inmediatamente a una solicitud HTTP al servidor de la página HTML. Este cambio de URL es gestionado por un enrutador configurado de tal manera que cada URL interna se corresponde con una vista V y su controlador C. A continuación, el enrutador muestra la nueva vista Vn. Antes de mostrarla, su controlador Cn toma el control, construye el modelo Mn y, a continuación, muestra la vista Vn [11]. Si la página HTML de la vista Vn no está almacenada en la caché del navegador, se solicitará al servidor de páginas HTML.

La capa [Presentación] de esta arquitectura es similar a la arquitectura JSF (Java Server Faces):

  • la vista V corresponde a la vista Facelet de JSF;
  • el controlador C corresponde al bean JSF, una clase Java que contiene tanto el modelo M de la vista V como sus controladores de eventos;

La capa [Servicios] difiere de las capas [Servicios] a las que estamos acostumbrados. En el desarrollo web del lado del servidor, solemos tener la siguiente arquitectura en capas:

En el diagrama anterior, la capa [web] se comunica con la capa [DAO] únicamente a través de la capa [business]. Nada nos impediría inyectar una referencia a la capa [DAO] en la capa [web] para habilitar esta comunicación. Pero evitamos hacerlo.

Con Angular, no nos imponemos restricciones. La arquitectura queda entonces así:

  • en [1], la capa de [presentación] puede comunicarse directamente con cualquier servicio;
  • en [2], los servicios son conscientes unos de otros. Un servicio puede utilizar uno o más servicios adicionales.

3.3. Las vistas del cliente de Angular

Las vistas del cliente Angular ya se presentaron en la sección 1.3.3. Para facilitar la comprensión de este nuevo capítulo, las repetimos aquí. La primera vista es la siguiente:

  • [6], la página de inicio de sesión de la aplicación. Se trata de una aplicación de gestión de citas para médicos;
  • en [7], una casilla de verificación que permite al usuario activar o desactivar el modo [debug]. Este modo se indica mediante la presencia del panel [8], que muestra el modelo de la vista actual;
  • en [9], un tiempo de espera artificial en milisegundos. El 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 ver la gestión de la espera implementada por la aplicación;
  • en [10], la URL del servidor Spring 4. Según lo anterior, es [http://localhost:8080];
  • en [11] y [12], el nombre de usuario y la contraseña del usuario que 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 utilizar la aplicación. El rol USER se incluye únicamente para demostrar la respuesta del 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: francés (predeterminado) e inglés.
  • en [1], inicias sesión;
  • una vez que hayas iniciado sesión, puedes elegir el médico con el que deseas concertar una cita [2] y la fecha de la cita [3];
  • En [4], solicitas ver la agenda del médico seleccionado para el día elegido;
  • Una vez que se muestra la agenda del médico, puedes reservar una franja horaria [5];
  • En [6], seleccione el paciente para la cita y confirme su selección en [7];

Una vez confirmada la cita, se le redirigirá automáticamente al calendario, donde aparecerá la nueva cita. Esta cita se puede eliminar más tarde [7].

Ya se han descrito las funciones principales. Son sencillas. Las que no se han descrito son funciones de navegación para volver a una vista anterior. Concluyamos con la configuración del idioma:

  • en [1], se cambia del francés al inglés;

Image

  • en [2], la vista cambia al inglés, incluido el calendario;

3.4. Configuración del proyecto Angular

Construiremos nuestro cliente Angular paso a paso. Estamos utilizando el IDE WebStorm.

Creemos una carpeta vacía [rdvmedecins-angular-v1] y abrámosla con WebStorm:

  • en [1], abre una carpeta;
  • En [2], seleccionamos la carpeta que hemos creado;
  • en [3], obtenemos un proyecto WebStorm vacío;
  • en [4], configuramos el proyecto mediante la opción [Archivo / Configuración];
  • en [5] y [6], configuramos la propiedad [Ortografía], que gestiona la revisión ortográfica. De forma predeterminada, está activada. Dado que el software descargado está en inglés, nuestros comentarios en francés en 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 utilizando la sintaxis JSON;
  • en [10], el archivo generado se modifica como se muestra en [11];
  • en [12], guarda este archivo tanto en [package.json] como en [bower.json];
  • En [13], reconfigura el proyecto;
  • en [14], configura la propiedad [Javascript / Bower], que nos permitirá declarar las bibliotecas de JavaScript que necesitamos;
  • en [15], especifica el archivo [bower.json] que acabamos de crear;
  • en [16], añade una biblioteca JavaScript;
  • en [17], se muestran todas las bibliotecas JavaScript descargables;
  • En [18], podemos introducir un término para filtrar la lista [17]. Aquí, especificamos que queremos la biblioteca [Angular JS];
  • en [19], aparecen los detalles de la biblioteca. Aquí vemos que se descargará la versión 1.2.18 de Angular;
  • en [20], la descargamos;
  • en [21], vemos que se ha descargado;
  • en [22], vemos la versión descargada. En realidad es la 1.2.19;
  • en [23], vemos la última versión disponible;
  • en [24], siguiendo el mismo procedimiento que antes, descargamos las siguientes bibliotecas:
angular-base64
para codificar la cadena «user:password» en Base64;
angular-i18n
para internacionalizar el calendario
angular-route
para enrutar las URL internas de la aplicación al controlador y la vista correctos;
angular-translate
permite la internacionalización de las vistas. Es un proyecto independiente de Angular. Aquí se utilizarán dos idiomas: francés e inglés;
angular-ui-bootstrap-bower
proporciona componentes visuales compatibles con Bootstrap. Aquí utilizaremos su calendario;
bootstrap
el marco CSS Bootstrap. Se utilizará para crear las vistas;
footable
proporciona un componente visual de tipo «tabla». Es «adaptativo» en el sentido de que puede adaptarse al tamaño de la pantalla;
bootstrap-select
proporciona un componente de «lista desplegable»;
  • En [25], las bibliotecas descargadas se instalaron en la carpeta [bower_components];
  • en [26], vemos que se ha descargado la biblioteca jQuery. Esto se debe a que Bootstrap la utiliza. El sistema para instalar dependencias de JavaScript en un proyecto es análogo a Maven en el mundo Java: si una biblioteca descargada tiene sus propias dependencias, estas se descargan automáticamente;

El archivo [bower.json] ha cambiado:

{
  "name": "rdvmedecins-angular",
  "version": "0.0.1",
  "dependencies": {
    "angular": "~1.2.18",
    "angular-base64": "~2.0.2",
    "angular-route": "~1.2.18",
    "angular-translate": "~2.2.0",
    "bootstrap": "~3.1.1",
    "footable": "~2.0.1",
    "angular-ui-bootstrap-bower": "~0.11.0",
    "bootstrap-select": "~1.5.2"
  }
}

Todas las dependencias descargadas se han incluido en el archivo.

3.5. La página inicial del cliente Angular

Creamos una versión inicial de la página de inicio del cliente Angular:

  • en [1] y [2], creamos un archivo HTML llamado [app-01] [3] y [4];

El archivo [app-01.html] servirá como nuestra página principal por el momento. Configuraremos la importación de los archivos CSS y JS que requiere 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é">
  <!-- on CSS -->
  <link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
  <link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
  <link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
  <link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
  • líneas 11-12: los archivos CSS de 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 para 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 para Angular y proyectos relacionados;
  • línea 26: el archivo JS de Angular. Debe cargarse después de jQuery si se utiliza esa biblioteca;
  • línea 27: el archivo JS para el proyecto [angular-ui-bootstrap];
  • línea 28: el archivo JS para el enrutador [angular-route];
  • línea 29: el archivo JS para el módulo de internacionalización de la aplicación Angular;
  • línea 30: el archivo JS para el módulo [angular-base64];

Se puede verificar la validez del archivo [app-01.html]:

  • en [1], solicitamos una revisión del código;
  • en [2], el resultado cuando todo es correcto;

Se recomienda realizar esta inspección sistemática del código antes de la ejecución. En este caso, esta comprobación permite detectar cualquier error en las referencias a archivos CSS y JS. Si una ruta es incorrecta, el inspector de código la señalará.

  • En [3], la página se puede cargar en un navegador a través de un depurador. En el navegador se muestra el siguiente resultado:
  • en [4], la página [app-01.html] fue servida por un servidor interno de WebStorm que se ejecuta aquí en el puerto 63342;
  • En [5], la consola del depurador. Si se hubiera producido algún error, habría aparecido aquí. Aquí es también donde se muestra la salida de pantalla generada por la instrucción JavaScript [console.log(expresión)]. Haremos un uso extensivo de esta función;

El modo de depuración te 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í que si añadimos la línea 3 a continuación:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <h2>Version 1</h2>
</div>

y cuando volvemos al navegador, vemos que la página ha cambiado:

 

3.6. Introducción a Bootstrap

A continuación, ilustraremos algunas de las características de Bootstrap utilizadas en la aplicación. Mis conocimientos sobre este marco de trabajo son limitados, ya que los he adquirido copiando y pegando código encontrado en Internet. Explicaré la función de las clases CSS que creo entender. Me abstendré de comentar las demás.

3.6.1. Ejemplo 1

En Angular, las operaciones que recogen información de fuentes externas son asíncronas. Esto significa que la operación se inicia y el control vuelve inmediatamente a la vista, lo que permite al usuario seguir interactuando con ella. Se notifica a la aplicación que la operación ha finalizado mediante un evento. Este evento es gestionado por una función JavaScript que puede actualizar o modificar la vista actual. Si es probable que la operación tarde mucho tiempo, resulta útil ofrecer al usuario la opción de cancelarla. Ofreceremos esta opción de forma sistemática. Para ello, utilizaremos un banner de Bootstrap:

Image

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 de color. La clase [alert-warning] utiliza un color predefinido;
  • línea 5: la clase [btn] aplica estilos a un botón. La clase [btn-primary] le da un color específico. La clase [pull-right] lo coloca a la derecha del banner de alerta;
  • línea 6: una imagen animada de carga;

3.6.2. Ejemplo 2

Las diferentes vistas de la aplicación tendrán un título común:

Image

Para ello, duplicamos [app-01.html] en [app-03.html] y modificamos las siguientes líneas:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <!-- Bootstrap Jumbotron -->
  <div class="jumbotron">
    <div class="row">
      <div class="col-md-2">
        <img src="assets/images/caduceus.jpg" alt="RvMedecins"/>
      </div>
      <div class="col-md-10">
        <h1>Les Médecins associés</h1>
      </div>
    </div>
  </div>
</div>
  • El área de color se crea utilizando la clase [jumbotron] en la línea 4;
  • Línea 5: La clase [row] define una fila con 12 columnas;
  • Línea 6: la clase [col-md-2] define un área de dos columnas dentro de la fila;
  • Línea 7: se coloca una imagen en estas dos columnas;
  • Líneas 9-11: se coloca texto en las 10 columnas restantes;

3.6.3. Ejemplo 3

Las vistas tendrán una barra de control superior. Contendrá opciones de control, enlaces o botones. También contendrá elementos de formulario. Por ejemplo:

Para lograr 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">
          <!-- debug mode -->
          <label style="width: 100px">
            <input type="checkbox">
            <span style="color: white">Debug</span>
          </label>
          <!-- identification form -->
          <div class="form-group">
            <input type="text" class="form-control" placeholder="Temps d'attente"
                   style="width: 150px"/>
            <input type="text" class="form-control" placeholder="URL du service web"
                   style="width: 200px"/>
            <input type="text" class="form-control" placeholder="Login"
                   style="width: 100px"/>
            <input type="password" class="form-control" placeholder="Mot de passe"
                   style="width: 100px"/>
          </div>
          <button class="btn btn-success">
            Connexion
          </button>
        </form>
      </div>
          <button class="btn btn-success">
            Connexion
          </button>
        </form>
      </div>
    </div>
  </div>
</div>
  • línea 4: la clase [navbar] aplica estilos a la barra de navegación. La clase [navbar-inverse] le da un fondo negro. La clase [navbar-fixed-top] garantiza que, al desplazarse por la página mostrada por el navegador, la barra de navegación permanezca en la parte superior de la pantalla;
  • Líneas 6-14: definen el área [1]. Normalmente se trata de una serie de clases que no entiendo. Utilizo el componente tal cual;
  • línea 15: define un área «adaptativa» de la barra de navegación. En un smartphone, esta área se contrae en un área de menú;
  • línea 16: la clase [navbar-form] envuelve un formulario en la barra de comandos. La clase [navbar-right] lo coloca a la derecha del formulario;
  • líneas 23–32: los cuatro campos de entrada del formulario de la línea 17 [3]. Se encuentran dentro de una clase [form-group] que envuelve los elementos de un formulario, y cada uno de ellos tiene la clase [form-control];
  • línea 33: la clase [btn] que ya hemos visto, mejorada con la clase [btn-success], que le da su color verde;

3.6.4. Ejemplo 4

La barra de control te permitirá cambiar el idioma mediante una lista desplegable:

Image

Para ello, duplicamos [app-01.html] en [app-05.html] y añadimos las siguientes líneas a la barra de control:


          <button class="btn btn-success">
            Connexion
          </button>
          <!-- languages -->
          <div class="btn-group">
            <button type="button" class="btn btn-danger">
              Langues
            </button>
            <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
              <span class="caret"></span>
              <span class="sr-only">Toggle Dropdown</span>
            </button>
            <ul class="dropdown-menu" role="menu">
              <li>
                <a href="">Français</a>
              </li>
              <li>
                <a href="">English</a>
              </li>
            </ul>
          </div>
</form>

Las líneas añadidas son las líneas 4–21.

  • Línea 5: La clase [btn-group] agrupa un conjunto de botones. Hay dos de ellos en las líneas 6 y 9;
  • Líneas 6–8: El primer botón define la etiqueta de la lista desplegable. La clase [btn-danger] le da un color rojo;
  • líneas 9-12: el segundo botón es el botón de la lista desplegable. Se coloca junto al primero, dando la impresión de ser un único componente;
  • Línea 10: muestra la flecha hacia abajo que indica que el botón es una lista desplegable;
  • línea 11: para lectores de pantalla;
  • líneas 13–20: los elementos de la lista desplegable son los elementos de una lista sin ordenar;

3.6.5. Ejemplo 5

Para enviar un formulario o navegar, el usuario dispondrá de opciones o botones en la barra de control, tal y como se muestra a continuación:

Se han añadido opciones de menú en [1]. Para ello, duplicamos [app-01.html] en [app-06.html] y añadimos las siguientes líneas:


<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
...
      </div>
      <!-- menu options -->
      <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active">
            <a href="">
              <span>Home</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Agenda</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Valider</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Annuler</span>
            </a>
          </li>
        </ul>
        <!-- right buttons -->
        <form class="navbar-form navbar-right" role="form">
...
        </form>
      </div>
    </div>
  </div>
</div>
  • Las opciones del menú se generan en las líneas 8–29. Se trata, de nuevo, de elementos de una lista <ul>. La clase [active] subraya el texto, lo que indica que se puede hacer clic en la opción.

3.6.6. Ejemplo 6

Mostraremos médicos y 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 conseguir este resultado, duplicamos [app-01.html] en [app-07.html] y añadimos las siguientes líneas:


<!DOCTYPE html>
<html>
<head>
...
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
 
</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <h2><label for="medecins">Médecins</label></h2>
  <select id="medecins" data-style="btn btn-primary" class="selectpicker">
    <option value="1">Mme Marie PELISSIER</option>
    <option value="1">Mr Jacques BROMARD</option>
    <option value="1">Mr Philippe JANDOT</option>
    <option value="1">Mme Justine JACQUEMOT</option>
  </select>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- local script -->
<script>
  $('.selectpicker').selectpicker();
</script>
</body>
</html>
  • Línea 5: Debes importar la hoja de estilos [bootstrap-select];
  • línea 13: el atributo [data-style] es utilizado por [bootstrap-select]. Se utiliza para aplicar estilo a la lista desplegable. Aquí, le damos el aspecto de un botón azul [btn-primary];
  • línea 13: el atributo [class] se utiliza en la línea 23. Puede ser cualquier cosa;
  • Líneas 14-17: los elementos de la lista desplegable. Se trata de etiquetas HTML estándar;
  • línea 22: se debe importar el JS [bootstrap-select];
  • líneas 24-26: un script de JavaScript que se ejecuta cuando la página termina de cargarse;
  • línea 25: una instrucción jQuery. Aplicamos el método [selectpicker] (selectpicker()) a todos los elementos con la clase [selectpicker] ($('.selectpicker')). Solo hay uno: la etiqueta <select> de la línea 13. El método [selectpicker] proviene del archivo JS al que se hace referencia en la línea 22;

3.6.7. Ejemplo 7

Para mostrar el horario de un médico, utilizaremos una tabla adaptativa proporcionada por la biblioteca JS [footable]:

  • en [1]: la tabla con una visualización normal;
  • en [2]: la tabla cuando se cambia el tamaño de la ventana del navegador. La columna [Acción] pasa automáticamente a la línea siguiente. Esto se denomina componente «adaptativo» o, simplemente, «responsivo».

Duplicamos [app-01.html] en [app-08.html] y añadimos las siguientes líneas:


...
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
...
<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <div class="row alert alert-warning">
    <div class="col-md-6">
      <table id="creneaux" class="table">
        <thead>
        <tr>
          <th data-toggle="true">
            <span>Créneau horaire</span>
          </th>
          <th>
            <span>Client</span>
          </th>
          <th data-hide="phone">
            <span>Action</span>
          </th>
        </thead>
        <tbody>
        <tr>
          <td>
            <span class='status-metro status-active'>
              9h00-9h20
            </span>
          </td>
          <td>
            <span></span>
          </td>
          <td>
            <a href="" class="status-metro status-active">
              Réserver
            </a>
          </td>
        </tr>
        <tr>
          <td>
            <span class='status-metro status-suspended'>
              9h20-9h40
            </span>
          </td>
          <td>
            <span>Mme Paule MARTIN</span>
          </td>
          <td>
            <a href="" class="status-metro status-suspended">
              Supprimer
            </a>
          </td>
        </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>
...
<script src="bower_components/footable/dist/footable.min.js" type="text/javascript"></script>
  • Las líneas 2 y 60 ya están presentes en [app-01.html]. Se trata de los archivos CSS y JS proporcionados por la biblioteca [footable];
  • La línea 3 hace referencia al siguiente archivo CSS:

@CHARSET "UTF-8";
 
#creneaux th {
    text-align: center;
}
 
#creneaux td {
    text-align: center;
    font-weight: bold;
}
 
.status-metro {
  display: inline-block;
  padding: 2px 5px;
  color:#fff;
}
 
.status-metro.status-active {
  background: #43c83c;
}
 
.status-metro.status-suspended {
  background: #fa3031;
}

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 fila [row] y un cuadro de alerta de color [alert alert-warning];
  • línea 9: la tabla abarcará 6 columnas [col-md-6];
  • línea 10: la tabla HTML está formateada por Bootstrap [class='table'];
  • línea 13: el atributo [data-toggle] especifica la columna que contiene el símbolo [+/-] que expande/colapsa la fila;
  • línea 19: el atributo [data-hide='phone'] especifica que la columna debe ocultarse si la pantalla tiene el tamaño de la pantalla de un teléfono. También se puede utilizar el valor «tablet»;

3.6.8. Ejemplo 8

Para ayudar al usuario, crearemos información sobre herramientas alrededor de los componentes principales de las vistas:

Para ello, duplicamos [app-01.html] en [app-09.html] y añadimos las siguientes líneas:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
      <!-- menu options -->
      <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active">
            <a href="">
              <span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
            </a>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- local script -->
<script>
  // --------------------- module Angular
  angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>

Las descripciones emergentes las proporciona la biblioteca [angular-ui-bootstrap], que a su vez depende de la biblioteca [angular]. La línea 50 importa la biblioteca [angular-ui-bootstrap]. Para implementar los componentes de la biblioteca [angular-ui-bootstrap], necesitamos crear un módulo Angular. Esto se hace en las líneas 52–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. Estos se denominan dependencias del módulo. Se proporcionan en una matriz como segundo parámetro de la función [angular.module]. Aquí, el módulo llamado [ui.bootstrap] lo proporciona la biblioteca [angular-ui-bootstrap]. Este módulo nos proporcionará las descripciones emergentes.

La línea 54 define un módulo Angular. Por defecto, esto no tiene ningún efecto en la página. Especificamos que la página debe ser gestionada 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 gestionados por el módulo [ui.bootstrap].

La sintaxis de la información sobre herramientas es la siguiente:


 <span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>

Arriba, añadimos una información sobre herramientas al texto [Inicio]:

  • [tooltip]: define el texto de la información sobre herramientas;
  • [tooltip-placement]: define su posición (abajo, arriba, izquierda, derecha);

Angular JS permite añadir nuevas etiquetas o atributos a los ya existentes en HTML. Esta extensión de HTML se consigue mediante directivas de Angular. En este caso, los atributos [tooltip] y [tooltip-placement] son creados por [angular-ui-bootstrap].

3.6.9. Ejemplo 9

Para ayudar al usuario a elegir la fecha de una cita, proporcionaremos un calendario:

Image

Al igual que con las descripciones emergentes, este calendario lo proporciona la biblioteca [angular-ui-bootstrap]. Para lograr este resultado, duplicamos [app-01.html] en [app-10.html] y añadimos las siguientes líneas:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <div>
    <pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
    <div class="row">
      <div class="col-md-2">
        <h4>Calendrier</h4>
 
        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
        </div>
 
        </div>
      </div>
    </div>
  </div>
</div>
...
<!-- local script -->
<script>
  // --------------------- module Angular
  angular.module("rdvmedecins", ['ui.bootstrap'])
</script>
 
</body>
</html>

Al igual que antes, la página está asociada a un módulo de Angular (líneas 2 y 28). El calendario se define mediante la etiqueta <datepicker> de la línea 16, proporcionada 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 cuadro gris con esquinas redondeadas;
  • [ng-model='day']: los atributos [ng-*] son atributos de Angular. El atributo [ng-model] designa los datos que se colocarán en el modelo de vista. Cuando el usuario haga clic en una fecha, esta se colocará en la variable [day] del modelo. Esta variable se utiliza en la línea 10. La sintaxis {{expression}} evalúa una expresión compuesta por elementos del modelo. Aquí, {{day}} mostrará el valor de la variable [day] del modelo. Una característica clave de Angular es que la vista se actualiza automáticamente en respuesta a los cambios en la variable [day]. Así, cuando el usuario cambie las fechas, estos cambios se mostrarán inmediatamente en la línea 10. En términos generales, el proceso funciona de la siguiente manera:
    • una vista V está asociada a un modelo M;
    • Angular observa el modelo M y actualiza automáticamente la vista V cuando se produce un cambio en su modelo M;

La sintaxis {{day|date}} se denomina filtro. No es el valor de [day] lo que se muestra, sino el valor de [day] filtrado a través de un filtro llamado [date]. Este filtro está predefinido en Angular. Se utiliza para dar formato a las fechas. Acepta parámetros que especifican el formato deseado. Por lo tanto, la expresión {{day | date:'fullDate'}} indica que queremos el formato de fecha completo, en este caso [Friday, June 20, 2014], ya que el calendario está en inglés por defecto. Hablaremos de su internacionalización en breve.

3.6.10. Conclusión

Hemos presentado los elementos del marco CSS Bootstrap que vamos a utilizar. Se trataba de componentes pasivos: sus eventos no se gestionaban. Por lo tanto, al hacer clic en botones o enlaces no ocurría nada. Estos eventos se gestionarán en JavaScript. Es posible utilizar este lenguaje sin marcos, pero al igual que ocurría en el lado del servidor, ciertos marcos son esenciales en el lado del cliente. Este es el caso del marco AngularJS, que aporta un nuevo enfoque al desarrollo de aplicaciones JavaScript ejecutadas por un navegador. Lo presentaremos ahora.

3.7. Introducción a AngularJS

A continuación ilustraremos algunas de las características del marco AngularJS utilizado en la aplicación. Ya nos hemos encontrado con algunas de ellas:

  • Una página HTML funciona con AngularJS si tiene un módulo asociado:

<html ng-app="rdvmedecins">
  • Angular te permite crear nuevas etiquetas y atributos HTML utilizando directivas:
attributs : ng-app, ng-model, tooltip-placement, tooltip
balises : datepicker
  • Angular te permite crear filtros:
{{jour|date:'fullDate'}}
  • Una vista V muestra un modelo M. Angular supervisa el modelo M y actualiza automáticamente la vista V cada vez que se produce un cambio en el modelo M. El valor de una variable del modelo M se muestra en la vista V utilizando:
{{variable}}

Comenzaremos profundizando en la implementación del patrón de diseño Modelo-Vista-Controlador en Angular. Repasemos las relaciones entre ellos desde una perspectiva arquitectónica:

  • La vista V1 muestra el modelo M1 construido por el controlador C1. El controlador C1 contiene no solo el modelo M1, sino también los controladores de eventos para la vista V1. Nos encontramos en los ciclos 5, 8 y 9:
    • [5]: Se produce un evento en la vista V1. Lo gestiona el controlador C1;
    • el controlador realiza su tarea [6-7] y, a continuación, construye el modelo M1 [8];
    • [9]: La vista V1 muestra el nuevo modelo M1. Como hemos mencionado, este paso final es automático. A diferencia de otros marcos MVC, no hay un envío explícito (C1 envía el modelo M1 a V1) ni una recepción explícita (la vista V1 obtiene el modelo M1 de C1). Hay 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

Volvamos al ejemplo del calendario. Ya hemos visto la directiva que lo genera:


          <datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>

Esta directiva admite otros atributos además de los mostrados anteriormente, incluido el atributo [min-date], que establece la fecha más temprana que se puede seleccionar en el calendario. Esto nos resultará útil. Cuando el usuario seleccione una fecha para una cita, esta debe ser igual o posterior a la fecha actual. Por lo tanto, escribiremos:


<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>

donde [dateMin] será una variable del modelo de la página con un valor igual a la fecha de hoy. El resultado será la siguiente página:

  • en [1], es 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 los siguientes cambios:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <div>
    <pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
    <div class="row">
      <div class="col-md-2">
        <h4>Calendrier</h4>
 
        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
        </div>
      </div>
    </div>
  </div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
...
<!-- local script -->
<script>
  // --------------------- module Angular
  angular.module("rdvmedecins", ['ui.bootstrap']);
  // contrôleur
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        // date minimale
        $scope.minDate = new Date();
      }]);
 
</script>
 
</body>
</html>

Examinemos primero el script local en las líneas 26-37:

  • línea 28: creación del módulo [rdvmedecins] con su dependencia del módulo [ui.bootstrap], que proporciona el calendario;
  • líneas 30–35: creación de un controlador. Este es el que contendrá el modelo de nuestra página. Aquí no habrá ningún controlador de eventos;
  • Líneas 30-31: El controlador [rdvMedecinsCtrl] pertenece al módulo [rdvmedecins]. Se pueden añadir tantos controladores como se desee a un módulo. En nuestra aplicación, tendremos:
    • un módulo de gestión de la aplicación;
    • un controlador por vista;
  • el segundo parámetro de la función [controller] es una matriz de la forma ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)]. El último parámetro es la función que implementa el controlador. Sus parámetros son objetos que AngularJS proporcionará a la función.

Volvamos a la arquitectura de una aplicación Angular:

Arriba, el controlador C1 contiene todos los controladores de eventos para la vista V1, así como su modelo M1. Los controladores de eventos pueden necesitar uno o más servicios [6] para realizar sus tareas. Pasamos todos ellos como parámetros a la función constructora del controlador:

['S1', 'S2', ..., 'Sn', function(S1, S2, ..., Sn)]

Los servicios Si son singletons. Angular crea una única instancia de cada uno. Se identifican mediante un nombre Si. ¿Por qué aparecen dos veces en la tabla anterior? En producción, los scripts JS se minifican. Durante este proceso de minificación, la tabla anterior queda así:

['S1', 'S2', ..., 'Sn', function(a1, a2, ..., an)]

Los parámetros pierden sus nombres. Sin embargo, estos son los nombres de los servicios. Por lo tanto, es importante conservar estos nombres. Por eso se pasan como cadenas como parámetros que preceden a la función. Las cadenas no se alteran durante el proceso de minificación. Cuando Angular compila el controlador utilizando la nueva matriz, sustituirá a1 por S1, a2 por S2, y así sucesivamente. 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]:


  // controller
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        // minimum date
        $scope.minDate = new Date();
}]);
  • líneas 3-4: el único objeto inyectado en el controlador es el objeto $scope. Se trata de un objeto predefinido que representa el modelo M de las vistas asociadas al controlador. Para enriquecer el modelo de una vista, basta con añadir campos al objeto $scope;
  • que es lo que se hace en la línea 6. Creamos el campo [minDate] con la fecha actual como su valor;

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 está asociado al controlador [rdvMedecinsCtrl] a través del 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 recuperar su modelo M. Una página HTML puede depender de varios controladores, estén o no anidados unos dentro de otros:
<div id='div1' ng-controller='c1'>
    ...
    <div id='div11' ng-controller='c11'>
    ...
    </div>
    ...
    <div id='div12' ng-controller='c12'>
    ...
    </div>
</div>

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 controladores de eventos del controlador c1;
  • el contenido de [div11] (líneas 3-4) muestra el modelo M11 gestionado por el controlador c11, así como el modelo M1. Existe herencia de modelos. Las etiquetas de esta zona pueden hacer referencia tanto a los controladores de eventos del controlador c11 como a los del controlador c1. No pueden hacer referencia ni al modelo M12 del controlador c12 ni a sus controladores de eventos. El controlador c12 no está definido entre las líneas 3 y 5;
  • líneas 7-9: podemos aplicar un razonamiento similar al utilizado anteriormente;

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, esto es [$scope.minDate]. El campo siempre se busca en el objeto $scope.

3.7.2. Ejemplo 2: Localización de fechas

Por ahora, el calendario no nos resulta muy útil, ya que está en inglés. Es posible localizarlo:

  • en [1], tenemos un calendario en francés;
  • en [2], lo cambiamos al inglés;
  • en [3], el calendario en inglés;

Duplicamos la página [app-11.html] en [app-12.html] y, a continuación, 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">
    <!-- the calendar-->
    <div class="col-md-4">
      <h4>Calendrier</h4>
 
      <div style="display:inline-block; min-height:290px;">
        <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
      </div>
    </div>
    <!-- languages -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
    </div>
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins.js"></script>
</body>
</html>

Hay pocos cambios. La única adición son las líneas 21–31, que contienen la lista desplegable de idiomas. Por primera vez, nos encontramos con un controlador de eventos en las líneas 27–28:

  • línea 27: el atributo [ng-click] es un atributo de Angular que especifica el controlador de eventos que se ejecutará cuando se haga clic en el elemento que lo lleva. Aquí se ejecutará la función [$scope.setLang('fr')]. Establecerá 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 gestiona la localización de la vista con un módulo llamado [ngLocale]. Por lo tanto, la definición de nuestro módulo [rdvmedecins] será la siguiente:


  // --------------------- Angular module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);

Línea 2: No olvides las dependencias, ya que los mensajes de error de Angular pueden resultar a veces imprecisos. Omitir una dependencia es especialmente difícil de detectar. Aquí tenemos una nueva dependencia del módulo [ngLocale].

Por defecto, Angular solo gestiona la localización de fechas, números, etc., que tienen variantes locales. No gestiona la internacionalización del texto. Para ello, utilizaremos la biblioteca [angular-translate]. La localización la gestiona la biblioteca [angular-i18n]. Esta biblioteca incluye tantos archivos como variantes hay para fechas, números, etc.

  

Para el calendario francés, utilizaremos el archivo [angular-locale_fr-fr.js], y para el calendario inglés, el archivo [angular-locale_en-us.js]. Echemos un vistazo al contenido del archivo [angular-locale_fr-fr.js], por ejemplo:


'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;}
});
}]);

Estos son los elementos utilizados para crear un calendario francés:

  • líneas 10–18: la matriz de días de la semana;
  • líneas 19–32: la matriz de los meses del año;
  • líneas 33-41: la tabla abreviada de los días de la semana;
  • líneas 42–55: la tabla de meses abreviados del año;
  • líneas 56–63: formatos de fecha y hora. La línea 62 muestra el formato «dd/mm/aa» utilizado para las fechas en francés;
  • líneas 65–95: información sobre el formato de los números. Esto no es relevante aquí;
  • línea 96: el identificador «fr-fr» para 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 EE. UU. (en-us).

El código anterior no es muy fácil de leer. Si lo lees con atención, verás que todo este código define la variable [$locale] en la línea 4. Es cambiando el valor de esta variable como logramos la internacionalización de fechas, números, divisas, etc. Curiosamente, Angular no permite cambiar la variable [$locale] en tiempo de ejecución. Se define de una vez por todas importando el archivo correspondiente a la configuración regional deseada:


<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>

No tiene sentido importar todos los archivos para las configuraciones regionales deseadas, ya que, como hemos visto, cada archivo solo hace una cosa: definir la variable [$locale]. El último archivo importado tiene prioridad, y no hay forma de cambiar la configuración regional posteriormente.

Mientras buscaba en la web una solución a este problema, no pude encontrar ninguna. Aquí propongo una [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. La idea es poner las diferentes configuraciones regionales que necesitamos en un diccionario. Ahí es donde las recuperaremos cuando necesitemos cambiarlas. El código JavaScript en [rdvmedecins.js] tiene la siguiente estructura:

 

Si eliminamos las definiciones de configuración regional, que ocupan 200 líneas (líneas 15-215 arriba), el código es sencillo:

  • línea 6: define el módulo [rdvmedecins] y sus dependencias;
  • líneas 8-10: define el controlador [rdvMedecinsCtrl] de la página;
  • línea 9: el constructor del controlador toma dos parámetros:
    • $scope: para crear la plantilla de vista;
    • $locale: es la variable que gestiona la configuración regional del calendario. Es la que hay que modificar al cambiar de idioma;
  • línea 13: la variable del modelo [minDate] se inicializa con la fecha de hoy;
  • línea 15: define el diccionario [locales]. Fíjate en que no hemos escrito [$scope.locales]. La variable [locales] no forma parte del modelo expuesto a 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 de los archivos JS [angular-locale_fr-fr.js] y [angular-locale_en-us.js], respectivamente. Lo más difícil es no cometer un error con los numerosos paréntesis de este diccionario...
  • línea 217: inicializamos la variable $locale con locales['fr'], es decir, la versión francesa de la configuración regional. No podemos escribir simplemente [$locale=locales['fr']] porque eso asignaría la dirección de locales['fr'] a $locale. Debemos realizar una copia del valor. Esto se puede hacer utilizando la función predefinida [angular.copy];
  • línea 219: la variable [day] del modelo se inicializa con la fecha de hoy. Esto garantiza que el calendario se muestre con la fecha establecida en hoy;
  • Líneas 223-230: definen el controlador de eventos que se invoca cuando cambia el idioma. Fíjate en la sintaxis:
$scope.nom_fonction=function(param1, param2, ...){...}

para definir un controlador de eventos llamado [nombre_función] que acepta los parámetros [param1, param2, ...];

Repasemos el código HTML de la lista desplegable:


    <!-- languages -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
</div>
  • línea 8: al seleccionar francés se activa la llamada a [setLang('fr')];
  • línea 9: al seleccionar inglés se activa la llamada a [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 vista;

Volvamos al código de [rdvmedecins.js]:

  • línea 225: cambiamos el valor de la variable [$locale] por el valor adecuado del diccionario [locales];
  • línea 227: mencionamos que cuando el modelo M de una vista V cambia, la vista V se actualiza automáticamente con el nuevo modelo. En la línea 225, cambiamos el valor de la variable [$locale], que no forma parte del modelo M mostrado por la vista V. Necesitamos encontrar una forma de actualizar este modelo M para que el calendario se actualice y utilice su nueva configuración regional. Aquí, cambiamos la variable [day] en el modelo del calendario. La inicializamos con un nuevo puntero (new) que apunta a una fecha idéntica a la que se muestra actualmente. [$scope.day.getTime()] es el número de milisegundos transcurridos entre el 1 de enero de 1970 y la fecha mostrada por el calendario. Utilizando este número, reconstruimos una nueva fecha. Por supuesto, obtendremos la misma fecha, y el calendario seguirá situado en la fecha que mostraba. Pero el valor de [$scope.day], que en realidad es un puntero, habrá cambiado, y el calendario se actualizará;
  • línea 229: establecemos el valor de la variable [isopen] en la plantilla en false. 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] cambiará a false, lo que cerrará la lista desplegable.

3.7.3. Ejemplo 3: Internacionalización del texto

Volvamos a la localización del calendario:

En [3], vemos que el calendario está en inglés, pero los textos de [Calendar, Languages] no lo están. Por defecto, Angular no proporciona una herramienta para internacionalizar mensajes. Aquí utilizaremos la biblioteca [angular-translate] (https://github.com/angular-translate/angular-translate).

Desarrollaremos el siguiente ejemplo:

  • en [1], la versión 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:


  // --------------------- Angular module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// configuration i18n
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // messages français
    $translateProvider.translations("fr", {
      'msg_header': 'Cabinet Médical<br/>Les Médecins Associés',
      'msg_langues': 'Langues',
      'msg_agenda': 'Agenda de {{titre}} {{prenom}} {{nom}}<br/>le {{jour}}',
      'msg_calendrier': 'Calendrier',
      'msg_jour': 'Jour sélectionné : ',
      'msg_meteo': "Aujourd'hui, il va pleuvoir..."
    });
    // messages anglais
    $translateProvider.translations("en", {
      'msg_header': 'The Associated Doctors',
      'msg_langues': 'Languages',
      'msg_agenda': "{{titre}} {{prenom}} {{nom}}'s Diary<br/> on {{jour}}",
      'msg_calendrier': 'Calendar',
      'msg_jour': 'Selected day: ',
      'msg_meteo': 'Today, it will be raining...'
    });
    // langue par défaut
    $translateProvider.preferredLanguage("fr");
}]);
  • línea 2: el primer cambio es la adición de una nueva dependencia. La internacionalización de la aplicación requiere el módulo de Angular [pascalprecht.translate];
  • líneas 5–26: definen la función [config] del módulo [rdvmedecins]. Cuando se inicia una aplicación Angular, el marco de trabajo instancia todos los servicios requeridos por la aplicación, incluidos los servicios predefinidos de Angular y los servicios definidos por el usuario. Por ahora, no hemos definido ningún servicio. La función [config] del módulo de una aplicación se ejecuta antes de que se instancie ningún servicio. Se puede utilizar para definir información de configuración para los servicios que se instanciarán posteriormente. Aquí, la función [config] se utilizará para definir los mensajes internacionalizados de la aplicación;
  • línea 5: el parámetro de la función [config] es una matriz ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)] donde Oi es un objeto conocido 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] toma dos parámetros:
    • El primer parámetro es la clave de un idioma. Puedes usar la que quieras. Aquí hemos utilizado «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 {'key1':'msg1', 'key2':'msg2', ...};
  • líneas 7–14: los mensajes en francés;
  • líneas 16-23: los mensajes en inglés;
  • línea 25: el método [preferredLanguage] establece el idioma predeterminado. Su parámetro es uno de los argumentos utilizados como primer parámetro de la función [$translateProvider.translations], por lo que aquí es «fr» (línea 7) o «en» (línea 16);
  • Ten en cuenta 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 los siguientes cambios:


<div class="container">
  <!-- a first text with HTML elements in it -->
  <h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
  <!-- a second text with parameters -->
  <h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
  <!-- a third text translated by the controller -->
  <h3 class="alert alert-danger">{{msg2}}</h3>
 
  <pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
  <div class="row">
    <!-- the calendar-->
    <div class="col-md-4">
      <h4>{{'msg_calendrier'|translate}}</h4>
 
      <div style="display:inline-block; min-height:290px;">
        <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
      </div>
    </div>
    <!-- languages -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          {{'msg_langues'|translate}}<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
    </div>
  </div>
</div>
  • Las traducciones se producen en las líneas 3, 5, 9, 13 y 23;
  • hay tres sintaxis distintas:
    • 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={{dictionary]}}] (línea 5), que 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:

línea
Francés
Inglés
3
Consulta médica<br/>The Associated Doctors
The Associated Doctors
13
Calendario
Calendario
23
Idiomas
Idiomas
9
Día seleccionado:
Día seleccionado:

Veamos ahora la línea 5:


<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

Tenga en cuenta que [msg.text] y [msg.model] no están entre comillas simples. No se trata de cadenas, sino de elementos del modelo:

  • msg.text: define la clave del mensaje configurado que se va a utilizar;
  • msg.model: es el diccionario que proporciona los valores de los parámetros;

Los nombres de los campos [text, model] pueden ser cualquiera. En el controlador [rdvMedecinsCtrl] de la vista, el objeto [msg] se define de la siguiente manera:

Image

  • línea 245: la definición del objeto [msg];
  • línea 245: el campo [text] tiene el valor [msg_agenda], que está asociado a dos valores:
    • {{title}} Diario de {{first_name}} {{last_name}}<br/> el {{day}} en el diccionario francés;
    • {{title}} Diario de {{first_name}} {{last_name}}<br/> el {{day}} en el diccionario inglés;

Por lo tanto, el mensaje que se va a mostrar tiene cuatro parámetros [title, first_name, last_name, day];

  • Línea 245: El campo [model] es un diccionario que asigna valores a estos cuatro parámetros. Hay un problema con el parámetro [day]. Queremos mostrar el nombre completo del día. Esto varía dependiendo de si el idioma es francés o inglés. Por lo tanto, utilizamos el filtro [date], que ya se ha utilizado en la vista, en la forma {{ day | date:'fullDate'}}. Cualquier filtro se puede utilizar en el código JavaScript en la forma $filter('filter')(value, options), donde $filter es un objeto Angular predefinido y 'filter' es el nombre del filtro;
  • líneas 33-34: el objeto $filter predefinido 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 troisième texte traduit par le contrôleur -->
<h3 class="alert alert-danger">{{msg2}}</h3>

Todas las traducciones anteriores se realizaron en la vista utilizando atributos del módulo [pascalprecht.translate]. También podemos optar por realizar esta traducción en el lado del servidor. Eso es lo que se hace aquí. En el controlador (línea 247 en la captura de pantalla anterior), tenemos el siguiente código:


$scope.msg2 = $filter('translate')('msg_meteo');

Utilizamos la misma sintaxis que para el filtro «date», ya que «translate» también es un filtro. Aquí, solicitamos el mensaje con la clave «msg_meteo».

Analicemos el mecanismo para los cambios de idioma. Vimos que la función [config] del módulo [rdvmedecins] había establecido el francés como idioma predeterminado (línea 9 a continuación):


// i18n configuration
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // french messages
    $translateProvider.translations("fr", {...});
    // english messages
    $translateProvider.translations("en", {...});
    // default language
    $translateProvider.preferredLanguage("fr");
}]);

Ten en cuenta que la configuración regional predeterminada también era el francés. En la inicialización del controlador [rdvmedecins], escribimos:


// we put the locale in French
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 mensajes que proporciona el módulo [pascalprecht.translate] y la localización de fechas que hemos implementado. Esta última utiliza una variable $locale que no es utilizada por el módulo [pascalprecht.translate]. Se trata de dos procesos independientes entre sí.

Ahora es el momento de ver qué ocurre cuando el usuario cambia el idioma:

Image

  • línea 251: cuando cambia el idioma, se llama a la función [setLang] con uno de los dos parámetros ['fr','en'];
  • líneas 252–257: ya se han explicado; cambian la variable [$locale] del calendario. Esto no tiene ningún efecto sobre el idioma de las traducciones;
  • línea 259: cambiamos el idioma de traducción. Utilizamos el objeto [$translate] proporcionado por el módulo [pascalprecht.translate]. Para ello, debemos inyectarlo en el controlador:

// controller
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 establecerse en una de las claves utilizadas en la configuración como primer parámetro de la función [$translateProvider.translations], es decir, «fr» o «en». Este es, de hecho, el caso;
  • Línea 261: Recalculamos el valor de msg2. ¿Por qué? En la vista, tras el cambio de idioma realizado en la línea 259, todos los atributos [translate] existentes se volverán a evaluar. Este no será el caso de la expresión {{msg2}}, que no tiene este atributo. Por lo tanto, su nuevo valor se calcula en el controlador. Esto debe hacerse después del cambio de idioma en la línea 259 para que se utilice el nuevo idioma en el cálculo de [msg2];

Si nos detenemos aquí, observamos dos anomalías:

  1. en [1], el día sigue apareciendo en francés, mientras que el resto de la vista está en inglés;
  2. en [2] y [3], la fecha seleccionada es el 24 de junio, mientras que en [1] la fecha sigue fijada en el 20 de junio;

Intentemos explicar estos problemas 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, aunque el atributo [translate] se reevalúa durante un cambio de idioma, no fue así en el caso del atributo [translate-values]. Por lo tanto, podemos forzar esta evaluación en el controlador:


      // ------------------- evts manager
      // language change
      $scope.setLang = function (lang) {
...
        // update msg2
        $scope.msg2 = $filter('translate')('msg_meteo');
        // and msg day
        $scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};

Cada vez que cambia el idioma, la línea 8 anterior vuelve a calcular el día que se muestra. Esto resuelve eficazmente 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 V mostrada solo cambia si cambia su modelo M. Sin embargo, en este caso, al seleccionar un nuevo día en el calendario se desencadena un evento que no se gestiona, lo que significa que el modelo [msg] no cambia y, por lo tanto, la vista tampoco cambia. Actualizamos la definición del calendario en la vista:


<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>

Arriba, especificamos que el clic en el calendario debe ser gestionado por la función [$scope.calendarClick]. Esta función es la siguiente:

Image

  • línea 267: el controlador de clic del calendario;
  • línea 269: forzamos la actualización del día mostrado utilizando el mensaje [msg];

3.7.4. Ejemplo 4: Un servicio de configuración

Repasemos la arquitectura de una aplicación AngularJS:

Aquí nos centraremos en el concepto de servicio. Se trata de un concepto bastante amplio. Si bien la capa [DAO] anterior 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 identifica por ese nombre;
  • Angular puede inyectar un servicio en controladores y otros servicios;

Algunos de los servicios que configuraremos en el módulo [rdvmedecins] necesitarán ser configurados. Dado que un servicio puede inyectarse en otro servicio, resulta tentador realizar la configuración en un servicio al que llamaremos [config] e inyectarlo en los servicios y controladores que deban configurarse. A continuación describiremos este proceso.

Duplicamos [app-13.html] en [app-14.html] y realizamos los siguientes cambios:


<div class="container">
  <!-- waiting msg control -->
  <label>
    <input type="checkbox" ng-model="waiting.visible">
    <span>Voir le message d'attente</span>
  </label>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
            {{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>
...
</div>
...
<script type="text/javascript" src="rdvmedecins-02.js"></script>
  • Líneas 3–6: Una casilla de verificación que controla si se muestra el mensaje de espera de las líneas 9–15. El valor de la casilla de verificació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 establecemos la variable [waiting.visible] en true, la casilla de verificación aparecerá 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í que cuando marcamos la casilla de verificación en la línea 4:
    • se asigna el valor «true» a la variable [waiting.visible] (ng-model, línea 4);
    • dado que se ha producido un cambio en el modelo M, la vista V se reevalúa automáticamente. El mensaje de espera se hará entonces visible (ng-show, línea 9);
    • el razonamiento es similar al desmarcar la casilla de verificación de la línea 4: el mensaje «waiting» se oculta;
  • línea 10: el mensaje «waiting» 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: se traduce la etiqueta del botón;
  • línea 19: colocamos el código JavaScript de la aplicación en un nuevo archivo JS [rdvmedecins-02] para no perder el código que ya se ha escrito y que ahora hay que reorganizar;

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]:

Image

  • 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 el diccionario locales={&#x27;fr&#x27;:..., &#x27;en&#x27;: ...} en el controlador, que tenía 200 líneas. Este diccionario es claramente un elemento de configuración, por lo que lo trasladamos al servicio [config] en las líneas 38–39. Este servicio se define de la siguiente manera:

Image

  • Líneas 38-39: Se crea un servicio utilizando la función [factory] del objeto [angular.module]. La sintaxis de esta función es la misma que la de las anteriores: factory('service_name', ['O1', 'O2', ..., 'On', function (O1, O2, ..., On){...}]), donde los Oi son nombres de objetos conocidos por Angular (predefinidos o creados por el desarrollador) que Angular inyecta como parámetros en la función factory. Dado que la función no tiene parámetros aquí, hemos utilizado una sintaxis más corta, igualmente válida: factory('service_name', function (){...}]);
  • línea 40: la función [factory] debe implementar el servicio utilizando 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 del servicio adopta la forma:


Angular.module('nom_module')
  .factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
     // service preparation
    ...
     // render the object implementing the service
    return {
         // fields
        ...
         // methods
        ...
        }
});
  • Línea 6: Devolvemos un objeto JavaScript que puede contener tanto campos como métodos. Son los métodos los que gestionan el servicio;

Aquí, el servicio [config] solo define campos y ningún método. Pondremos aquí todo lo que se pueda configurar en la aplicación:

  • líneas 42-47: las claves de los mensajes que se van a traducir;
  • líneas 59–62: las URL de la aplicación;
  • líneas 64–69: las URL del servicio web remoto;
  • línea 71: una llamada HTTP a un servicio web que no responde puede tardar mucho tiempo. Aquí, establecemos el tiempo máximo de espera para la respuesta del servicio web en 1 segundo. Transcurrido este tiempo, la llamada HTTP falla y se lanza una excepción de JavaScript;
  • línea 73: antes de cada llamada al servidor, simularemos 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 modo que el usuario pueda cancelar una operación que haya iniciado. Para que sea cancelable, debe durar al menos unos segundos. Utilizaremos esta espera artificial para simular operaciones de larga duración;
  • línea 75: en el modo [debug=true], se muestra información adicional en la vista actual. Por defecto, este modo está habilitado. En producción, estableceríamos este campo en false;
  • líneas 77–278: el diccionario para 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:

Image

  • líneas 284-285: el servicio [config] se inyecta en el controlador;
  • línea 290: el diccionario [locales] se encuentra ahora en el servicio [config] y ya no en el controlador;
  • línea 294: el objeto [waiting] que controla la visualización del mensaje de espera. La clave del mensaje de espera se encuentra en el servicio [config] (campo text). Por defecto, el mensaje de espera está oculto (campo visible). El campo cancel tiene como valor el nombre de la función de la línea 316. Por lo tanto, este campo es un método o una función;
  • línea 316: la función [cancel] es privada (no hemos escrito $scope.cancel=function(){}). Repasemos el 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 llama al método [$scope.waiting.cancel()]. En última instancia, es la función privada cancel de la línea 316 la que se ejecuta. Simplemente oculta el mensaje de espera estableciendo la variable del modelo [waiting.visible] en false (línea 318);

3.7.5. Ejemplo 5: Programación asíncrona

Ahora presentaremos un nuevo servicio con un nuevo concepto: 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 para acceder al servicio web de programación de citas. Lo presentaremos en breve;

Escribiremos la siguiente aplicación:

  • El objetivo es mostrar el banner [2] durante un tiempo establecido en [1]. La espera se puede cancelar con [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">
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>
 
  <!-- the form -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="form-group">
      <label for="waitingTime">{{waitingTimeText | translate}}</label>
      <input type="text" id="waitingTime" ng-model="waiting.time"/>
    </div>
    <button class="btn btn-primary" ng-click="execute()">Exécuter</button>
  </div>
</div>
..
<script type="text/javascript" src="rdvmedecins-03.js"></script>
</body>
</html>
  • línea 11: el atributo [ng-cloak] impide que se muestre el área hasta que se hayan evaluado sus expresiones Angular. Esto evita que el área aparezca brevemente antes de que se evalúe el atributo [ng-show], lo que en realidad hará 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:

Image

  • línea 6: el módulo Angular que gestiona la aplicación;
  • línea 10: la función [config] utilizada para internacionalizar los mensajes;
  • línea 41: el servicio [config] que hemos descrito;
  • línea 286: el servicio [utils] que vamos a crear;
  • línea 315: el controlador [rdvmedecinsCtrl] que vamos a crear;

Añadimos una nueva clave de mensaje a la función [config] (líneas 6, 11):


angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // french messages
    $translateProvider.translations("fr", {
...
      'msg_waiting_time_text': "Temps d'attente : "
    });
    // english messages
    $translateProvider.translations("en", {
...
      'msg_waiting_time_text': "Waiting time:"
    });
    // default language
    $translateProvider.preferredLanguage("fr");
}]);

Añadimos una nueva línea (línea 6) al servicio [config] para esta clave de mensaje:


angular.module("rdvmedecins")
  .factory('config', function () {
    return {
      // messages to be internationalized
      ...
waitingTimeText: 'msg_waiting_time_text',

El servicio [utils] contiene dos métodos (líneas 4 y 12):


angular.module("rdvmedecins")
  .factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
    // display the Json representation of an object
    function debug(message, data) {
      if (config.debug) {
        var text = data ? message + " : " + angular.toJson(data) : message;
        console.log(text);
      }
    }
 
    // waiting
    function waitForSomeTime(milliseconds) {
      // asynchronous waiting milliseconds milliseconds
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // we return the task
      return task;
    };
 
    // service authority
    return {
      debug: debug,
      waitForSomeTime: waitForSomeTime
    }
}]);
  • línea 2: se invoca el servicio [utils] (primer parámetro). Depende de tres servicios: dos servicios predefinidos de Angular, $timeout y $q, y el servicio config. El servicio [$timeout] permite ejecutar una función una vez transcurrido un tiempo determinado. El servicio [$q] permite crear tareas asíncronas;
  • línea 4: una función local [debug];
  • línea 12: una función local [waitForSomeTime];
  • líneas 23–26: la instancia del servicio [utils]. Se trata de un objeto que expone dos métodos, los de las líneas 4 y 12. Ten en cuenta que los campos del objeto pueden tener cualquier nombre. Por coherencia, les hemos dado los nombres de las funciones a las que hacen referencia;
  • líneas 4–9: el método [debug] escribe un mensaje [message] en la consola y, si procede, 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 utilizando el objeto predefinido [$q] (https://docs.angularjs.org/api/ng/service/$q). A continuación se muestra la API de la tarea denominada [deferred] en la documentación de Angular:

Image

  • una tarea asíncrona [task] se crea mediante la instrucción [$q.defer()];
  • se completa utilizando uno de estos dos métodos:
    • [task.resolve(value)]: que completa con éxito la tarea y devuelve el valor [value] a quienes esperan a que la tarea termine;
    • [task.reject(value)]: que termina la tarea con un error y devuelve el valor [value] a quienes esperan a que la tarea finalice;

La tarea [task] puede proporcionar información periódicamente a quienes esperan a que termine:

    • [task.notify(value)]: envía el valor [value] a quienes esperan a que la tarea termine. La tarea continúa ejecutándose;

Quienes deseen esperar a que la tarea finalice utilizan su campo [promise]:

var promise=[task].promise ;

El objeto [promise] tiene la siguiente API (http://www.frangular.com/2012/12/api-promise-angularjs.html):

Image

Para gestionar tanto el éxito como el fracaso de la tarea, escribimos:

1
2
3
var promise=[task].promise;
promise.then(successCallback, errorCallBack);
promise['finally'](finallyCallback);
  • Línea 1: Recuperamos la promesa de la tarea;
  • línea 2: definimos las funciones que se ejecutarán en caso de éxito o de fallo. Podemos optar por no incluir una función de fallo. La función [successCallback] solo se ejecutará si la [tarea] se completa con éxito [task.resolve()]. La función [errorCallback] solo se ejecutará si la [tarea] falla [task.reject()].
  • Línea 3: Definimos la función que se ejecutará después de que se haya ejecutado una de las dos funciones anteriores. Aquí colocamos el código común a ambas funciones [successCallback, errorCallback].

Volvamos al código de la función [waitForSomeTime]:


    // attente
    function waitForSomeTime(milliseconds) {
      // attente asynchrone de milliseconds millisecondes
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // on retourne la tâche
      return task;
};
  • línea 4: se crea una tarea;
  • líneas 5–7: el objeto [$timeout] permite definir una función (primer parámetro) que se ejecuta tras un cierto retraso 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: tras el retraso de [millisegundos], la tarea se completa con éxito;
  • línea 9: se devuelve la tarea [task]. Es importante señalar aquí que la línea 9 se ejecuta inmediatamente después de definir el objeto [$timeout]. No esperamos a que transcurra el retraso de [millisegundos]. Por lo tanto, el código de las líneas 2 a 10 se ejecuta en dos momentos diferentes:
    • la primera vez, cuando se define el objeto [$timeout];
    • la segunda vez, cuando ha transcurrido el tiempo de espera [millisegundos];

Se trata de una función asíncrona: su resultado se obtiene más tarde que su ejecución.

El código del controlador que utiliza el servicio [config] es el siguiente:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
    function ($scope, utils, config, $filter) {
      // ------------------- model initialization
      // waiting message
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      // waiting task
      var task;
      // logs
      utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
      utils.debug("locales['fr']=", config.locales['fr']);
 
      // execution action
      $scope.execute = function () {
        // log
        utils.debug('début', new Date());
        // the waiting msg is displayed
        $scope.waiting.visible = true;
        // simulated waiting
        task = utils.waitForSomeTime($scope.waiting.time);
        // end of wait
        task.promise.then(function () {
          // success
          utils.debug('fin', new Date());
        }, function () {
          // failure
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          // end of wait in all cases
          $scope.waiting.visible = false;
        });
 
      };
 
      // cancel wait
      function cancel() {
        // complete the task
        task.reject();
      }
    }]);
  • línea 3: el controlador utiliza el servicio [config];
  • línea 7: hemos añadido el campo [time] al objeto [$scope.waiting]. El objeto [$scope.waiting.time] recibe el valor del tiempo de espera establecido por el usuario;
  • línea 8: la clave del mensaje de espera que muestra la vista se coloca en el modelo [$scope.waitingTimeText]. Por lo general, todo lo que muestra una vista V debe colocarse en el objeto [$scope];
  • línea 10: una variable local. No se expone a la vista V;
  • líneas 12-13: uso del método [debug] del servicio [config]. Se muestra el siguiente resultado en la consola:
libellé temps d'attente : "Temps d'attente : "
locales['fr']= : {"DATETIME_FORMATS":{"AMPMS":["AM","PM"],"DAY":["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],"MONTH":["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],"SHORTDAY":["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],"SHORTMONTH":["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],"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":"","DECIMAL_SEP":",","GROUP_SEP":" ","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":" ¤)","posPre":"","posSuf":" ¤"}]},"id":"fr-fr"}

Línea 2: Obtenemos la representación JSON del objeto locales['fr'].

  • Línea 16: el método que se ejecuta cuando el usuario hace clic en el botón [Ejecutar];
  • línea 18: muestra la hora de inicio de la ejecución del método;
  • línea 22: se inicia la tarea [waitForSomeTime]. No esperamos a que termine. La ejecución continúa con la siguiente línea 24;
  • líneas 24-30: definen las funciones que se ejecutarán cuando la tarea finalice correctamente (línea 26) y en caso de error (línea 29);
  • línea 26: muestra la hora de finalización de la ejecución del método;
  • línea 29: muestra que la operación se ha cancelado. Esto ocurre solo cuando el usuario hace clic en el botón [Cancel]. La instrucción de la línea 41 detiene entonces la tarea asíncrona con un código de error;
  • líneas 31–34: definen la función que se ejecutará después de que se haya ejecutado una de las dos funciones anteriores;

Es importante comprender la secuencia de ejecución de este código. Si el usuario establece un retraso de 3 segundos y no cancela la espera:

  • al hacer clic en el botón [Ejecutar], se ejecuta la función [$scope.execute]. Las líneas 16-34 se ejecutan sin esperar los 3 segundos. Al final de esta ejecución, la vista V se sincroniza con el modelo M. Se muestra el mensaje de espera (ng-show=$scope.waiting.visible=true, línea 20) y se oculta el formulario (ng-hide=$scope.waiting.visible=true, línea 20);
  • a partir de este momento, el usuario puede volver a interactuar con la vista. En concreto, puede hacer clic en el botón [Cancelar];
  • si no lo hace, tras 3 segundos, se ejecuta la función [$timeout] (véanse las líneas 5-7 a continuación):

    // attente
    function waitForSomeTime(milliseconds) {
      // attente asynchrone de milliseconds millisecondes
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // on retourne la tâche
      return task;
};
  • Tras 3 segundos, se ejecuta el código. Este código completa la tarea [task] con un código de éxito (resolve). Esto activará la ejecución de todo el código que estaba esperando esta finalización (línea 4 a continuación):

        // simulated waiting
        task = utils.waitForSomeTime($scope.waiting.time);
        // end of wait
        task.promise.then(function () {
          // success
          utils.debug('fin', new Date());
        }, function () {
          // failure
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          // end of wait in all cases
          $scope.waiting.visible = false;
        });
 
  • Por lo tanto, se ejecutará la línea 6 anterior (finalizada con éxito). A continuación, se ejecutarán las líneas 11-14. Una vez que se haya ejecutado este código, volvemos a la vista V, que se sincronizará con su modelo M. El mensaje de espera se oculta (ng-show=$scope.waiting.visible=false, línea 13) y se muestra el formulario (ng-hide=$scope.waiting.visible=false, línea 13);

La pantalla se muestra entonces de la siguiente manera:

début : "2014-06-23T15:05:58.480Z"
fin : "2014-06-23T15:06:01.481Z"

Como se muestra arriba, hay un retraso de 3 segundos (06:01–05:58) entre el inicio y el final de la espera. Por el contrario, si el usuario cancela la espera antes de que transcurran los 3 segundos, se muestra lo siguiente:

début : "2014-06-23T15:08:09.564Z"
Opération annulée

Por último, es importante comprender que, en un momento dado, solo hay un hilo de ejecución, conocido como el hilo de la interfaz de usuario (UI). La finalización de una tarea asíncrona se señala mediante un evento, igual que al pulsar un botón. Este evento no se procesa de inmediato. Se coloca en la cola de eventos que esperan ser ejecutados. Cuando llega su turno, se procesa. Este procesamiento utiliza el hilo de la interfaz de usuario, por lo que durante este tiempo, la interfaz se congela. No responde a las entradas del usuario. Por esta razón, es importante que el procesamiento de eventos sea rápido. Dado que cada evento es procesado por el hilo de la interfaz de usuario, nunca es necesario resolver problemas de sincronización entre hilos que se ejecutan simultáneamente. En cualquier momento dado, solo se está ejecutando el hilo de la interfaz de usuario.

3.7.6. Ejemplo 6: Servicios HTTP

A continuación, presentamos el servicio [dao] que se comunica con el servidor web:

3.7.6.1. La vista V

Escribiremos un formulario para solicitar la lista de médicos:

Image

Duplicamos [app-01.html] en [app-16.html], que luego modificamos de la siguiente manera:


<div class="container" ng-cloak="">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>
 
  <!-- the request -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="form-group">
      <label for="waitingTime">{{waitingTimeText | translate}}</label>
      <input type="text" id="waitingTime" ng-model="waiting.time"/>
    </div>
    <div class="form-group">
      <label for="urlServer">{{urlServerLabel | translate}}</label>
      <input type="text" id="urlServer" ng-model="server.url"/>
    </div>
    <div class="form-group">
      <label for="login">{{loginLabel | translate}}</label>
      <input type="text" id="login" ng-model="server.login"/>
    </div>
    <div class="form-group">
      <label for="password">{{passwordLabel | translate}}</label>
      <input type="password" id="password" ng-model="server.password"/>
    </div>
    <button class="btn btn-primary" ng-click="execute()">{{medecins.title|translate:medecins.model}}</button>
  </div>
 
  <!-- list of doctors -->
  <div class="alert alert-success" ng-show="medecins.show">
    {{medecins.title|translate:medecins.model}}
    <ul>
      <li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
    </ul>
  </div>
 
  <!-- the error list -->
  <div class="alert alert-danger" ng-show="errors.show">
    {{errors.title|translate:errors.model}}
    <ul>
      <li ng-repeat="message in errors.messages">{{message|translate}}</li>
    </ul>
  </div>
 
</div>
...
<script type="text/javascript" src="rdvmedecins-04.js"></script>
  • Líneas 13–31: implementa el formulario. Este formulario no es visible cuando se muestra el mensaje de espera (ng-hide="waiting.visible"). Tenga en cuenta que los cuatro campos de entrada se almacenan en (atributos ng-model) [waiting.time (línea 16), server.url (línea 20), server.login (línea 24), server.password (línea 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}}"> ya vista anteriormente;
  • línea 36: una lista desordenada;
  • línea 37: la lista de médicos se encuentra en el modelo [medecins.data]. La directiva de Angular [ng-repeat] permite recorrer una lista. La sintaxis ng-repeat="doctor 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>, mostramos el título, el nombre y los apellidos del médico actual designado por la variable [medecin];
  • líneas 42-47: muestran la lista de errores. Esta lista no siempre está visible (ng-show="errors.show"). Esta visualización sigue el mismo patrón que la visualización de la lista de médicos. Por lo general, para mostrar una lista de objetos, utilizamos la directiva de Angular [ng-repeat];
  • línea 51: el código JavaScript se encuentra ahora en el archivo [rdvmedecins-04]

3.7.6.2. El controlador C y el modelo M

El código JavaScript cambia de la siguiente manera:

Image

  • 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 se utiliza para codificar en Base64 la cadena [login:password] enviada al servicio web para la autenticación;
  • líneas 12-13: la función de inicialización que contiene nuestros mensajes internacionalizados. Aparecen nuevos mensajes. No los trataremos más a fondo;
  • líneas 69–70: el servicio [config] que configura nuestra aplicación. Aquí se han añadido nuevas claves de mensajes. No las trataremos más a fondo;
  • líneas 318–319: el servicio [utils], que contiene métodos de utilidad. Se han añadido otros nuevos. Los presentaremos;
  • líneas 385–386: el servicio [dao] responsable de la comunicación con el servicio web. En esto es en lo que nos centraremos;
  • líneas 467–468: el controlador C para la vista V que acabamos de comentar. Lo trataremos ahora porque actúa como el orquestador que responde a las solicitudes de los usuarios;

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) {
      // ------------------- model initialization
      // model
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      $scope.server = {url: undefined, login: undefined, password: undefined};
      $scope.medecins = {title: config.listMedecins, show: false, model: {}};
      $scope.errors = {show: false, model: {}};
      $scope.urlServerLabel = config.urlServerLabel;
      $scope.loginLabel = config.loginLabel;
      $scope.passwordLabel = config.passwordLabel;
 
      // asynchronous task
      var task;
 
      // execution action
      $scope.execute = function () {
        // the UI is updated
        $scope.waiting.visible = true;
        $scope.medecins.show = false;
        $scope.errors.show = false;
        // simulated waiting
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        // waiting
        promise = promise.then(function () {
          // we ask for the list of doctors;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
        });
        // analyze the result of the previous call
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // we put the acquired data into the model
            $scope.medecins.data = result.data;
            // the UI is updated
            $scope.medecins.show = true;
            $scope.waiting.visible = false;
          } else {
            // there were errors in obtaining the list of doctors
            $scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // the UI is updated
            $scope.waiting.visible = false;
          }
        });
      };
 
      // cancel wait
      function cancel() {
        // complete the task
        task.reject();
        // the UI is updated
        $scope.waiting.visible = false;
        $scope.medecins.show = false;
        $scope.errors.show = false;
      }
 
    }
  ])
;
  • línea 2: el controlador tiene una nueva dependencia, concretamente el servicio [dao];
  • líneas 6–13: el modelo M de la vista V se inicializa la primera vez que se muestra la vista;
  • 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.doctors] recopilará la información necesaria para mostrar la lista de médicos:

  <!-- list of doctors -->
  <div class="alert alert-success"  ng-show="medecins.show">
    {{medecins.title|translate:medecins.model}}
    <ul>
      <li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
    </ul>
</div>

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. Se utiliza simplemente para ilustrar el uso de la variante de traducción utilizada en la línea 3. Aún sin definir, el atributo [medecins.data] contendrá la lista de médicos (línea 5).

  • Línea 10: [$scope.errors] recopilará la información necesaria para mostrar la lista de errores:

  <!-- the error list -->
  <div class="alert alert-danger"  ng-show="errors.show">
    {{errors.title|translate:errors.model}}
    <ul>
      <li ng-repeat="message in errors.messages">{{message|translate}}</li>
    </ul>
</div>

El atributo [errors.title] será el título del banner. Se define en el servicio [config]. El atributo [errors.show] controla 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. Se utiliza simplemente para ilustrar el uso de la variante de traducción utilizada en la línea 3. Aún sin definir, el atributo [errors.messages] contendrá la lista de mensajes de error que se mostrarán (línea 5).

  • Línea 16: la tarea asíncrona. El controlador lanzará sucesivamente dos tareas asíncronas. Las referencias a estas tareas sucesivas se colocarán en la variable [task]. Esto permitirá cancelarlas (línea 55);
  • Línea 19: El método que se ejecuta cuando el usuario hace clic en el botón [Lista de médicos]:

    <button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
  • Líneas 21-23: Se actualiza la interfaz de usuario: se muestra el mensaje de carga y se oculta todo lo demás;
  • línea 25: se crea la tarea de espera asíncrona. Se recibirá una señal (tarea completada) una vez transcurrido el tiempo introducido por el usuario en el formulario;
  • línea 26: recuperamos la promesa de la tarea asíncrona. El programa que inicia la tarea trabaja con esta promesa. Sin embargo, debemos tener la referencia a la propia tarea para poder cancelarla (línea 55);
  • líneas 28-32: definimos el trabajo que se debe realizar una vez finalizada la espera;
  • línea 30: utilizamos el método [dao.getData] para iniciar una nueva tarea asíncrona. Le pasamos la información que necesita:
    • la URL raíz del servicio web [$scope.server.url], por ejemplo [http://localhost:8080];
    • el nombre de usuario [$scope.server.login] para la autenticación, por ejemplo [admin];
    • la contraseña [$scope.server.password] para la autenticación, por ejemplo [admin];
    • la URL que devuelve el servicio solicitado [config.urlSvrMedecins], aquí [/getAllMedecins]. En total, la URL completa será [http://localhost:8080/getAllMedecins];

El método [dao.getData] devuelve un resultado que puede adoptar dos formas:

  • (continuación)
    • {err: 0, data: [med1, med2, ...]} donde [medi] es un objeto que representa a un médico (título, nombre, apellidos),
    • {err: n, messages: [msg1, msg2, ...]} donde [msg] es un mensaje de error y n no es igual a 0;
  • línea 31: devolvemos 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];
    • En definitiva, promise = promise.then(...; return task.promise) es una cadena de dos promesas [promise2.promise1]. [promise1] solo se evaluará una vez que se haya resuelto la promesa [promise2], es decir, cuando la tarea [dao.getData] haya finalizado. La promesa [promise1] no depende de ninguna tarea asíncrona. Por lo tanto, se resolverá inmediatamente;
  • líneas 34–50: De la explicación anterior se deduce que estas líneas solo se ejecutarán una vez que la tarea [dao.getData] haya finalizado. El parámetro [result] pasado a la función en la línea 34 es construido por el método [dao.getData] y pasado al código de llamada mediante la operación [task.resolve(result)], donde [result] tiene la siguiente forma:
    • {err: 0, data: [med1, med2, ...]} donde [medi] es un objeto que representa a un médico (título, nombre, apellidos),
    • {err: n, messages: [msg1, msg2, ...]} donde [msg1] es un mensaje de error y n no es igual a 0;
  • línea 37: comprobamos el código de error [result.err];
  • líneas 38–42: si no hay ningún error (result.err == 0), recuperamos la lista de médicos y la mostramos;
  • líneas 44–47: si, por el contrario, hay un error (result.err != 0), recuperamos la lista de mensajes de error y la mostramos;
  • líneas 53–56: el mensaje de carga con su botón de cancelar permanece visible hasta que ambas operaciones asíncronas se hayan completado. Veamos qué ocurre dependiendo de cuándo se produzca la cancelación:
    • En primer lugar, es importante comprender que las líneas 19-50 se ejecutan todas a la vez. En este momento solo se ha iniciado una tarea asíncrona, la de la línea 25.
    • Tras esta ejecución inicial, se actualiza la vista V y, por lo tanto, el banner de espera y su botón de cancelación quedan visibles. Si el usuario cancela la espera antes de que finalice la tarea de la línea 25, se ejecuta el método de la línea 53 y la tarea se cancela con un error (línea 55);
    • Líneas 56-59: Se actualiza la interfaz: se vuelve a mostrar el formulario y se oculta todo lo demás,
    • A continuación, vuelve a la vista V y el navegador procesa el siguiente evento. Dado que la tarea se ha completado, la promesa de esta tarea se resuelve, lo que desencadena un evento. A continuación, se procesa;
    • y se ejecutan las líneas 28-32. No hay ninguna función definida para el caso de fallo, por lo que no se ejecuta ningún código. Se obtiene una nueva promesa, la que siempre devuelve [promise.then] y que siempre se resuelve;
    • una vez gestionado el evento, el control vuelve a la vista V y el navegador procede a gestionar el siguiente evento. Dado que la [promise] de la línea 28 se ha resuelto, la de la línea 34 se resolverá, lo que desencadenará un nuevo evento. A continuación, se gestiona;
    • las líneas 34-49 se ejecutarán entonces por turno, ya que la promesa utilizada en la línea 34 se ha cumplido. De nuevo, como no hay ninguna función definida para el caso de fallo, no se ejecuta ningún código,
    • y así llegamos a la línea 50. Ya no hay ninguna tarea en espera y se muestra la nueva vista V;
    • Ahora supongamos que se produce una cancelación mientras se está ejecutando la segunda tarea asíncrona [dao.getData]. Se aplica de nuevo el razonamiento anterior. El final de la tarea desencadenará la ejecución de las líneas 34–50 con un fallo de la tarea. Pronto descubriremos que el método [dao.getData] realiza una llamada HTTP asíncrona al servicio web. Esta llamada no se cancelará, pero su resultado no se utilizará.

Es importante comprender este constante vaivén entre la representación de la vista V y el manejo de los eventos del navegador. Los eventos son desencadenados por el usuario (un clic) o por operaciones del sistema, como la finalización de una operación asíncrona. El estado de inactividad del navegador es la representación de la vista V. Sale de este estado de inactividad cuando se produce un evento, que luego procesa. Tan pronto como se ha procesado el evento, vuelve a su estado de inactividad. A continuación, la vista V se actualiza si el evento procesado ha modificado su modelo M. El navegador sale de su estado de inactividad con el siguiente evento.

Todo ocurre en un único subproceso. Nunca se procesan dos eventos simultáneamente. Su ejecución es secuencial. El navegador pasa al siguiente evento solo cuando el anterior libera el control, normalmente porque se ha procesado por completo.

Hay un punto más que explicar. Para mostrar 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:


// error analysis in server response JSON
    function getErrors(data) {
      // data {err:n, messages:[]}, err!=0
      // errors
      var errors = [];
      // error code
      var err = data.err;
      switch (err) {
        case 2 :
          // not authorized
          errors.push('not_authorized');
          break;
        case 3 :
          // forbidden
          errors.push('forbidden');
          break;
        case 4 :
          // local error
          errors.push('not_http_error');
          break;
        case 6 :
          // document not found
          errors.push('not_found');
          break;
        default :
          // other cases
          errors = data.messages;
          break;
 
      }
      // if no msg, we put one
      if (! errors || errors.length == 0) {
        errors=['error_unknown'];
      }
      // return the list of errors
      return errors;
    }
  • 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: construiremos una matriz de mensajes de error. Estos mensajes están internacionalizados. Por este motivo, no son los mensajes en sí los que colocamos en la matriz, sino sus claves de internacionalización, excepto en la línea 27. En este caso, utilizamos el atributo [messages] del parámetro [data]. Estos mensajes son mensajes reales y no claves de mensajes. Sin embargo, la vista V los tratará como claves de mensajes, que entonces no se encontrarán. En este caso, el módulo [translate] muestra la clave de mensaje que no ha encontrado; en este caso, un mensaje real. Este es el resultado deseado;
  • líneas 32-34: gestionan el caso en el que [data.messages] en la línea 27 es nulo. Esto ocurre con el servicio web escrito. Este escenario debería haberse evitado.

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) {
 
      // logs
      utils.debug("[dao] init");
 
      // ----------------------------------méthodes privées
      // obtain data from the web service
      function getData(serverUrl, username, password, urlAction, info) {
        // asynchronous operation
        var task = $q.defer();
        // url request HTTP
        var url = serverUrl + urlAction;
        // basic authentication
        var basic = "Basic " + $base64.encode(username + ":" + password);
        // the answer
        var réponse;
        // all http requests must be authenticated
        var headers = $http.defaults.headers.common;
        headers.Authorization = basic;
        // query HTTP
        var promise;
        if (info) {
          promise = $http.post(url, info, {timeout: config.timeout});
        } else {
          promise = $http.get(url, {timeout: config.timeout});
        }
        promise.then(success, failure);
        // the task itself is returned so that it can be cancelled
        return task;
 
        // success
        function success(response) {
          // response.data={status:0, data:[med1, med2, ...]} or {status:x, data:[msg1, msg2, ...]
          utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
          // answer
          var payLoad = response.data;
          réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
          // we return the answer
          task.resolve(réponse);
        }
 
        // failure
        function failure(response) {
          utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
          // status analysis
          var status = response.status;
          var error;
          switch (status) {
            case 401 :
              // unauthorized
              error = 2;
              break;
            case 403:
              // forbidden
              error = 3;
              break;
            case 404:
              // not found
              error = 6;
              break;
            case 0:
              // local error
              error = 4;
              break;
            default:
              // something else
              error = 5;
          }
          // we return the answer
          task.resolve({err: error, messages: [response.statusText]});
        }
      }
 
      // --------------------- service instance [dao]
      return {
        getData: getData
      }
}]);
  • líneas 77-79: el servicio solo tiene un campo: el método [getData], que recupera información del servicio web / JSON;
  • línea 2: aparece una dependencia [$http] que aún no hemos visto. Se trata de un servicio Angular predefinido 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] acepta cinco parámetros:
    • [serverUrl]: la URL raíz del servicio web (http://localhost:8080);
    • [urlAction]: la URL del servicio específico que se solicita (/getAllMedecins);
    • [username]: el nombre de usuario;
    • [password]: la contraseña del usuario;
    • [info]: un objeto que contiene información adicional cuando se accede a la URL del servicio específico solicitado mediante una operación POST. En el caso de la URL (/getAllMedecins), este parámetro no se pasó. Por lo tanto, es [undefined];
  • línea 12: se crea una tarea asíncrona;
  • línea 14: la URL completa del servicio solicitado (http://localhost:8080/getAllMedecins);
  • línea 16: se realiza la autenticación enviando el siguiente encabezado HTTP:
Authorization:Basic code

donde [código] es la cadena codificada en Base64 [nombre de usuario:contraseña];

La línea 16 construye la parte [Código básico] del encabezado HTTP;

  • línea 18: la respuesta del servicio web;
  • línea 20: los encabezados HTTP enviados por defecto por Angular en una solicitud HTTP se definen en el objeto [$http.defaults.headers.common]. El encabezado [Authorization:Basic code] no está incluido;
  • línea 21: lo añadimos a los encabezados HTTP para que se envíe sistemáticamente. A la izquierda de la asignación, tenemos el encabezado [Authorization] que hay que inicializar, y a la derecha, el valor del encabezado; en este caso, el valor definido en la línea 16. Así que si escribimos:
headers.Authorization = 'x';

Angular enviará el encabezado HTTP:

Authorization : x
  • línea 23: los métodos del servicio [$http] devuelven promesas. 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 la URL (http://localhost:8080/getAllMedecins) mediante una solicitud GET. Para evitar una espera demasiado larga, establecemos un tiempo de espera máximo para recibir la respuesta del servidor. Por defecto, este tiempo de espera es de un segundo;
  • Línea 29: definimos los dos métodos que se ejecutarán cuando se cumpla la promesa:
    • [success]: definido en la línea 34, es el método que se ejecuta cuando la promesa se resuelve tras completar con éxito la tarea;
    • [failure]: definido en la línea 45, es el método que se ejecuta cuando la promesa se resuelve debido a un fallo en la tarea;
    • Ambos métodos (deberíamos decir funciones) se definen dentro de la función [getData]. Esto es posible en JavaScript. Se puede acceder a las variables definidas en [getData] desde las dos funciones internas [success] y [failure];
  • línea 31: devolvemos la tarea creada en la línea 12. Aquí, debemos recordar el código de llamada:

        promise = promise.then(function () {
          // we ask for the list of doctors;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
});

La línea 3 anterior recupera una tarea.

  • Línea 34: La función [success] se ejecuta más tarde, una vez que la solicitud HTTP se completa con éxito. Este concepto de éxito está vinculado a la primera línea de una respuesta HTTP. Tiene la forma:
HTTP/1.1 code texte

El código es un número de tres dígitos que indica si la solicitud se ha realizado con éxito o no. En términos generales, los códigos 2xx y 3xx son códigos de éxito, mientras que los demás son códigos de error. El texto es un breve mensaje explicativo. A continuación se muestran dos posibles respuestas, una para el éxito y otra para el error:

HTTP/1.1 200 OK
HTTP/1.1 404 Not Found
  • Línea 36: La respuesta del servidor se muestra en la consola. En el error [404 No encontrado], obtenemos algo como:

[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}

En esta respuesta, solo utilizaremos los campos [data], [status] y [statusText].

  • Línea 38: Recuperamos el campo [data] de la respuesta. Adoptará una de las siguientes formas:
    • {status: 0, data: [med1, med2, ...]} donde [medi] es un objeto que representa a un médico (título, nombre, apellidos),
    • {status: n, data: [msg1, msg2, ...]} donde [msg1] es un mensaje de error y n no es igual a 0;

Image

  • Línea 39: Construimos la respuesta {0,data} o {n,messages}. La primera respuesta contiene los médicos en el campo [data]. La segunda indica un error que se produjo en el lado del servidor. El servidor gestionó este error, generó un código de error en [err] y una lista de mensajes de error en [data]. En ambos casos, devuelve un código de estado HTTP 200 que indica que la solicitud HTTP se ha procesado por completo. Por eso ambos casos se gestionan en la misma función [success];
  • línea 41: la tarea se completa [task.resolve] y se devuelve una de las dos respuestas:
    • {err: 0, data: [med1, med2, ...]} donde [medi] es un objeto que representa a un médico (título, nombre, apellidos),
    • {err: n, messages: [msg1, msg2, ...]} donde [msgi] es un mensaje de error y n no es igual a 0;

Este código debe estar vinculado a cómo se recupera esta respuesta en el código de llamada del controlador:


        // analyze the result of the previous call
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          ...
          }

La respuesta de [task.resolve(response)] se almacena en la variable [result] anterior.

  • línea 45: la función [failure] cuando la tarea asíncrona termina en un error. Hay dos casos posibles:
    • el servidor señala este fallo devolviendo un código de estado que no es ni 2xx ni 3xx,
    • Angular cancela la solicitud HTTP. En este caso, no se realiza ninguna solicitud. Se produce una excepción de Angular, pero el servidor no devuelve ningún código de error HTTP. Esto ocurre, por ejemplo, si se proporciona una URL no válida a la que no se puede acceder;
  • línea 46: mostramos la respuesta en la consola;
  • línea 48: recordamos que la respuesta del servidor tiene el siguiente formato:

{"data":"...","status":404,"config":{...},"statusText":"Not Found"}

Línea 48: recuperamos el atributo [status] anterior;

  • Líneas 50–70: Basándonos en el código de error HTTP, generamos un nuevo código de error para ocultar la naturaleza HTTP del método [dao.getData] al código que lo invoca. Podemos comprobar que, en el controlador que utiliza este método, nada sugiere que haya una llamada HTTP dentro del método;
    • línea 51: el error [401] corresponde a una autenticación fallida (contraseña incorrecta, por ejemplo),
    • línea 55: el error [403] corresponde a una solicitud no autorizada. El usuario se ha autenticado correctamente, pero no tiene permisos suficientes para solicitar la URL que ha solicitado. Esto ocurrirá con el usuario [user / user]. Este usuario existe en la base de datos, pero no tiene permiso para utilizar la aplicación. Solo el usuario [admin / admin] tiene este permiso;
    • Línea 59: El error [404] indica que no se ha encontrado la URL. El error puede tener varias causas:
      • el usuario ha cometido un error tipográfico en la URL del servicio;
      • el servicio web no se ha iniciado;
      • el servicio web no respondió con la suficiente rapidez (tiempo de espera predeterminado de un segundo);
    • línea 63: el código de error HTTP 0 no existe. Esto ocurre cuando Angular no ha realizado la llamada HTTP solicitada porque la URL introducida por el usuario no es válida y no se puede acceder a ella. Más adelante nos encontraremos con otros casos en los que Angular no ejecuta la llamada HTTP solicitada;
  • línea 72: completamos con éxito la tarea (task.resolve) devolviendo una respuesta de tipo {err, messages}, donde la matriz [messages] consiste únicamente en el mensaje [response.statusText]. Si Angular no realizó la llamada HTTP solicitada, tendremos una cadena vacía;

Ahora que tenemos una visión tanto general como detallada de la aplicación, podemos comenzar con las pruebas.

3.7.6.5. Pruebas de la aplicación - 1

Empecemos con entradas válidas:

Image

  • en [1], introducimos 0 para evitar cualquier retraso;
  • en [2], aparece un mensaje de error aunque las entradas sean correctas. Aún no hemos tratado 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 detectado un problema que le ha impedido realizar una solicitud HTTP. En estos casos, debes consultar los registros de la consola de JavaScript. Hay dos formas de hacerlo:
    • pulsar [F12] en el navegador Chrome;
    • utilizar la consola de WebStorm;

En la consola de WebStorm, encontramos varios mensajes, incluido este:

XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.
[dao] getData[/getAllMedecins] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}
  • Línea 1: Angular informa de un error, al que volveremos más adelante;
  • línea 2: el registro del método [dao.getData]. Aquí hay algunos detalles interesantes:
    • [status] es 0, lo que indica que no se realizó ninguna solicitud HTTP. En consecuencia, [statusText] está vacío,
    • [url] es equivalente a [http://localhost:8080/getAllMedecins], lo cual es correcto;
    • el encabezado de autenticación HTTP [Authorization":"Basic YWRtaW46YWRtaW4=] también es correcto;

Entonces, ¿por qué no funcionó? La frase clave en los registros es [No hay ningún encabezado 'Access-Control-Allow-Origin' presente]. Para entender esto, se necesita una explicación detallada. Empecemos por repasar la arquitectura general de la aplicación cliente/servidor:

Image

  • 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 bloquea el navegador que ejecuta la aplicación Angular porque supone una vulnerabilidad de seguridad. La aplicación solo puede consultar al servidor del que proviene, es decir, el servidor [1];

De hecho, no es exacto decir que el navegador impide que la aplicación Angular consulte al servidor [2]. En realidad, lo consulta para preguntarle si permite que un cliente que no proviene de su propio dominio lo consulte. Esta técnica de intercambio se denomina CORS (Cross-Origin Resource Sharing). El servidor [2] concede el permiso enviando encabezados HTTP específicos. Es precisamente porque nuestro servidor [2] no los envió aquí por lo que el navegador se negó a realizar la solicitud HTTP solicitada por la aplicación.

Ahora entremos en detalles. Examinemos el tráfico de red que se produjo durante la solicitud HTTP. Para ello, en el navegador Chrome, pulsamos [F12] para abrir las herramientas de desarrollador y seleccionamos la pestaña [Red] para ver el tráfico de red:

  • En [1], seleccionamos la pestaña [Red];
  • en [2], solicitamos la lista de médicos;

En la pestaña [Red] obtenemos la siguiente información:

  • en [1], la información enviada al servidor;
  • en [2], la respuesta del servidor;

Podemos ver en [1] que el navegador envió una solicitud HTTP [OPTIONS] a la URL solicitada. [OPTIONS] es uno de los métodos HTTP, 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 método. El servidor responde en [2]. Para indicar que acepta solicitudes de clientes fuera de su dominio, debe devolver un encabezado específico llamado [Access-Control-Allow-Origin]. Y como no devolvió este encabezado, Angular no ejecutó la llamada HTTP solicitada y devolvió el error:


XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.

Por lo tanto, debemos modificar nuestro servidor para que envíe el encabezado HTTP esperado.

3.7.6.6. Modificación del servidor Web/JSON

Volvemos a Eclipse. Para conservar nuestro progreso, duplicamos la versión actual del servidor web/JSON [rdvmedecins-webapi-v2] en [rdvmedecins-webapi-v3] [1]:

Realizamos una modificación inicial en [ApplicationModel], que es uno de los elementos de configuración del servicio web:


package rdvmedecins.web.models;
 
...
 
@Component
public class ApplicationModel implements IMetier {
 
    // the [business] layer
    @Autowired
    private IMetier métier;
 
    // data from the [business] layer
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    // configuration data
    private boolean CORSneeded = true;
 
...
 
    public boolean isCORSneeded() {
        return CORSneeded;
    }
 
}
  • línea 17: creamos una variable booleana que indica si se aceptan o no clientes ajenos al dominio del servidor;
  • líneas 21-23: el método para acceder a esta información;

A continuación, creamos un nuevo controlador Spring MVC [3]:

La clase [RdvMedecinsCorsController] es la siguiente:


package rdvmedecins.web.controllers;
 
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
import rdvmedecins.web.models.ApplicationModel;
 
@Controller
public class RdvMedecinsCorsController {
 
    @Autowired
    private ApplicationModel application;
 
    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
        }
 
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • Líneas 28–31: definen un controlador para la URL [/getAllMedecins] cuando se solicita con el método HTTP [OPTIONS];
  • línea 29: el método [getAllMedecins] toma el objeto [HttpServletResponse] como parámetro, que se enviará al cliente que realizó la solicitud. Este objeto es inyectado por Spring;
  • línea 30: el manejo de la solicitud se delega 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 desde fuera de su dominio, 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 seguir probando. Lanzamos la nueva versión del servicio web y vemos que el problema sigue sin resolverse. No ha cambiado nada. Si añadimos una salida de consola en la línea 30 anterior, nunca se muestra, lo que indica que el método [getAllMedecins] de la línea 29 nunca se llama.

Tras investigar un poco, descubrimos que Spring MVC gestiona las solicitudes HTTP [OPTIONS] por sí mismo 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 {
 
    // dispatcherservlet configuration for CORS headers
    @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] se utiliza para definir el servlet que gestiona las solicitudes de los clientes. Es de tipo [DispatcherServlet]. Este servlet se crea normalmente de forma predeterminada. Si lo creamos nosotros mismos, podemos configurarlo;
  • línea 14: creamos una instancia de tipo [DispatcherServlet];
  • línea 15: indicamos al servlet que reenvíe los comandos HTTP [OPTIONS] a la aplicación;
  • línea 16: devolvemos el servlet configurado de esta manera;

Todavía tenemos que 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 recuperar la lista de médicos utilizando nuestro cliente Angular. Examinamos el tráfico de red en la pestaña [Red]:

  • En [1], podemos ver que el encabezado HTTP [Access-Control-Allow-Origin: *] ahora está presente en la respuesta del servidor. Y, sin embargo, sigue sin funcionar. Examinamos los registros de la consola en [2]. Allí encontramos el siguiente registro:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Request header field Authorization is not allowed by Access-Control-Allow-Headers

Podemos ver que el navegador espera un nuevo encabezado HTTP [Access-Control-Allow-Headers] que le indique que tenemos permiso para enviar el encabezado de autenticación:

Authorization:Basic code

Esto podría ser una buena señal. Es posible que Angular haya intentado enviar la solicitud HTTP GET. Sin embargo, dado que esta solicitud incluye un encabezado de autenticación, está comprobando si el servidor lo acepta.

Modificamos nuestro servidor web / JSON para enviar este encabezado. La clase [RdvMedecinsCorsController] cambia de la siguiente manera:


    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // we authorize the header [Authorization]
            response.addHeader("Access-Control-Allow-Headers", "Authorization");            
}
  • Las líneas 6 y 7 añaden el encabezado que faltaba.

Reiniciamos el servidor y volvemos a solicitar la lista de médicos utilizando el cliente Angular:

 

Esta vez ha funcionado. 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"}

Podemos ver 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]: (que no debe confundirse con el código de error HTTP [status]). Aquí, [status=0] indica que la URL [/getAllMedecins] se procesó sin errores;
    • [data]: que contiene la lista JSON de médicos;

Veamos ahora algunos otros casos interesantes:

Introducimos credenciales incorrectas [login, password]:

Iniciamos sesión como [usuario / usuario], que no tiene acceso a la aplicación (solo [admin] tiene acceso):

Esta vez, el error ya no es [Error de autenticación], sino [Acceso denegado].

3.7.7. Ejemplo 7: Lista de clientes

Utilizaremos la aplicación anterior para mostrar la lista de clientes en un menú desplegable del tipo [Bootstrap select] (véase la sección 3.6.6).

3.7.7.1. Vista V

La vista inicial será la siguiente:

 

Para obtener la vista V, copiamos el código de [app-16.html] en [app-17.html] y lo modificamos de la siguiente manera:


<div class="container" >
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible" >
...
  </div>
 
  <!-- the request -->
  <div class="alert alert-info" ng-hide="waiting.visible" >
...
    <button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
  </div>
 
  <!-- customer list -->
  <div class="row" style="margin-top: 20px" ng-show="clients.show">
    <div class="col-md-3">
      <h2 translate="{{clients.title}}"></h2>
      <select data-style="btn-primary" class="selectpicker">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
      </select>
    </div>
  </div>
 
  <!-- the error list -->
  <div class="alert alert-danger"  ng-show="errors.show">
   ...
  </div>
 
</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
  • líneas 5-7: el banner de carga no cambia;
  • líneas 10-13: el formulario no cambia, salvo la etiqueta del botón (línea 12);
  • líneas 28-30: el banner de error no cambia;
  • líneas 16-25: los clientes se muestran en una lista desplegable con el estilo del componente [Bootstrap-selectpicker] (atributos data-style y class, línea 19);
  • línea 20: se utiliza la directiva [ng-repeat] para generar las distintas opciones de la lista desplegable. Tenga en cuenta que la etiqueta 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 ID del cliente mostrado;
  • 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 copia del archivo [rdvmedecins-04]:

Image

Casi nada ha cambiado, excepto en el controlador, que ahora se ha adaptado para proporcionar la lista de clientes:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
    function ($scope, utils, config, dao, $translate) {
      // ------------------- model initialization
      // model
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      $scope.server = {url: undefined, login: undefined, password: undefined};
      $scope.clients = {title: config.listClients, show: false, model: {}};
      $scope.errors = {show: false, model: {}};
      $scope.urlServerLabel = config.urlServerLabel;
      $scope.loginLabel = config.loginLabel;
      $scope.passwordLabel = config.passwordLabel;
 
      // asynchronous task
      var task;
 
      // execution action
      $scope.execute = function () {
        // the UI is updated
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        // simulated waiting
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        // waiting
        promise = promise.then(function () {
          // we ask for the customer list;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
          return task.promise;
        });
        // analyze the result of the previous call
        promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // we put the acquired data into the model
            $scope.clients.data = result.data;
            // the UI is updated
            $scope.clients.show = true;
            $scope.waiting.visible = false;
            // style the drop-down list
            $('.selectpicker').selectpicker();
          } else {
            // there were errors in obtaining the customer list
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // the UI is updated
            $scope.waiting.visible = false;
          }
        });
      };
 
      // cancel wait
      function cancel() {
        // complete the task
        task.reject();
        // the UI is updated
        $scope.waiting.visible = false;
        $scope.clients.show = false;
        $scope.errors.show = false;
      }
    }
  ])
;
  • El controlador apenas ha cambiado. Antes proporcionaba una lista de médicos. Ahora proporciona una lista de clientes;
  • línea 9: [$scope.clients] será el modelo para el banner de clientes en la vista V;
  • línea 30: ahora se utiliza la URL [/getAllClients];
  • líneas 35–36: los dos formatos de respuesta devueltos por el método [dao.getData]. Ahora tenemos clientes en lugar de médicos;
  • línea 44: una instrucción bastante poco habitual en el código de Angular. Estamos manipulando directamente el DOM (Document Object Model). Aquí queremos aplicar el método [selectpicker] (parte de [bootstrap-select.min.js]) a los elementos DOM que tienen la clase [selectpicker] [$('.selectpicker)']. Solo hay uno: la lista desplegable:

      <select data-style="btn-primary" class="selectpicker" select-enable="">
....
      </select>

En la sección 3.6.6, vimos que esto aplicaba el siguiente estilo a la lista desplegable:

Al igual que hicimos con los médicos, también tenemos que modificar el servicio web.

3.7.7.3. Modificación del servicio web - 1

  

La clase [RdvMedecinsController] se ha mejorado con un nuevo método:


package rdvmedecins.web.controllers;
 
...
 
@Controller
public class RdvMedecinsCorsController {
 
    @Autowired
    private ApplicationModel application;
 
    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // we authorize the header [Authorization]
            response.addHeader("Access-Control-Allow-Headers", "Authorization");
        }
 
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }
 
    // customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
    public void getAllClients(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • Líneas 29–32: El método [getAllClients] gestionará la solicitud HTTP [OPTIONS] que le envía el navegador;

3.7.7.4. Pruebas de la aplicación – 1

Ya estamos listos para realizar las pruebas. Iniciamos el servidor web y, a continuación, introducimos valores válidos en el formulario de Angular. Obtenemos la siguiente respuesta:

Image

Este mensaje de error se muestra cuando Angular no ha podido realizar la solicitud HTTP solicitada. Debemos entonces buscar las causas en los registros de la consola. Allí encontramos el siguiente mensaje:

XMLHttpRequest cannot load http://localhost:8080/getAllClients. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.

Un problema que creíamos resuelto. Veamos ahora el tráfico de red que se produjo:

Image

Vemos que la operación [getAllClients] que utilizaba el método HTTP [OPTIONS] se completó con éxito, pero la operación [getAllClients] que utilizaba el método HTTP [GET] se canceló. La respuesta a la solicitud [OPTIONS] fue la siguiente:

Image

Los encabezados HTTP de CORS están efectivamente presentes. Veamos ahora los intercambios HTTP durante la solicitud GET:

Image

La solicitud HTTP parece correcta. En concreto, podemos ver el encabezado de autenticación.

Además del mensaje de error anterior, aparece el siguiente mensaje en los registros de la consola:


[dao] getData[/getAllClients] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}

Este es el registro que el método [dao.getData] genera sistemáticamente al recibir la respuesta a su solicitud HTTP. Destacan dos cosas:

  • [status=0]: esto significa que Angular canceló la solicitud HTTP;
  • [method=GET]: y fue la solicitud GET la que se canceló;

Si lo combinamos con el primer mensaje, esto significa que Angular también espera encabezados CORS para la solicitud GET. Sin embargo, actualmente, nuestro servicio web solo los envía para la solicitud HTTP [OPTIONS]. Es muy extraño encontrar este error ahora y no en la lista de médicos. No tengo ninguna explicación.

Por lo tanto, tenemos que modificar el servicio web de nuevo.

3.7.7.5. Modificación del servicio web – 2

  

Los métodos [GET] y [POST] se gestionan en la clase [RdvMedecinsController]. Tenemos que 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;
 
...
 
    // customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients(HttpServletResponse response) {
        // headers CORS
        rdvMedecinsCorsController.getAllClients(response);
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // customer list
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
    }
...
  • línea 8: queremos reutilizar el código que colocamos en el controlador [RdvMedecinsCorsController]. Así que lo inyectamos aquí;
  • línea 14: el método que gestiona la solicitud [GET /getAllClients]. Realizamos dos cambios:
    • 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 configurar los encabezados CORS en este objeto;

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], obtenemos una respuesta, pero está vacía [2];
  • en [3]: los intercambios de red se han realizado sin problemas;

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"} 

Así pues, el método sí que recibió la lista de clientes. Una vez verificado el código, empezamos a sospechar de la siguiente instrucción, que no entendemos del todo:


// on style la liste déroulante
$('.selectpicker').selectpicker();

Comentamos la línea 2 y lo intentamos de nuevo. A continuación, obtenemos la siguiente respuesta:

Así pues, hemos identificado el problema. Es la aplicación del método [selectpicker] a la lista desplegable lo que está causando el problema. Cuando miramos el código fuente de la página con el error, vemos lo siguiente:

  • descubrimos que en [1], la lista desplegable sí está presente con sus elementos, pero no se muestra [style='display:none'];
  • En [2], se muestra el botón [bootstrap select]. Los elementos de la lista desplegable deberían aparecer en la lista <ul role='menu'>. No están ahí, por lo que tenemos una lista vacía. Parece que cuando se aplicó el método [selectpicker] a la lista desplegable, su contenido estaba vacío en ese momento;

Mientras buscábamos una solución en la web, encontramos esta. Reemplazamos el código:


// on style la liste déroulante
$('.selectpicker').selectpicker();

con lo siguiente:


            // on style la liste déroulante
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

El estilo [bootstrap-select] se aplica mediante una función [$timeout]. Ya hemos visto esta función, que permite que una función se ejecute tras un cierto retraso. En este caso, la ausencia de retraso significa un retraso de cero. Las líneas anteriores colocan un evento en la cola de eventos del navegador. Cuando el evento actual (clic en el botón [Lista de clientes]) termine de procesarse, se mostrará la vista V. Inmediatamente después, el navegador comprobará su lista de eventos. Debido a su retardo cero, el evento [$timeout] estará en la parte superior de la lista y se procesará. A continuación, se aplica el estilo [bootstrap-select] a una lista desplegable rellenada. Veamos el resultado:

Si volvemos a mirar el código fuente de la página mostrada, vemos 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:


            // on style la liste déroulante
            $('.selectpicker').selectpicker();

Estamos manipulando un objeto DOM. Muchos desarrolladores de Angular son reacios a manipular el DOM dentro del código del controlador. Para ellos, esto debería hacerse en una directiva. Una directiva de Angular puede considerarse una extensión del lenguaje HTML. Esto permite 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 de controladores con la que ya estamos familiarizados:

angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)

La directiva pertenece al módulo [rvmedecins]. Es una función que acepta dos parámetros:

  • (continúa)
    • el primero es el nombre de la directiva [selectEnable];
    • el segundo es una matriz ['obj1','obj2',..., function(obj1, obj2,...)] donde los [obj] son los objetos que se van a 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 varios atributos. Aquí, el único atributo es el atributo [link] (línea 3). Su valor aquí es una función que toma tres parámetros:
    • scope: el modelo de la vista en la que se utiliza la directiva;
    • element: el elemento de la vista, el destino de la directiva;
    • attrs: los atributos de este elemento;

Veamos un ejemplo. La directiva [selectEnable] podría utilizarse en el siguiente contexto:

<div select-enable="data"></div>

En el ejemplo anterior, el atributo [select-enable] aplica la directiva [selectEnable] al elemento HTML <div>. Una directiva [doSomething] se puede aplicar a cualquier elemento HTML añadiéndole el atributo [do-something]. Fíjate en la diferencia ortográfica entre el nombre de la directiva y su atributo asociado. Pasamos de [camelCase] a [camel-case].

La directiva [selectEnable] también se podría utilizar de la siguiente manera:

<select-enable attr1='val1' attr2='val2' ...>...</select-enable>

Aquí, la directiva [doSomething] se aplica en forma de etiqueta HTML <do-something>.

Volvamos a la sintaxis

<div select-enable="data"></div>

y a los tres parámetros de la función [link] de la directiva, [scope, element, attrs]:

  • scope: es el modelo de la vista en la que se encuentra el <div>;
  • element: es el propio <div>;
  • attrs: es la matriz de atributos del <div>. Estos se pueden utilizar para pasar información a la directiva. En el ejemplo anterior, escribimos attrs['selectEnable'] para recuperar la información [data]. Obsérvese el cambio en la notación [selectEnable] para referirse al 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í vemos el código que colocamos anteriormente en el controlador. Este código se ejecuta cuando se encuentra la directiva [select-enable] (como elemento o atributo) durante la representación de la vista V.

Para implementar esta directiva, copiamos el archivo [app-17.html] en [app-17B.html] y lo modificamos de la siguiente manera:


      <select data-style="btn-primary" class="selectpicker" select-enable="">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
</select>
  • línea 1: aplicamos la directiva [selectEnable] al elemento HTML [select]. Como no hay información que pasar a la directiva, simplemente escribimos [select-enable=""] ;

También modificamos el controlador duplicando el archivo JS [rdvmedecins-05.js] en [rdvmedecins-05B.js] y hacemos referencia al nuevo archivo JS en el archivo [app-17B.html] y en el archivo de la directiva [selectEnable.js]. No olvides este último punto. Si falta el archivo de la directiva, el atributo [select-enable=""] no se gestionará, pero Angular no informará de 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 las siguientes líneas del controlador:


            // on style la liste déroulante
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

Esta operación ahora la gestiona 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], obtenemos una lista vacía.

Los registros de la consola muestran lo siguiente:

1
2
3
[dao] init
directive selectEnable
[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"}
  • línea 1: inicialización del servicio [dao];
  • línea 2: al mostrarse 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 [Lista de clientes]. Podemos ver que la directiva [selectEnable] no se ejecuta por segunda vez. En definitiva, se ejecutó cuando la lista de clientes estaba vacía, por lo que tenemos una lista desplegable vacía;

En otras palabras, es la operación:


$('.selectpicker').selectpicker();

no se ejecutó en el momento adecuado. Podemos intentar resolver el problema de varias maneras. Tras numerosas pruebas infructuosas, nos damos cuenta de que la operación anterior debe ejecutarse solo una vez y únicamente cuando la lista desplegable se haya rellenado. 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>

Línea 1: La etiqueta <select> solo se genera si existe [clients.data]. Este no es el caso cuando se muestra inicialmente 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 [Lista de clientes], [clients.data] tendrá un nuevo valor en el modelo M. Dado que el modelo M ha cambiado, la etiqueta <select> se volverá a evaluar y se generará aquí. Por lo tanto, la directiva [selectEnable] también se evaluará. Cuando se evalúa, las líneas 2-4 de la etiqueta <select> aún no se han evaluado. Por lo tanto, tenemos una lista vacía de clientes. 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 entonces veremos una lista desplegable vacía en la pantalla. Por lo tanto, debemos escribir:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable");
      $timeout(function () {
        $('.selectpicker').selectpicker();
      })
    }
  }
}]);

para obtener el resultado esperado. Debido al [$timeout] de la línea 5, la línea 6 solo se ejecutará una vez que la vista V se haya renderizado por completo, es decir, en el momento en que la etiqueta <select> tenga todos sus elementos.

3.7.8. Ejemplo 8: Horario de un médico

A continuación, 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], solicitamos la agenda de la Sra. PELISSIER [2] para el 25 de junio de 2014 [3];

Se obtiene el siguiente resultado [4]:

Examinaremos las dos perspectivas por separado.

3.7.8.2. El formulario

Duplicamos el archivo [app-17.html] como [app-18.html] y, a continuación, modificamos el código de la siguiente manera:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>
 
  <!-- the request -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="row" style="margin-bottom: 20px">
      <div class="col-md-3">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" class="selectpicker">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
      <div class="col-md-3">
        <h2 translate="{{calendar.title}}"></h2>
        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="calendar.jour" min-date="calendar.minDate" show-weeks="true"
                      class="well well-sm"></datepicker>
        </div>
      </div>
    </div>
    <button class="btn btn-primary" ng-click="execute()">{{agenda.title|translate}}</button>
  </div>
 
  <!-- the error list -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>
 
  <!-- the diary -->
  <div id="agenda" ng-show="agenda.show">
...
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
  • líneas 5-7: el mensaje de carga no cambia;
  • líneas 12-19: la lista de médicos utilizando el componente [bootstrap select];
  • líneas 20-26: el calendario [ui-bootstrap] que ya hemos presentado. Ten en cuenta que el día seleccionado se coloca en el modelo [calendar.day] (atributo ng-model);
  • línea 28: el botón que solicita el calendario;
  • líneas 32-34: la lista de errores permanece sin cambios;
  • 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] copiando el archivo [rdvmedecins-05.js];

3.7.8.3. El controlador C

El código JS de la aplicación queda así:

Image

Solo el servicio [utils] y el controlador [rdvMedecinsCtrl] se verán afectados por los cambios.

El controlador [rdvMedecinsCtrl] queda así:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
       // ------------------- model initialization
      // model
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      $scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
      $scope.errors = {show: false, model: {}};
      $scope.medecins = {
        data: [
          {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
          {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
          {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
          {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
        ],
        title: config.listMedecins};
      $scope.agenda = {title: config.getAgendaTitle, data: undefined, show: false};
      $scope.calendar = {title: config.getCalendarTitle, minDate: new Date(), jour: new Date()};
      // style the drop-down list
      $timeout(function () {
        $('.selectpicker').selectpicker();
      });
      // for the French local calendar
      angular.copy(config.locales['fr'], $locale);
 ...
    }
  ])
;
  • línea 7: establecemos un tiempo de espera de 3 segundos antes de realizar la solicitud HTTP;
  • línea 8: los elementos necesarios para la conexión HTTP están codificados de forma fija;
  • líneas 10–17: la lista de médicos está codificada de forma fija;
  • línea 18: el modelo [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. Establecemos la fecha mínima [minDate] en hoy y la fecha actual también en hoy;
  • líneas 21-23: la lista desplegable se diseña utilizando el método visto anteriormente;
  • línea 25: establecemos la configuración regional de la aplicación en «fr». Por defecto, es «en»;

El método que se ejecuta cuando se solicita el calendario es el siguiente:


// exécution action
      $scope.execute = function () {
        // les infos du formulaire
        var idMedecin = $('.selectpicker').selectpicker('val');
 
        // vérification
        utils.debug("[homeCtrl] idMedecin", idMedecin);
        utils.debug("[homeCtrl] jour", $scope.calendar.jour);
 
        // on met le jour au format yyyy-MM-dd
        var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
        // mise à jour de la vue
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        $scope.agenda.show = false;
...
      };
  • Línea 4: Recuperamos el atributo [value] del médico seleccionado. Aquí volvemos a utilizar el método [selectpicker] del archivo [bootstrap-select.min.js]. Recuerda 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}}

Por lo tanto, el valor (atributo value) de la opción es el [id] del médico.

  • línea 11: formateamos la fecha seleccionada por el usuario como [aaaa-mm-dd], que es el formato de fecha que espera el servidor web;
  • líneas 13-15: cuando finalice el método [execute], se mostrará el banner de carga y se ocultará todo lo demás;

El código continúa de la siguiente manera:


// simulated waiting
        var task = utils.waitForSomeTime($scope.waiting.time);
        // we ask for the doctor's diary
        var promise = task.promise.then(function () {
          // the URL service path
          var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
          // we ask for the agenda
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
          // we return the promise of task completion
          return task.promise;
        });
        // we analyze the result of the call to service [dao]
        promise.then(function (result) {
          // end of wait
          $scope.waiting.visible = false;
          // mistake?
          if (result.err == 0) {
            // we prepare the agenda model
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            // timetable display formatting
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
            // we create an evt to style the table after the view is displayed
            $timeout(function () {
              $("#creneaux").footable();
            });
          } else {
            // mistakes were made in obtaining the agenda
            $scope.errors = {
              title: config.getAgendaErrors,
              messages: utils.getErrors(result),
              show: true
            };
}
  • línea 2: la tarea asíncrona que espera 3 segundos;
  • líneas 5-10: el código que se ejecutará cuando finalice esta espera;
  • línea 6: se construye la URL solicitada [/getAgendaMedecinJour/1/2014-06-25];
  • línea 8: se consulta la URL. Se inicia una tarea asíncrona;
  • Línea 10: hacemos que esta tarea sea asíncrona;
  • líneas 14-38: el código que se ejecutará una vez que la solicitud HTTP haya devuelto su respuesta;
  • línea 13: [result] es la respuesta enviada por el método [dao.getData]. Aquí, debemos 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, a su vez, contiene el atributo [creneauxMedecin] [2] mencionado anteriormente. Se trata de una matriz de franjas horarias, cada una de las cuales contiene dos datos:

  • [rv]: la representación JSON de una cita, o [null] si no se ha programado ninguna cita para ese intervalo;
  • [hDeb, mDeb, hFin, mFin]: la información horaria de la franja;

Volvamos al código del controlador:

  • línea 15: la espera ha terminado;
  • línea 19: rellenamos el modelo [$scope.agenda], que controla la visualización del calendario;
  • línea 20: el calendario se hace visible;
  • líneas 22-24: iteramos por cada elemento C de la matriz [creneauxMedecin] que acabamos de comentar;
  • línea 23: cada elemento C tiene un atributo [slot] que representa la franja horaria. Esto se complementa con un atributo [text] que será la representación textual de la franja horaria en el formato [10:20–10:40];
  • líneas 26–28: hacemos que la tabla HTML utilizada para mostrar los intervalos del calendario sea adaptativa. Ya tratamos este concepto en la sección 3.6.7;
 
  • línea 27: para que la tabla sea adaptativa, debemos 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 adaptativa. Resolvemos este problema de la misma manera utilizando la función [$timeout] (línea 26);
  • líneas 31-34: el caso en el que la solicitud HTTP ha fallado. A continuación, se muestran mensajes de error;

3.7.8.4. Visualización del calendario

Ahora volvemos al código del calendario en el archivo [app-18.html]. Es el siguiente:


<!-- the diary -->
  <div id="agenda" ng-show="agenda.show">
    <!-- case of a doctor without consultation slots -->
    <h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
        translate="agenda_medecinsanscreneaux"></h4>
    <!-- doctor's diary -->
    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table creneaux-table id="creneaux" class="table">
          <thead>
          <tr>
            <th data-toggle="true">
              <span translate="agenda_creneauhoraire"></span>
            </th>
            <th>
              <span translate="agenda_client">Client</span>
            </th>
            <th data-hide="phone">
              <span translate="agenda_action">Action</span>
            </th>
          </tr>
          </thead>
          <tbody>
          <tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
            <td>
            <span
              ng-class="! creneauMedecin.rv ? 'status-metro status-active' : 'status-metro status-suspended'">
              {{creneauMedecin.creneau.text}}
            </span>
            </td>
            <td>
              <span>{{creneauMedecin.rv.client.titre}} {{creneauMedecin.rv.client.prenom}} {{creneauMedecin.rv.client.nom}}</span>
            </td>
            <td>
              <a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active">
              </a>
              <a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended">
              </a>
            </td>
          </tr>
          </tbody>
        </table>
      </div>
    </div>
</div>
  • Líneas 4-5: Recordemos que [agenda.data] es el calendario, y que [agenda.data.creneauxMedecin] es una matriz de objetos de tipo [creneauMedecin]. Cada elemento de este tipo tiene un atributo [creneauMedecin.creneau] que es un intervalo de tiempo. Cada intervalo de tiempo tiene dos elementos que nos interesan:
    • [doctorSlot.slot.appointment], que es la cita (si la hay; cita ≠ nula) programada para el intervalo;
    • [doctorSlot.slot.text], que es el texto [inicio:fin] del intervalo de tiempo;
  • línea 4: muestra un mensaje especial si el médico no tiene franjas horarias. Esto es poco probable, pero resulta que nuestra base de datos está incompleta y este escenario sí se da. La directiva [ng-if] controla si el mensaje se muestra en HTML o no;

Image

La directiva [ng-if] es diferente de las directivas [ng-show, ng-hide]. Estas últimas simplemente ocultan un área presente en el documento. Si [ng-if='false'], entonces el área se elimina del documento. La hemos utilizado aquí con fines ilustrativos;

  • Línea 9: El atributo [id='creneaux'] es importante. Se utiliza en la siguiente 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: iteramos por la matriz [agenda.data.creneauxMedecin];
  • líneas 26–29: se renderiza el texto [3]. La directiva [ng-class] se utiliza para generar el atributo [class] del elemento. Aquí, si [creneauMedecin.rv == null], esto significa que la franja horaria está disponible, y al texto se le asigna un fondo verde. De lo contrario, se le asigna un fondo rojo;
  • línea 32: escribimos el nombre del cliente para el que se ha concertado la cita [4]. Si [rv==null], esta información no existe, pero Angular gestiona este caso correctamente y no genera un error;
  • Líneas 34-39: Mostrar uno de los dos botones: [Reservar] o [Eliminar]. La existencia o no de una cita determina qué botón se selecciona;

3.7.8.5. Modificación del servidor web

Al igual que en los ejemplos anteriores, hay que modificar el servidor web para que la URL [/getAgendaMedecinJour] envíe los encabezados CORS:

  

En la clase [RdvMedecinsCorsController], añade un nuevo método:


    // doctor's diary
    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
    public void getAgendaMedecinJour(HttpServletResponse response) {
        sendOptions(response);
}

Este método enviará los encabezados CORS para la solicitud HTTP [OPTIONS]. Debemos hacer lo mismo para la solicitud HTTP [GET] en la clase [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
        // headers CORS
        rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}

3.7.8.6. Uso de directivas

Al igual que hicimos anteriormente, trasladaremos la manipulación del DOM a las directivas. Tenemos dos manipulaciones del DOM:

  • cuando se muestra la vista por primera vez:

      // on style la liste déroulante
      $timeout(function () {
        $('.selectpicker').selectpicker();
});
  • Cuando se muestra el calendario:

            // we create an evt to style the table after the view is displayed
            $timeout(function () {
              $("#creneaux").footable();
});

En el primer caso, utilizaremos la directiva [selectEnable] ya presentada. En el segundo caso, creamos la directiva [ footable] en el siguiente archivo JS [footable.js]:


angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive footable");
      $timeout(function () {
        $("#creneaux").footable();
      })
    }
  }
}]);

Por lo tanto, utilizamos la misma técnica que para la directiva [selectEnable].

El código HTML [app-18.html] se duplica en [app-18B.html]. A continuación, lo modificamos 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: Aplica la directiva [selectEnable] (mediante el atributo [select-enable]) a la etiqueta <select> para 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: la directiva [footable] (a través del atributo [footable]) se aplica a la tabla HTML del calendario;

<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- directives -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • líneas 3-4: hacen referencia a los archivos JS para 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. Se han eliminado las líneas que manipulan el DOM:


      // on style la liste déroulante
      $timeout(function () {
        $('.selectpicker').selectpicker();
});

            // we create an evt to style the table after the view is displayed
            $timeout(function () {
              $("#creneaux").footable();
});

Una vez hecho esto, al ejecutar la aplicación [app-18B.html] se obtienen los mismos resultados que al ejecutar [app-18.html].

3.7.9. Ejemplo 9: Creación y cancelación de reservas

A continuación, presentamos una aplicación que permite crear y cancelar reservas.

3.7.9.1. Vista V de la aplicación

Presentaremos el siguiente formulario:

  • En [1], puede realizar una reserva. La reserva se realizará para un cliente aleatorio;
  • En [2], puede eliminar las reservas que haya realizado;

Duplicamos el archivo [app-18.html] como [app-19.html] y, a continuación, modificamos el código de la siguiente manera:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>
 
  <!-- the error list -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>
 
  <!-- the diary -->
  <div id="agenda" ng-show="agenda.show">
..
    <!-- doctor's diary -->
    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table id="creneaux" class="table" footable="">
...
          <tbody>
          <tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
...
            <td>
              <a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active"  ng-click="reserver(creneauMedecin.creneau.id)">
              </a>
              <a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended" ng-click="supprimer(creneauMedecin.rv.id)">
              </a>
            </td>
          </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>
....
<script type="text/javascript" src="rdvmedecins-07.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • líneas 5-7: el mensaje de carga es el mismo que en la versión anterior;
  • líneas 10-12: el mensaje de error es el mismo que en la versión anterior;
  • líneas 15-36: el calendario es el mismo que en la versión anterior, con dos excepciones:
    • línea 26: al hacer clic en el botón [reservar] (atributo ng-click), se gestiona mediante el método [reserve] del modelo M en la vista V. Se le pasa el número de la franja horaria de la reserva;
    • línea 26: al hacer clic en el botón [eliminar], se ejecuta el método [reserve] del modelo M en la vista V. Se le pasa el número de la cita que se va a eliminar;
  • línea 39: el código JavaScript que gestiona la aplicación se encuentra en el archivo [rdvmedecins-07.js];
  • línea 40: el código JS para la directiva [footable] aplicada en la línea 20;

3.7.9.2. El controlador C

El código JavaScript para [rdvmedecins-07.js] se crea primero copiando el archivo [rdvmedecins-06.js]. A continuación, se modifica. Se mantienen los bloques de código grandes habituales. Los cambios se realizan principalmente en el controlador:

Image

Describiremos el controlador C para 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) {
      // ------------------- model initialization
      // model
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      $scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
      $scope.errors = {show: false, model: {}};
      $scope.medecins = {
        data: [
          {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
          {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
          {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
          {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
        ],
        title: config.listMedecins
      };
      var médecin = $scope.medecins.data[0];
      var clients = [
        {id: 1, version: 1, titre: "Mr", nom: "MARTIN", prenom: "Jules"},
        {id: 2, version: 1, titre: "Mme", nom: "GERMAN", prenom: "Christine"},
        {id: 3, version: 1, titre: "Mr", nom: "JACQUARD", prenom: "Maurice"},
        {id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
      ];
      // for the date
      angular.copy(config.locales['fr'], $locale);
      var today = new Date();
      var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
      var fullDay = $filter('date')(today, 'fullDate');
      $scope.agenda = {title: config.agendaTitle, data: undefined, show: false, model: {titre: médecin.titre, prenom: médecin.prenom, nom: médecin.nom, jour: fullDay}};
 
 
      // ---------------------------------------------------------------- agenda initial
      // the global asynchronous task
      var task;
      // we ask for the agenda
      getAgenda();
 
      // ------------------------------------------------------------------ réservation
      $scope.reserver = function (creneauId) {
....
      };
 
      // ------------------------------------------------------------ suppression RV
      $scope.supprimer = function (idRv) {
...
      };
 
      // obtaining the agenda
      function getAgenda() {
 ...
      }
 
      // cancel wait
      function cancel() {
...
      }
} ]);
  • línea 6: configuración del mensaje de espera. Por defecto, esperaremos 3 segundos antes de realizar una solicitud HTTP;
  • línea 7: información necesaria para las solicitudes HTTP;
  • línea 8: configuración del mensaje de error;
  • líneas 9-17: médicos predefinidos;
  • línea 18: un médico específico. Se realizarán reservas para las franjas horarias de este médico;
  • líneas 19-24: clientes codificados;
  • línea 26: queremos gestionar fechas en francés;
  • línea 27: las citas se programarán para la fecha de hoy;
  • línea 28: el servicio de reservas web espera fechas en el formato «aaaa-mm-dd»;
  • línea 29: la fecha de hoy en el formato [jueves, 26 de junio de 2014];
  • línea 30: configuración del calendario. El atributo [model] contiene los parámetros del mensaje internacionalizado que se va a mostrar:

        agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
  • línea 35: la variable global [task] representa la tarea asíncrona que se está ejecutando en un momento dado;
  • línea 37: se solicita el calendario inicial;

Eso es todo lo que se hace durante la carga inicial de la página. Si todo va bien, la vista muestra el calendario de la Sra. PELISSIER para ese día.

Image

3.7.9.4. Recuperación del calendario

El calendario se recupera utilizando el siguiente método [getAgenda]:


      // obtaining the agenda
      function getAgenda() {
        // the URL service path
        var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
        // we ask for the agenda
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
        // waiting msg
        $scope.waiting.visible = true;
        // we analyze the result of the call to service [dao]
        task.promise.then(function (result) {
          // end of wait
          $scope.waiting.visible = false;
          // mistake?
          if (result.err == 0) {
            // we prepare the agenda model
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            // timetable display formatting
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
          } else {
            // mistakes were made in obtaining the agenda
            $scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
          }
        });
}

Este código es el mismo que el estudiado en la aplicación anterior. Hay dos cambios:

  • no hay una espera simulada antes de la llamada HTTP;
  • línea 4: utilizamos el doctor creado durante la inicialización del controlador, así como el día formateado que se construyó;

Este código se ha aislado en una función porque también lo utilizan las funciones [reserve] y [delete].

3.7.9.5. Reservar una franja horaria

Recuerda que los clientes se eligen al azar.

El código de reserva es el siguiente:


$scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        // we create a RV with a random customer in the slot identified by [id]
        var idClient = clients[Math.floor(Math.random() * clients.length)].id;
        utils.debug("réservation du créneau pour le client", idClient);
        // simulated waiting
        $scope.waiting.visible = true;
        var task = utils.waitForSomeTime($scope.waiting.time);
        // we add the
        var promise = task.promise.then(function () {
          // the URL service path
          var path = config.urlSvrResaAdd;
          // data to be sent to the service
          var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
          // start the asynchronous task
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // we return the promise of task completion
          return task.promise;
        });
 
        // task result analysis
        promise = promise.then(function (result) {
          if (result.err != 0) {
            // there were errors in validating the appointment
            $scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
          } else {
            // we ask for the new agenda
            getAgenda();
          }
        });
 
      };
  • línea 1: ten en cuenta que el parámetro de la función [reserve] es el número de ranura (atributo id);
  • línea 4: Se selecciona al azar a un cliente de la lista de clientes predefinida en el código de inicialización. Conservamos su identificador [id];
  • líneas 7-8: la espera de 3 segundos;
  • líneas 11-18: estas líneas se ejecutan solo después de que hayan transcurrido los 3 segundos;
  • línea 12: la URL del servicio de reservas [/ajouterRv]. Esta URL es diferente de las 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: la URL no tiene parámetros y se solicita mediante un POST;
    • línea 2: los parámetros enviados tienen la forma de un objeto JSON. Este se deserializará en el parámetro [post] (@RequestBody);

Vimos un ejemplo de este POST (sección 2.12.2):

  • en [0], la URL del servicio web;
  • en [1], se utiliza el método POST;
  • en [2], el texto JSON de la información enviada al servicio web en el formato {day, clientId, slotId};
  • en [3], el cliente informa al servicio web de que está enviando datos JSON;

Volvamos al código JS de la función [reserve]:

  • línea 14: creamos el valor que se va a enviar en forma de objeto JS. Angular lo serializará a JSON cuando se envíe;
  • línea 16: se realiza la solicitud 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] realiza un POST en lugar de un GET (véase el código en la sección 3.7.6.4);
  • Línea 18: se devuelve la promesa de la llamada HTTP;
  • líneas 23-29: se ejecutan solo cuando la llamada HTTP ha devuelto su respuesta;
  • línea 23: el parámetro [result] tiene el formato [err,data] o [err,messages], donde [err] es un código de error;
  • líneas 23-26: si se han producido errores, se muestra el mensaje de error;
  • línea 28: si la reserva se ha realizado correctamente, se vuelve a mostrar el nuevo calendario;

3.7.9.6. Modificación del servidor

  

En la clase [RdvMedecinsCorsController], añadimos el siguiente método:


    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // we authorize the header [authorization]
            response.addHeader("Access-Control-Allow-Headers", "authorization");
        }
 
    @RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
    public void ajouterRv(HttpServletResponse response) {
        sendOptions(response);
}

La adición se realiza en las líneas 10–13. Los encabezados de las líneas 2–8 se enviarán para la URL [/addAppt] (línea 10) y el método HTTP [OPTIONS] (línea 10).

La clase [RdvMedecinsController] se modifica de la siguiente manera:


    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
        // headers CORS
        rdvMedecinsCorsController.ajouterRv(response);
...

Para el método [POST] (línea 1) y la URL [/addAppointment] (línea 1), se invoca el método que acabamos de añadir a [RdvMedecinsCorsController] (línea 4), devolviendo así los mismos encabezados HTTP que para el método HTTP [OPTIONS].

3.7.9.7. Pruebas

Realicemos una prueba inicial en la que reservemos cualquier franja horaria disponible:

 

Como siempre en estos casos, tenemos 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] ha fallado con [status=0], lo que significa que Angular ha cancelado la solicitud. La causa del error se encuentra en los registros:

XMLHttpRequest cannot load http://localhost:8080/ajouterRv. Request header field Content-Type is not allowed by 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 permisos:
    • permiso para enviar los encabezados HTTP [accept, authorization, content-type];
    • permiso para enviar una solicitud POST;
  • en [4]: el servidor autoriza el encabezado [authorization]. Recuerda que, en el lado del servidor, somos nosotros quienes enviamos esta autorización;

La novedad es que, para una operación POST, el cliente Angular solicita autorizaciones adicionales al servidor. Por lo tanto, debemos modificar el servidor para concedérselas:

  

En la clase [RdvMedecinsCorsController], modificamos el método privado que genera los encabezados HTTP enviados para las solicitudes OPTIONS, GET y POST:


    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // certain headers are allowed
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // the POST is authorized
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
}
  • línea 7: hemos añadido una autorización para los encabezados HTTP [accept, content-type];
  • línea 9: hemos añadido una autorización para el método POST;

Volvemos a ejecutar la prueba tras reiniciar el servidor:

 

Esta vez, la reserva se ha realizado correctamente.

3.7.9.8. Eliminar una cita

El código de la función [delete] es el siguiente:


$scope.supprimer = function (idRv) {
        utils.debug("suppression rv n°", idRv);
        // simulated waiting
        $scope.waiting.visible = true;
        task = utils.waitForSomeTime($scope.waiting.time);
        // we add the
        var promise = task.promise.then(function () {
          // the URL service path
          var path = config.urlSvrResaRemove;
          // data to be sent to the service
          var post = {idRv: idRv};
          // start the asynchronous task
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // we return the promise of task completion
          return task.promise;
        });
 
        // task result analysis
        promise = promise.then(function (result) {
          if (result.err != 0) {
            // there have been errors deleting the rv
            $scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
            // the UI is updated
            $scope.waiting.visible = false;
          } else {
            // we ask for the new agenda
            getAgenda();
          }
        });
      };
  • línea 1: recuerda que el parámetro de la función es el ID de la cita que se va a eliminar. Este código es muy similar al código de reserva. Solo comentaremos las diferencias;
  • línea 9: la URL del servicio aquí es [/deleteAppointment] y, como antes, se accede a ella mediante una solicitud 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 de nuevo en formato JSON. En la sección 2.12.17, mostramos la naturaleza de la solicitud POST realizada manualmente:

  • en [1], la URL del servicio web;
  • en [2], se utiliza el método POST;
  • en [3], el texto JSON de la información enviada al servicio web en el formato {idRv};
  • en [4], el cliente informa al servicio web de que está enviando datos JSON;

Volvamos al código JS de la función [delete]:

  • línea 11: creamos el objeto enviado. Angular lo serializará automáticamente a JSON;

El resto del código es similar al de la reserva.

3.7.9.9. Cambios en el lado del servidor

En el lado del servidor, realizamos los siguientes cambios:

  

En la clase [RdvMedecinsCorsController], añadimos el siguiente método:


    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // certain headers are allowed
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // the POST is authorized
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
    }
...
    @RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
    public void supprimerRv(HttpServletResponse response) {
        sendOptions(response);
}

La adición se realiza en las líneas 13–16. Los encabezados de las líneas 2–10 se enviarán para la URL [/deleteAppointment] (línea 13) y el método HTTP [OPTIONS] (línea 13).

La clase [RdvMedecinsController] se modifica de la siguiente manera:


    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
        // headers CORS
        rdvMedecinsCorsController.supprimerRv(response);
...

Para el método [POST] (línea 1) y la URL [/deleteAppointment] (línea 1), se invoca el método que acabamos de añadir a [RdvMedecinsCorsController] (línea 4), devolviendo así los mismos encabezados HTTP que para el método HTTP [OPTIONS].

3.7.10. Ejemplo 10: Creación y cancelación de citas - 2

Ahora presentamos la misma aplicación que antes, pero en lugar de reservar para un cliente aleatorio, el cliente 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 principales diferencias.

Duplicamos el archivo [app-19.html] como [app-20.html] y, a continuación, creamos el código para la lista desplegable de clientes [1]:


<!-- customer list -->
  <div class="alert alert-info">
    <h3>{{agenda.title|translate:agenda.model}}</h3>
 
    <div class="row" ng-show="clients.show">
      <div class="col-md-3">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
  • líneas 8–12: la lista desplegable se implementará utilizando el componente [bootstrap-select];
  • línea 1: la directiva [selectEnable] se aplica mediante el atributo [select-enable];
  • línea 1: la etiqueta <select> se genera solo si existe [clients.data] (# null, undefined). Este punto es importante y se explicó en la sección 3.7.7.8;

Además, importamos nuevos archivos JS:


<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- directives -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • Línea 1: El archivo [rdvmedecins-08.js] se crea copiando el archivo [rdvmedecins-0.js];
  • Líneas 3-4: Se importan los archivos de ambas directivas;

3.7.10.2. Controlador C

El código del controlador C evoluciona de la siguiente manera:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- model initialization
...
      // our customers
      $scope.clients = {title: config.listClients, show: false, model: {}};
 
      //------------------------------------------- initilisation vue
      // the global asynchronous task
      var task;
      // we ask for the customers, then the agenda
      getClients().then(function () {
        getAgenda();
      });
...
 
      // execution action
      function getClients() {
....
      };
} ]);
  • línea 8: el objeto [$scope.clients] configura la lista desplegable de clientes en la vista V;
  • líneas 14–16: de forma asíncrona, primero solicitamos la lista de clientes y, una vez obtenida, solicitamos la agenda de la Sra. PELISSIER para hoy. La sintaxis utilizada aquí solo funciona porque la función [getClients] devuelve una promesa;

El método [getClients] recupera la lista de clientes:


function getClients() {
        // the UI is updated
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        // we ask for the customer list;
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // analyze the result of the previous call
        promise = promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // we put the acquired data into the model
            $scope.clients.data = result.data;
            // the UI is updated
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            // there were errors in obtaining the customer list
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // the UI is updated
            $scope.waiting.visible = false;
          }
        });
        // we return the promise
        return promise;
      };

Este es un código que ya hemos visto y comentado. La parte importante a destacar es la línea 31:

  • línea 27: devolvemos la promesa de la línea 10, es decir, la última promesa obtenida en el código. Esta promesa solo se cumplirá una vez que la solicitud HTTP haya devuelto su respuesta;

El método [reserve] cambia ligeramente:


      $scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        // on crée un RV pour le client sélectionné
        var idClient = $(".selectpicker").selectpicker('val');
        ...
        });
  • Línea 4: Ya no hacemos reservas para un cliente cualquiera, sino para el cliente seleccionado de la lista de clientes.

3.7.11. Ejemplo 11: una directiva [selectEnable2]

Este ejemplo vuelve a tratar 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>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>
 
  <!-- the error list -->
  <div class="alert alert-danger" ng-show="errors.show">
   ...
  </div>
 
  <!-- customer list -->
  <div class="alert alert-info">
    <div class="row" ng-show="clients.show">
      <div class="col-md-4">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
 
  <!-- list of doctors -->
  <div class="alert alert-info">
    <div class="row" ng-show="medecins.show">
      <div class="col-md-4">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins-09.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="selectEnable2.js"></script>
  • líneas 19–23: la lista desplegable del cliente;
  • 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 [doctors.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 para 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: mostramos el valor del parámetro [attrs] para ayudar a comprender cómo funciona el código. Veremos que attrs['id']='selectpickerClients' para la lista de clientes;
  • línea 6: para localizar un elemento con [id='x'] en el DOM, escribimos [$('#x')]. Por lo tanto, debemos escribir [$('#selectpickerClients')] para localizar la lista de clientes. Esto se consigue utilizando la sintaxis [$('#' + attrs['id'])];

La directiva [selectEnable2] utiliza, por tanto, la información que contiene uno de los atributos del elemento HTML al que se aplica.

3.7.11.4. Controlador C

El controlador C se encuentra en el archivo JS [rdvmedecins-09.js] y tiene la siguiente estructura:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
    function ($scope, utils, config, dao) {
      // ------------------- model initialization
      // the waiting msg
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      // login information
      $scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
      // errors
      $scope.errors = {show: false, model: {}};
      // the doctors
      $scope.medecins = {title: config.listMedecins, show: false, model: {}};
      // our customers
      $scope.clients = {title: config.listClients, show: false, model: {}};
 
      // the global asynchronous task
      var task;
      // ---------------------------------------------------- initialisation vue
      // the UI is updated
      $scope.waiting.visible = true;
      $scope.clients.show = false;
      $scope.medecins.show = false;
      $scope.errors.show = false;
      // we ask for customers, then doctors
      getClients().then(function () {
        getMedecins();
      });
 
      // customer list
      function getClients() {
        ...
      }
 
      // list of doctors
      function getMedecins() {
...
      }
 
      // cancel wait
      function cancel() {
...
      }
    } ]);
  • líneas 26–28: primero consultamos a los clientes, luego a los médicos;

3.7.11.5. Pruebas

Prueba esta nueva versión.

3.7.12. Ejemplo 12: Una directiva [list]

Usaremos el mismo ejemplo que antes, pero queremos optimizar el código HTML utilizando una directiva. Actualmente, tenemos el siguiente código HTML:


<!-- customer list -->
  <div class="alert alert-info">
    <div class="row" ng-show="clients.show">
      <div class="col-md-4">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
  <!-- list of doctors -->
  <div class="alert alert-info">
    <div class="row" ng-show="medecins.show">
      <div class="col-md-4">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>

Las líneas 14–26 son idénticas a las líneas 1–13. Se aplican a los médicos en lugar de a los clientes. Nos gustaría poder escribir lo siguiente:


  <!-- la liste des clients -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- la liste des médecins -->
<list model="medecins" ng-if="medecins.show"></list>

Este código utiliza 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) {
    // instance de la directive retournée
    return {
      // élément HTML
      restrict: "E",
      // url du fragment
      templateUrl: "list.html",
      // scope unique à chaque instance de la directive
      scope: true,
      // fonction lien avec le document
      link: function (scope, element, attrs) {
        utils.debug("directive list attrs", attrs);
        scope.model = scope[attrs['model']];
        utils.debug("directive list model", scope.model);
        $timeout(function () {
          $('#' + scope.model.id).selectpicker();
        })
      }
    }
}]);
  • línea 2: define una directiva llamada «list»;
  • línea 6: el atributo [restrict] especifica cómo se puede utilizar la directiva. [restrict: "E"] significa que la directiva [list] se puede utilizar como un elemento HTML <list ...>...</list>. [restrict: "A"] significa que la directiva [list] se puede utilizar como un atributo, por ejemplo <div ... list='...'>. [restrict: "AE"] significa que la directiva [list] se puede utilizar tanto como atributo como elemento;
  • línea 8: el atributo [templateUrl] especifica el nombre del fragmento HTML que se utilizará cuando se encuentre la etiqueta. Este fragmento será el cuerpo de la etiqueta;
  • línea 10: el atributo [scope] establece el ámbito de la plantilla de la directiva. [scope: true] significa que dos elementos <list> tendrán cada uno su propia plantilla. Por defecto (scope 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 cómo se utilizará la directiva:


  <!-- la liste des clients -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- la liste des médecins -->
<list model="medecins" ng-if="medecins.show"></list>

La directiva [list] se utiliza como un 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 rellenará el modelo de la directiva;
  • [ng-if]: que garantiza que no se genere el código HTML de la directiva si no hay nada que mostrar;

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();
        })
      }

Combinemos este código JS con el código HTML que utiliza la directiva:


  <list model="clients" ng-if="clients.show"></list>
  • línea 3: attrs['model'] tiene aquí el valor «clients»;
  • línea 3: scope[attrs['model']] tiene el valor scope['clients'] y, por lo tanto, representa [$scope.clients], es decir, el campo [clients] del modelo de vista. Este campo tendrá el valor {id:'...', data:[client1, client2, ...], show:..., title:'...'};
  • línea 3: añadimos un campo [model] al modelo de la directiva. Este campo hereda del modelo de la vista en la que se encuentra. Por lo tanto, debemos evitar conflictos con cualquier campo [model] que la vista también pueda tener. Aquí no habrá ningún conflicto;
  • línea 4: mostramos [scope.model] para comprender mejor el código;
  • líneas 5-7: vemos código que ya hemos visto antes. La diferencia es que el ID del componente se recuperaba anteriormente de un atributo attrs['id']. Aquí, se recuperará de [scope.model.id];

Ahora, veamos el código HTML generado por la directiva. Debido al atributo [templateUrl: "list.html"] de la directiva, debemos buscarlo en el archivo [list.html]:


<!-- a list of customers or doctors -->
<div class="alert alert-info" ng-show="model.show">
  <div class="row">
    <div class="col-md-4">
      <h2 translate="{{model.title}}"></h2>
      <select data-style="btn-primary" id="{{model.id}}" ng-if="model.data">
        <option ng-repeat="element in model.data" value="{{element.id}}">
          {{element.titre}} {{element.prenom}} {{element.nom}}
        </option>
      </select>
    </div>
  </div>
</div>
  • Lo primero que hay que recordar al 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 ID a la etiqueta <select>. Este ID es utilizado por el código JavaScript de la directiva;
  • línea 6: uso de [model.data] para generar el <select> solo si hay datos que mostrar;
  • líneas 7-9: uso de [model.data] para generar los elementos de la lista desplegable;

3.7.12.2. El código HTML

El código HTML de la aplicación [app-22.html] es el siguiente:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>
 
  <!-- the error list -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>
 
  <!-- customer list -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- list of doctors -->
  <list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="list.js"></script>
  • línea 22: no olvides incluir el código JS para 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) {
      // ------------------- model initialization
...
      // the doctors
      $scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
      // our customers
      $scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
  • Líneas 7 y 9: añadimos el atributo [id] a los modelos doctors y clients;

3.7.12.4. Las pruebas

Las pruebas arrojan los mismos resultados que en el ejemplo anterior.

3.7.13. Ejemplo 13: Actualización del modelo de una directiva

Continuamos nuestro estudio de las directivas y seguimos con el ejemplo de la lista desplegable. Aquí queremos examinar 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], solicitamos la lista de clientes por primera vez;
  • en [2], solicitamos la lista de clientes por segunda vez. Esta segunda lista se combina entonces con la primera [3]. Es la actualización del componente [Bootstrap select] lo que queremos examinar en este ejemplo.

3.7.13.2. La página HTML

La página HTML [app-23.html] se crea copiando [app-22.html] y, a continuación, se modifica de la siguiente manera:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- le message d'attente -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>
 
  <!-- la liste d'erreurs -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>
 
  <!-- le bouton -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>
 
  <!-- la liste des clients -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- directives -->
<script type="text/javascript" src="list2.js"></script>

Los cambios respecto a la aplicación anterior son los siguientes:

  • líneas 15-17: se ha añadido un botón;
  • línea 20: uso de una nueva directiva [list2];
  • línea 23: uso de un nuevo archivo JS;
  • línea 25: importación del archivo JS 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) {
    // instance de la directive retournée
    return {
      // élément HTML
      restrict: "E",
      // url du fragment
      templateUrl: "list.html",
      // scope unique à chaque instance de la directive
      scope: true,
      // fonction lien avec le document
      link: function (scope, element, attrs) {
        utils.debug('directive list2');
        scope.model = scope[attrs['model']];
        $timeout(function () {
          $('#' + scope.model.id).selectpicker('refresh');
        })
      }
    }
}]);

La única diferencia con respecto a la directiva [list] está en la línea 16: con el método [selectpicker('refresh')], le indicamos 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, la lista desplegable se actualice. No 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], creado al copiar el archivo [rdvmedecins-10.js]:


      // our customers
      $scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
      // customer list
      $scope.getClients = function getClients() {
        // the UI is updated
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        // we ask for the customer list;
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // analyze the result of the previous call
        promise = promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
             // put the acquired data into a new model to force the view to refresh
            $scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
            // the UI is updated
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            // there were errors in obtaining the customer list
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // the UI is updated
            $scope.waiting.visible = false;
          }
        });
}
  • línea 1: para permitir la concatenación de matrices en [clients.data], este objeto se inicializa con una matriz vacía;
  • línea 18: concatenamos la nueva lista de clientes con los que ya están presentes en la matriz [clients.data];

Anteriormente, habíamos escrito:

// we put the acquired data into the model
$scope.clients.data = result.data;

Ahora escribimos:

// put the acquired data into a new model to force the view to refresh
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};

Para entender este código, debes recordar cómo se utiliza el modelo M en la vista V en el caso de la directiva [list2]:


  <!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>

El modelo utilizado por la directiva [list2] es [clients]. Solo se volverá a evaluar en la vista V si [clients] cambia en el modelo M de la vista. La primera idea que se nos ocurre para la modificación es escribir:

$scope.clients.data=$scope.clients.data.concat(result.data) ;

para tener en cuenta que la nueva lista de clientes debe añadirse a las anteriores. Al hacerlo, se modifica [clients.data], pero no [clients]. No estoy familiarizado con las complejidades de JavaScript, pero no sería de extrañar que [clients] fuera un puntero, al igual que [clients.data]. El puntero [clients] no cambia cuando modificamos el puntero [clients.data]. Por lo tanto, la directiva [list2] no se reevalúa. Esto es precisamente lo que observamos al depurar la aplicación (F12 en Chrome).

Al escribir:

$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};

Nos aseguramos de que [$scope.clients] reciba efectivamente un nuevo valor. El puntero [$scope.clients] apunta a un nuevo objeto. La directiva [list2] debería entonces volver a evaluarse. Sin embargo, no obtenemos el resultado deseado. Examinemos las capturas de pantalla cuando solicitamos la lista de clientes dos veces:

  • en [1], solo tenemos cuatro elementos en lugar de ocho;
  • en [2], estos cuatro elementos se encuentran en un elemento [select], pero están ocultos (style='display: none');
  • en [3], encontramos los cuatro clientes en un diseño HTML diferente, y esto es lo que ve el usuario al hacer clic en la lista desplegable;

Por último, los registros de la consola muestran lo siguiente:

1
2
3
4
[dao] init
[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"}
directive list2
[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"}
  • línea 1: se instancia el servicio [dao];
  • línea 2: el servicio [dao] recupera una lista inicial de clientes;
  • línea 3: se ejecuta la directiva [list2];
  • línea 4: el servicio [dao] recupera una segunda lista de clientes;

La salida de la línea 2 proviene del siguiente código de 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 se activa aunque la vista se haya mostrado por primera vez. Esto se debe a su atributo [ng-if="clients.show"] en la vista V:

<list2 model="clients" ng-if="clients.show"></list2>
  • línea 3: tras recuperar la primera lista de médicos, [clients.show] pasa a ser true y se activa la directiva;
  • tras recuperar la segunda lista de clientes, vemos que el código de la directiva [list2] no se ejecuta. Por eso no vemos la segunda lista;

Para resolver este problema, modificamos la directiva [list2] de la siguiente manera:


angular.module("rdvmedecins")
  .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
    // instance de la directive retournée
    return {
      // élément HTML
      restrict: "E",
      // url du fragment
      templateUrl: "list.html",
      // scope unique à chaque instance de la directive
      scope: true,
      // fonction lien avec le document
      link: function (scope, element, attrs) {
        // à chaque fois que attrs["model"] change, le modèle de la directive doit changer également
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("directive list2 newValue", newValue);
          // on met à jour le modèle de la directive
          scope.model = newValue;
          $timeout(function () {
            $('#' + scope.model.id).selectpicker('refresh');
          })
        });
      }
    }
}]);
  • Línea 14: La función [scope.$watch] permite observar un valor en el modelo. Su sintaxis es [scope.$watch('var'), f], donde [var] es el identificador de una variable en el modelo y f es la función que se ejecutará cuando esa variable cambie de valor. Aquí queremos observar la variable [clients]. Por lo tanto, debemos escribir [scope.$watch('clients')]. Dado que tenemos attrs['model']='clients', escribimos [scope.$watch(attrs["model"], function (newValue)];
  • línea 14: el segundo parámetro de la función [scope.$watch] es la función que se ejecutará cuando la variable observada cambie de valor. El parámetro [newValue] es el nuevo valor de la variable, es decir, en nuestro caso, el nuevo valor de la variable [clients] en el modelo;
  • línea 17: este nuevo valor se asigna al campo [model] del modelo de la directiva;

Una vez realizado este cambio, los registros cambian:

Arriba vemos que, tras obtener la segunda lista de clientes, la directiva [list2] se ejecuta de nuevo, tal y como 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>
 
  <!-- le message d'attente -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>
 
  <!-- la liste d'erreurs -->
  <div class="alert alert-danger" ng-show="errors.show">
  ...
  </div>
 
  <!-- le bouton -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>
 
  <!-- la liste des clients -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
  • líneas 5-7: el mensaje de carga;
  • líneas 10-12: el mensaje de error;

Decidimos colocar el código HTML de estos dos mensajes dentro de las 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>
 
  <!-- le message d'attente -->
  <waiting model="waiting"></waiting>
 
  <!-- la liste d'erreurs -->
  <errors model="errors"></errors>
 
  <!-- le bouton -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>
 
  <!-- la liste des clients -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- directives -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
  • 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 para las tres directivas;

3.7.14.2. La directiva [waiting]

El código JS para la directiva [waiting] se encuentra en el siguiente archivo [waiting.js]:


angular.module("rdvmedecins")
  .directive("waiting", ['utils', function (utils) {
    // returned directive instance
    return {
      // element HTML
      restrict: "E",
      // fragment url
      templateUrl: "waiting.html",
      // scope unique to each directive instance
      scope: true,
      // function link to document
      link: function (scope, element, attrs) {
        // each time attr["model"] changes, the page model must also change
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[waiting] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
  }]);

Este código sigue la misma lógica que la directiva [list2] ya comentada.

En la línea 8, hacemos 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, el modelo [$scope.waiting] para este código HTML se definirá de la siguiente manera:


// the waiting msg
$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) {
    // returned directive instance
    return {
      // element HTML
      restrict: "E",
      // fragment url
      templateUrl: "errors.html",
      // scope unique to each directive instance
      scope: true,
      // function link to document
      link: function (scope, element, attrs) {
        // each time attr["model"] changes, the page model must also change
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[errors] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
}]);

Este código sigue la misma lógica que la directiva [list2] ya comentada.

En la línea 8, hacemos 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, el modelo [$scope.errors] para este código HTML se definirá de la siguiente manera:


// there were errors in obtaining the customer list
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};

3.7.15. Ejemplo 15: Navegación

Hasta ahora, hemos utilizado aplicaciones de una sola página. En este ejemplo, trataremos 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], la URL de la vista n.º 1;
  • en [2], su contenido;
  • en [3], vamos a la página 2;
  • en [4], vista n.º 2;
  • en [5], vamos a la página 3;
  • en [6], vista n.º 3;
  • en [7], vamos a la página 1;
  • en [8], volvemos a la vista n.º 1;

3.7.15.2. Organización del código

Estamos iniciando 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 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">
        <!-- the navigation bar -->
        <ng-include src="'views/navbar.html'"></ng-include>
 
        <!-- the current view -->
        <ng-view></ng-view>
    </div>
 
...
<!-- the module -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- controllers -->
<script type="text/javascript" src="controllers/mainController.js"></script>
<script type="text/javascript" src="controllers/page1Controller.js"></script>
<script type="text/javascript" src="controllers/page2Controller.js"></script>
<script type="text/javascript" src="controllers/page3Controller.js"></script>
</body>
</html>
  • línea 7: el cuerpo del contenedor está controlado por [mainCtrl];
  • línea 9: la directiva [ng-include] permite incluir un archivo HTML externo, en este caso una barra de navegación;
  • línea 12: las diferentes vistas mostradas por el contenedor se representan dentro de la directiva [ng-view]. En definitiva, 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: importamos los archivos JS del módulo de la aplicación [rdvmedecins-13.js] y sus controladores;

3.7.15.4. El módulo de aplicación

El archivo [rdvmedecins-13.js] define el módulo de la aplicación y el enrutamiento entre vistas:


// --------------------- Angular module
angular.module("rdvmedecins", [ 'ngRoute' ]);
 
angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ routage
  $routeProvider.when("/page1",
    {
      templateUrl: "views/page1.html",
      controller: 'page1Ctrl'
    });
  $routeProvider.when("/page2",
    {
      templateUrl: "views/page2.html",
      controller: 'page2Ctrl'
    });
  $routeProvider.when("/page3",
    {
      templateUrl: "views/page3.html",
      controller: 'page3Ctrl'
    });
  $routeProvider.otherwise(
    {
      redirectTo: "/page1"
    });
}]);
  • línea 1: define el módulo [rdvmedecins]. Depende del módulo [ngRoute] proporcionado por la biblioteca [angular-route.min.js]. Este módulo habilita el enrutamiento definido en las líneas 6–24;
  • línea 4: define la función [config] del módulo [rdvmedecins]. Tenga en cuenta que esta función se ejecuta antes de que se instancie ningún servicio. Es una función de configuración del módulo. Aquí se configura su enrutamiento. Esto se hace utilizando el objeto [$routeProvider] proporcionado por el módulo [ngRoute];
  • Líneas 6-10: definen la vista que se mostrará cuando el usuario solicite la URL [/page1]. Se trata de un enrutamiento interno dentro de la aplicación. La URL es en realidad [/rdvmedecins-angular-v1/app-21.html#/page1]. Podemos ver que se sigue utilizando la URL del contenedor [/rdvmedecins-angular-v1/app-21.html], pero con información adicional tras el carácter #. Es esta información adicional la que gestiona el enrutamiento de Angular;
  • línea 8: especifica el fragmento HTML que se insertará en la directiva [ng-view] del contenedor:
  • línea 9: especifica el nombre del controlador para este fragmento;
  • líneas 11–15: definen la vista que se mostrará cuando el usuario solicite la URL [/page2];
  • líneas 16–20: definen la vista que se mostrará cuando el usuario solicite la URL [/page3];
  • líneas 21–24: definen el enrutamiento que se debe realizar cuando la URL solicitada no es ninguna de las tres anteriores (en caso contrario, línea 21);
  • línea 23: redirige a la URL [/page1] y, por lo tanto, a la vista definida en las líneas 6–10;

3.7.15.5. El controlador del contenedor de vistas

Hemos visto que el contenedor de vistas declaró un controlador:


<div class="container" ng-controller="mainCtrl">

El controlador [mainCtrl] se define en el archivo [mainController.js]:


// controller
angular.module("rdvmedecins")
  .controller('mainCtrl', ['$scope', '$location',
    function ($scope, $location) {
 
      // page templates
      $scope.page1 = {};
      $scope.page2 = {};
      $scope.page3 = {};
      // global model
      var main = $scope.main = {};
      main.text = "[Modèle global]";
 
      // methods exposed to view
      main.showPage1 = function () {
        $location.path("/page1");
      };
      main.showPage2 = function () {
        $location.path("/page2");
      };
      main.showPage3 = function () {
        $location.path("/page3");
      }
}]);
  • 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">
        <!-- the navigation bar -->
        <ng-include src="'views/navbar.html'"></ng-include>
 
        <!-- the current view -->
        <ng-view></ng-view>
</div>
  • El controlador [mainCtrl] crea el modelo para el área 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 controlador crea el modelo para el área que se muestra en la línea 6. Entonces tenemos dos modelos en esta área:
    • el modelo creado por el controlador [mainCtrl];
    • el modelo creado por el controlador [page1Ctrl];

Existe una convención de nomenclatura para los modelos. En la vista que se muestra en la línea 6, los modelos de los controladores [mainCtrl] y [pagexCtrl] son visibles. Si dos variables de estos modelos tienen el mismo nombre, una anulará a la otra. Para evitar este conflicto de nombres, creamos cuatro modelos con cuatro nombres diferentes:

página
controlador
modelo
línea de código
contenedor
mainCtrl
principal
11
página1
página1Ctrl
página1
7
página 2
página2Ctrl
página2
8
página 3
página3Ctrl
página3
9
  • línea 12: define un elemento [text] en la plantilla [main];

Las líneas 7–11 tienen un efecto muy específico: definen el [$scope] del controlador [mainCtrl] y, dentro de él, crean cuatro variables [main, page1, page2, page3]. Estas cuatro variables se utilizarán como los respectivos modelos para el contenedor y las tres vistas que este contendrá a su vez.

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">
        <!-- the navigation bar -->
        <ng-include src="'views/navbar.html'"></ng-include>
 
        <!-- the current view -->
        <ng-view></ng-view>
</div>

La barra de navegación se define en la línea 3. Esto significa que solo conoce la plantilla [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 activará la ejecución del método [$scope.main.showPage1]. Este se define en el controlador [mainCtrl] de la siguiente manera:

      // global model
      var main = $scope.main = {};
      main.text = "[Modèle global]";
 
      // methods exposed to view
      main.showPage1 = function () {
        $location.path("/page1");
};
  • Línea 6: En el código anterior, podemos ver que el método [main.showPage1] es en realidad el método [$scope.main.showPage1]. Por lo tanto, este es el que se ejecutará;
  • línea 7: cambiamos la URL de la aplicación a [/page1]. Volvamos al enrutamiento definido en el módulo principal:

  $routeProvider.when("/page1",
    {
      templateUrl: "views/page1.html",
      controller: 'page1Ctrl'
});

Podemos ver 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, la plantilla [main] es visible. Esto es lo que queremos comprobar en la línea 4. Además, el controlador [page1Ctrl] para el fragmento [views/page1.html] define una plantilla [page1]. Esta es la 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) {
 
      // page 1 template
      var page1=$scope.page1;
      page1.text="[Modèle local dans page 1]";
}]);
  • Línea 2: El [$scope] inyectado aquí no está vacío. Dado que el controlador [page1Ctrl] controla un área insertada en un contenedor controlado por [mainCtrl], el [$scope] de la línea 2 contiene los elementos del [$scope] definido por el controlador [mainCtrl]. Es importante comprender esto. El [$scope] definido por el controlador [mainCtrl] contiene los siguientes elementos: [main, page1, page2, page3]. Esto significa que tenemos acceso a los modelos de todas las vistas. Esto no es necesariamente deseable, pero es el caso aquí. En la versión final del cliente Angular, utilizaremos esta característica para almacenar en el modelo [main] la información que deba compartirse entre vistas. Esto será análogo al concepto de «sesión» del lado del servidor;
  • línea 6: recuperamos el modelo [page1] para la página 1 desde [$scope] y luego trabajamos con él (línea 7). A continuación, obtenemos la siguiente visualización:
 

Las vistas [/page2] y [/page3] se basan en el mismo modelo que la vista [/page1] (véanse las capturas de pantalla de la página 240).

3.7.15.8. Control de navegación

Ahora queremos controlar la navegación de la siguiente manera [página1 --> página2 --> página3 --> página1]. Por lo tanto, si el usuario se encuentra en la página 1 [/página1] y escribe la URL [/página3] en su navegador, esta navegación no debería aceptarse y el usuario debería permanecer en la página 1.

Para lograrlo, modificamos los controladores de página de la siguiente manera:


angular.module("rdvmedecins")
  .controller('page1Ctrl', ['$scope', '$location',
    function ($scope, $location) {
      // authorized navigation?
      var main = $scope.main;
      if (main.lastUrl && main.lastUrl != '/page3') {
        // we return to the last URL
        $location.path(main.lastUrl);
        return;
      }
      // we store the URL of the page
      main.lastUrl = '/page1';
      // page template
      var page1 = $scope.page1;
      page1.text = "[Modèle local dans page 1]";
    }]);
  • línea 12: cuando se muestra una página, almacenamos su URL en el modelo [main.lastUrl]. Aquí estamos utilizando el concepto que comentamos anteriormente: usar el modelo [main] para almacenar información compartida por todas las vistas. En este caso, se trata de la última URL visitada;
  • El código de las líneas 4–12 se duplica y se adapta para las tres vistas. Aquí nos encontramos en la vista [/page1];
  • línea 5: recuperamos el modelo [main];
  • línea 6: si el modelo [main.lastUrl] existe y es diferente de [/page3], entonces se prohíbe la navegación (la última URL visitada existe y no es /page3);
  • línea 8: a continuación, volvemos a la última URL visitada;

Probémoslo:

  • en [1], estamos en la página 1 y escribimos la URL de la página 3 en [2];
  • en [3], no se produjo la navegación y volvimos a la URL de la página 1;

3.7.16. Conclusión

Hemos cubierto todos los casos de uso que encontraremos en la versión final del cliente Angular. Cuando la presentemos, nos centraremos más en las características de la aplicación que en los detalles de su implementación. Para estos últimos, simplemente nos remitiremos al ejemplo que ilustra el caso de uso en cuestión.

3.8. El cliente Angular definitivo

3.8.1. Estructura del proyecto

El proyecto final tiene este aspecto:

  • en [1], todo el proyecto. [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 distintas vistas que se insertan en la página maestra [app.html];

3.8.2. Dependencias del proyecto

Las dependencias del proyecto son las siguientes:

 

La función de estos diversos elementos se explicó en la sección 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é">
  <!-- on CSS -->
  <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
  <link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
  <link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
  <link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
  <link href="assets/css/footable.core.min.css" rel="stylesheet"/>
</head>
<!-- controller [appCtrl], model [app] -->
<body ng-controller="appCtrl">
<div class="container">
 ...
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- modules -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- services -->
<script type="text/javascript" src="services/config.js"></script>
<script type="text/javascript" src="services/dao.js"></script>
<script type="text/javascript" src="services/utils.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="directives/waiting.js"></script>
<script type="text/javascript" src="directives/errors.js"></script>
<script type="text/javascript" src="directives/footable.js"></script>
<script type="text/javascript" src="directives/debug.js"></script>
<script type="text/javascript" src="directives/list.js"></script>
<!-- controllers -->
<script type="text/javascript" src="controllers/appController.js"></script>
<script type="text/javascript" src="controllers/loginController.js"></script>
<script type="text/javascript" src="controllers/homeController.js"></script>
<script type="text/javascript" src="controllers/agendaController.js"></script>
<script type="text/javascript" src="controllers/resaController.js"></script>
</body>
</html>
  • línea 18: ten en cuenta 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">
  <!-- navigation bars -->
  <ng-include src="'views/navbar-start.html'" ng-show="app.navbarstart.show"></ng-include>
  <ng-include src="'views/navbar-run.html'" ng-show="app.navbarrun.show"></ng-include>
  <!-- the jumbotron -->
  <ng-include src="'views/jumbotron.html'"></ng-include>
  <!-- page title -->
  <div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
       translate-values="{{app.titre.model}}"></div>
  <!-- page errors -->
  <errors model="app.errors" ng-show="app.errors.show"></errors>
  <!-- the waiting message -->
  <waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
  <!-- the current view -->
  <ng-view></ng-view>
  <!-- debug -->
  <debug model="app" ng-show="app.debug.on"></debug>
</div>

Independientemente de la vista que se muestre, siempre tendrá los siguientes elementos:

  • líneas 3-4: una barra de comandos. Las dos barras de las líneas 3 y 4 son mutuamente excluyentes;

Image

Image

  • línea 6: un logotipo o texto de la aplicación:

Image

  • línea 8: un título

Image

  • línea 11: un mensaje de error:

Image

  • línea 13: un mensaje de carga:

Image

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

Image

Todos los elementos anteriores están controlados por una directiva [ng-show / ng-hide], lo que significa que, aunque estén presentes, no tienen por qué ser visibles.

3.8.4. Las vistas de la aplicación

En el código de la página maestra, tenemos:


<div class="container">
  ...
  <!-- the current view -->
  <ng-view></ng-view>
  ...
</div>

La línea 4 recibe las distintas vistas de la aplicación. Estas se definen en el módulo [main.js]:

Image

La función de configurar las diferentes rutas se explicó en la sección 3.7.15.4, página 242.

La vista [login.html] está vacía, lo que significa que no añade ningún elemento a los que ya están presentes en la página maestra.

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

Image

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

Image

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

Image

3.8.5. Características de la aplicación

Las vistas del cliente Angular ya se presentaron en la sección 1.3.3, en la página 7. Para facilitar la lectura de este nuevo capítulo, las repetimos aquí. La primera vista es la siguiente:

  • [6], la página de inicio de sesión de la aplicación. Se trata de una aplicación de gestión de citas para médicos;
  • en [7], una casilla de verificación que permite al usuario activar o desactivar el modo [debug]. Este modo se caracteriza por la presencia del panel [8], que muestra el modelo de la vista actual;
  • en [9], un tiempo de espera artificial en milisegundos. El 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 ver la gestión de la espera implementada por la aplicación;
  • en [10], la URL del servidor Spring 4. Según lo anterior, es [http://localhost:8080];
  • en [11] y [12], el nombre de usuario y la contraseña del usuario que 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 utilizar la aplicación. El rol USER se incluye únicamente para demostrar la respuesta del 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: francés (predeterminado) e inglés.
  • en [1], inicias sesión;
  • una vez que hayas iniciado sesión, puedes elegir el médico con el que deseas concertar una cita [2] y la fecha de la cita [3];
  • En [4], solicitas ver la agenda del médico seleccionado para el día elegido;
  • Una vez que se muestra la agenda del médico, puedes reservar una franja horaria [5];
  • En [6], seleccione el paciente para la cita y confirme su selección en [7];

Una vez confirmada la cita, se le redirigirá automáticamente al calendario, donde aparecerá la nueva cita. Esta cita se puede eliminar más tarde [7].

Ya se han descrito las funciones principales. Son sencillas. Las que no se han descrito son funciones de navegación para volver a una vista anterior. Concluyamos con la configuración del idioma:

  • En [1], el idioma cambia de francés a inglés;
  • en [2], la vista cambia al inglés, incluido el calendario;

3.8.6. El módulo [main.js]

El módulo [main.js] define el módulo Angular que controlará la aplicación:

 
  • línea 4: el módulo se llama [rdvmedecins];
  • línea 5: el módulo [ngRoute] se utiliza para el enrutamiento de URL;
  • línea 6: el módulo [translate] se utiliza para la internacionalización del texto;
  • línea 7: el módulo [base64] se utiliza para codificar la cadena «login:password» en Base64;
  • 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: configuración de rutas;
  • línea 40: internacionalización de mensajes;

3.8.7. El controlador de la página maestra

Repasemos el código HTML de la página maestra [app.html]:


<body ng-controller="appCtrl">
<div class="container">
...

Línea 1: Todo el cuerpo de la página maestra está controlado por el controlador [appCtrl]. Debido a su posición, esto lo convierte en el controlador general y principal de la aplicación. Tal y como se explica en la sección 3.7.15, el modelo creado por este controlador es heredado por todas las vistas que se insertarán en la página maestra.

Su código es el siguiente:


angular.module("rdvmedecins")
  .controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
    function ($scope, config, utils, $location, $locale) {
 
      // debug
      utils.debug("[app] init");
 
      // ----------------------------------------initialisation page
      // templates for # pages
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      // current page template
      var app = $scope.app;
      ...
 
      // ---------------------------------- méthodes
 
      // cancel current job
      app.cancel = function () {
...
      };
 
      // disconnect
      app.deconnecter = function () {
        ...
      };
 
      // this code must remain here as it refers to the preceding [cancel] function
      app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
    }])
;

Las líneas 10–14 definen los cinco modelos utilizados en la aplicación:

Modelo
Ver
Controlador
$scope.app
app.html
appCtrl
$scope.login
login.html
loginCtrl
$scope.home
home.html
homeCtrl
$scope.reservation
reserva.html
resaCtrl
$scope.agenda
agenda.html
agendaCtrl

Es importante comprender que el objeto [$scope], al ser el modelo del controlador de la página maestra, es heredado por todas las vistas y controladores. Por lo tanto, el controlador [loginCtrl] tiene acceso a los elementos [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda]. En otras palabras, un controlador tiene acceso a los ámbitos de otros controladores. La aplicación en cuestión evita cuidadosamente utilizar esta capacidad. Así, por ejemplo, el controlador [loginCtrl] trabaja solo con dos ámbitos:

  • su propio [$scope.login];
  • y el del controlador padre [$scope.app];

Lo mismo se aplica a todos los demás controladores. El modelo [$scope.app] se utilizará como memoria compartida entre los diferentes controladores. Cuando un controlador C1 necesita pasar información al controlador C2, se sigue el siguiente procedimiento:

En [C1]:

$scope.app.info=value ;

En [C2]:

var value=$scope.app.info ;

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 controladores, se denomina a menudo «sesión» en los comentarios, imitando la sesión utilizada en las aplicaciones web tradicionales, que hace referencia a la memoria compartida entre solicitudes HTTP sucesivas.

Volvamos al código del controlador [appCtrl]:


      // templates for # pages
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      // current page template
      var app = $scope.app;
      // [app.debug] and [utils.verbose] must always be synchronized
      app.debug = utils.verbose;
      app.debug.on = config.debug;
      // no page title for the moment
      app.titre = {show: false};
      // no navigation bars
      app.navbarrun = {show: false};
      app.navbarstart = {show: false};
      // no errors
      app.errors = {show: false};
      // local default
      angular.copy(config.locales['fr'], $locale);
      // the current view
      app.view = {url: undefined, model: {}, done: false};
      // the current task
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
  • línea 8: [$scope.app] será el modelo de la página maestra. También servirá como memoria compartida entre los distintos controladores. En lugar de escribir [$scope.app.field=value] en todas partes, el puntero [$scope.app] se asigna a la variable [app], por lo que escribimos [app.field=value]. Solo hay que recordar que [app] es el modelo expuesto a la página maestra;
  • línea 11: [app.debug.on] es un booleano que controla el modo de depuración de la aplicación. Por defecto, está establecido en true. Su valor está vinculado a la casilla de verificació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:

Image

  • línea 16: [app.navbarstart.show] controla la visualización de la siguiente barra de navegación:

Image

  • línea 18: [app.errors] es la plantilla para el banner de error;

Image

  • Línea 22: [app.view] contendrá información sobre la vista actual, es decir, la que muestra actualmente la etiqueta [ng-view] en la página maestra. Incluiremos allí la siguiente información:
    • [url]: la URL de la vista actual, por ejemplo [/agenda];
    • [model]: el modelo de la vista actual, por ejemplo [$scope.agenda];
    • [done]: cuando es true, indica que la vista actual ha terminado su trabajo y que estamos cambiando a otra vista;

Esta información de se utiliza para controlar la navegación.

  • línea 24: inicia una tarea asíncrona, una espera simulada. La tarea asíncrona se referencia mediante dos punteros [app.view.model.task.action] y [app.task];

Se han incorporado dos métodos en el controlador [appCtrl]:


      // cancel current job
      app.cancel = function () {
...
      };
 
      // disconnect
      app.deconnecter = function () {
        ...
};
  • línea 2: la función [app.cancel] se utiliza para cancelar la tarea actual para la que se muestra un mensaje de carga. Todas las vistas muestran este mensaje, por lo que la tarea se cancelará aquí;
  • línea 7: la función [app.logout] devuelve al usuario a la página de inicio de sesión. Todas las vistas, excepto la vista [/login], ofrecen esta opción;

La función [app.deconnecter] es la siguiente:


      // disconnect
      app.deconnecter = function () {
        // we return to the login page
        $location.path(config.urlLogin);
};
  • línea 4: volver a la página de inicio de sesión en la URL [/login];

3.8.8. Gestión de tareas asíncronas

En nuestra aplicación, en un momento dado, solo se ejecutará una tarea asíncrona. Es posible tener varias tareas en ejecución. Por ejemplo, cuando se inicia la aplicación, solicita la lista de médicos al servicio web y, a continuación, la lista de clientes con dos solicitudes HTTP sucesivas. Podríamos hacer lo mismo con dos solicitudes HTTP simultáneas. Angular proporciona las herramientas para ello. En este caso, no hemos optado por ese enfoque.

La tarea que se está ejecutando actualmente se cancela con el siguiente código en el controlador [appCtrl]:


      // cancel current job
      app.cancel = function () {
        utils.debug("[app] cancel task");
        // cancel the current view's asynchronous task
        var task = app.view.model.task;
        task.isFinished = true;
        task.action.reject();
 
        ...
};
  • línea 5: la tarea se recupera de [app.view.model.task]. Por lo tanto, todos los controladores se asegurarán de que sus tareas asíncronas hagan referencia a este objeto;
  • línea 6: para indicar que la tarea ha finalizado;
  • línea 7: para terminar la tarea con un error. Esta notación difiere de la utilizada en los ejemplos de Angular estudiados:
    • en los ejemplos, el objeto [task] era un objeto [$q.defer()] que podía cancelarse;
    • en la versión final, el objeto [task] es un objeto con los campos [action, isFinished], donde [action] es el objeto [$q.defer()] que puede terminarse y [isFinished] es un valor booleano que indica que la acción se ha completado;

Examinemos el ciclo de vida del objeto [task] mediante un ejemplo. Al inicio, tras el controlador [appCtrl], el controlador [loginCtrl] toma el relevo para mostrar la vista [views/login.html]. Su código de inicialización es el siguiente:


      // retrieve the parent model
      var login = $scope.login;
      var app = $scope.app;
      // current view
app.view = {url: config.urlLogin, model: login, done: false};

En la línea 5, tenemos [model=login]. Esto significa que cuando modificamos el objeto [login], modificamos el objeto [app.view.model], es decir, [$scope.app.view.model]. Cuando queremos simular una espera en el controlador [loginCtrl], escribimos:


// simulated waiting
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};

Al añadir el campo [task] al objeto [login], este se añade al objeto [$scope.app.view.model]. Si el usuario cancela la espera, el código en [appCtrl.cancel]:


// current page template
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();

completará con éxito la espera simulada (líneas 4–6).

3.8.9. Control de navegación

Las reglas de navegación utilizadas en la aplicación son las siguientes:

URL de destino
URL anterior
Navegación permitida
/login
cualquiera
/inicio
/login
sí, si el controlador [loginCtrl] ha indicado que ha terminado su trabajo

/home

/calendario
/calendario
/hogar
sí, si el controlador [homeCtrl] ha indicado que ha terminado su trabajo

/reset

/agenda
/res
/calendario
sí, si el controlador [homeCtrl] ha indicado que ha terminado su trabajo

/reiniciar

Esto se implementa con el siguiente código:

Para [agendaCtrl]:

Image

  • líneas 11-20: implementación de la regla de navegación;
  • línea 26: nueva vista actual;

Para [resaCtrl]:

Image

  • líneas 12–20: implementación de la regla de navegación:
  • línea 27: nueva vista actual;

Para [loginCtrl]:

Image

  • Aquí no hay ningún control de navegación, ya que la regla establece que se puede acceder a la URL [/login] desde cualquier lugar. Por lo tanto, si el usuario escribe esta URL en su navegador, funcionará independientemente de la vista actual;
  • línea 16: la nueva vista actual;

El código para el controlador [homeCtrl] se proporcionó en la sección 3.8.7.

Por último, para una regla como:

/agenda
/home
sí, si el controlador [homeCtrl] ha indicado que ha terminado su trabajo

aquí hay un ejemplo de código que navega desde la URL [/home] a la URL [/agenda]:

 

Arriba, nos encontramos en el método [displayCalendar] del controlador [homeCtrl]. El usuario ha solicitado el calendario 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]. Como hemos visto, este objeto se utiliza como plantilla para la vista [app.html]. Esta plantilla [$scope.app] también se utiliza para almacenar información que debe compartirse entre 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 el modelo [app];
  • línea 116: el controlador [homeCtrl] cederá el control al controlador [agendaCtrl]. Indica que ha terminado su trabajo con el código de la línea 115. Este código será utilizado por el controlador [agendaCtrl] de la siguiente manera:

Image

  • 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. Servicios

  

Los servicios [config, utils, dao] son los que ya se describieron en la descripción general de Angular:

  • el servicio [config] se presentó en la sección 3.7.4;
  • el servicio [utils] se presentó en la sección 3.7.5;
  • el servicio [dao] se presentó en la sección 3.7.6;

A modo de recordatorio, esta es la estructura de estos servicios:

Servicio [config]

  • en [1]: vemos que el código tiene unas 250 líneas. La mayor parte de este código consiste en externalizar las claves para los mensajes internacionalizados [2]. Evitamos codificar estas claves directamente en el código;

servicio [utils]

 
  • Línea 8: Todavía no hemos visto la variable [verbose]. 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 falso. 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 cambio realizado en [app.debug] también se aplicará a [utils.verbose];
  • línea 22: el valor inicial de [app.debug.on] se toma del archivo de configuración. Por defecto, está establecido en true. Este valor puede cambiar con el tiempo. El usuario puede modificarlo a través de las barras de navegación:
 
  • línea 45: una casilla de verificación (type=checkbox) te permite cambiar el valor de [app.debug.on] (atributo ng-model);

Servicio [dao]

 

3.8.11. Directivas

  

Las directivas [errors, footable, list, waiting] son las que ya se describieron en la descripción general de Angular:

  • la directiva [footable] se presentó en la sección 3.7.8.6;
  • la directiva [list] se presentó en la sección 3.7.12;
  • las directivas [errors] y [waiting] se presentaron en la sección 3.7.14;

No habíamos visto 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 representación JSON del modelo [$scope.app] cuando se está en modo de depuración (atributo ng-show). Esto genera una salida como la siguiente:

Esto requiere un buen conocimiento del código para poder interpretarlo, pero una vez adquirido, la información anterior resulta útil para la depuración. Aquí hemos resaltado los elementos del modelo [$scope.app] mostrado. Recordemos que [$scope.app] es la memoria compartida entre los controladores;

  • [waitingBeforeTask]: el tiempo de espera simulado antes de cualquier solicitud HTTP;
  • [debug]: modo de depuración; es necesariamente verdadero si se muestra este banner;
  • [navbarrun]: un valor booleano que controla la visualización de la siguiente barra de navegación:

Image

  • [navbarstart]: un valor booleano que controla la visualización de la siguiente barra de navegación:

Image

  • [errors]: plantilla para la directiva [errors];
  • [view]: encapsula información sobre la vista que se muestra actualmente;
  • [waiting]: plantilla para la directiva [waiting];
  • [serverUrl, username, password]: credenciales de inicio de sesión para el servicio web;
  • [doctors]: modelo para la directiva [list] aplicada a los médicos;
  • [clients]: igual que el anterior, pero para clientes;
  • [menu]: controla las opciones del menú que se muestran. Estas se definen en [navbar-run.html]:

Image

Las opciones del menú se encuentran en las líneas 16, 23, 29 y 36.

  • [formattedDay]: el día seleccionado en el calendario en el formato «aaaa-mm-dd»;
  • [agenda]: la agenda del médico. Contiene franjas horarias disponibles (rv==null) y franjas reservadas. En el caso de estas últimas, incluye el nombre del cliente que realizó la reserva;
  • [selectedCreneau]: la franja horaria seleccionada para realizar una reserva;

3.8.12. El controlador [loginCtrl]

  

El controlador [loginCtrl] está asociado a la vista [views/login.html], que, al combinarse con la página maestra, genera la siguiente página:

Image

El controlador [loginCtrl] es el siguiente:

Image

  • línea 13: [login] será el modelo para la vista actual;
  • línea 14: [app] es la memoria compartida entre controladores;
  • línea 16: [app.view] se rellena con información de la vista actual;

Este código de inicialización se encontrará en todos los controladores. Para el controlador C1 de una vista V1 con el modelo M1, tendremos el siguiente código de inicialización:

1
2
3
var app=$scope.app;
var M1=$scope.M1;
app.view={url: config.urlV1, model:M1, done:false};
  • línea 18: quizá recuerdes que [appCtrl] inició una espera simulada a la que hace referencia el objeto [app.task.action]. Utilizamos la [promise] de esta tarea para esperar a que finalice;
  • línea 39: el método [login.setLang] se encarga del cambio de idioma;
  • línea 47: el método [login.authenticate] se encarga de la autenticación del usuario;

Veamos los pasos principales del método de autenticación:

Image

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

Image

  • Línea 87: El valor booleano [task.isFinished] se establece en true en los siguientes casos:
    • el usuario canceló la espera;
    • la solicitud de los médicos terminó con un error;
  • líneas 91-98: el caso en el que tenemos los clientes;
  • línea 93: [app.clients] es el modelo para la directiva [list] que mostrará los clientes en una lista desplegable;
  • líneas 97-98: nos preparamos para cambiar de vista (línea 98), pero primero indicamos que el controlador ha terminado su trabajo (línea 97). Recuerda que [$scope.app.view.done] se utiliza para el control de la navegación;

Lo importante aquí es que los médicos y los clientes se han almacenado en la caché del navegador. Ya no se solicitarán al servicio web.

3.8.13. El controlador [homeCtrl]

  

El controlador [homeCtrl] está asociado a la vista [views/home.html], que, al combinarse con la página maestra, genera la siguiente página:

Image

La estructura del controlador [homeCtrl] es la siguiente:

Image

  • líneas 12–20: este 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;

Image

  • líneas 25–28: aquí encontramos líneas similares a las del controlador [loginCtrl]. [home] es, por tanto, la plantilla de vista asociada al controlador;
  • línea 33: un atributo que aún no hemos visto. Este es el modelo para la barra de encabezado de la vista:

Image

  • línea 36: [home.datepicker] es el modelo del calendario;
  • línea 38: [app.menu] es el modelo del menú de la barra de navegación. Aquí aparecerá la opción [Schedule]. Esto es lo que te permite consultar la agenda de un médico;

Por último, el controlador tiene dos métodos:

Image

La visualización de la agenda (línea 51) se trató en la sección 3.7.8.

3.8.14. El controlador [agendaCtrl]

  

El controlador [agendaCtrl] está asociado a la vista [views/agenda.html], que, al combinarse con la página maestra, genera la siguiente página:

Image

La estructura del controlador [agendaCtrl] es la siguiente:

Image

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

Image

  • líneas 23-26: [agenda] será la plantilla de vista asociada al controlador [agendaCtrl];
  • líneas 36–44: [app.title] es la plantilla para la siguiente barra de título:

Image

  • línea 46: el menú tendrá la opción [Inicio]:

Image

Los métodos del controlador son los siguientes:

Image

  • línea 95: el método [agenda.delete] se trató en la sección 3.7.9;

El método [agenda.home] es un método de navegación puro:

Image

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

Image

  • línea 73: el parámetro de la función [reserve] es el número de franja horaria (id);
  • líneas 77-86: buscan el intervalo de tiempo con este identificador;
  • línea 82: el intervalo de tiempo encontrado se coloca en la memoria compartida [app]. El controlador [resaCtrl], que tomará el control (línea 90), utilizará esta información para mostrar su barra de título;
  • líneas 89-90: navega a [/resa.html];

3.8.15. El controlador [resaCtrl]

  

El controlador [resaCtrl] está asociado a la vista [views/resa.html], que, al combinarse con la página maestra, genera la siguiente página:

Image

La estructura del controlador [resaCtrl] es la siguiente:

Image

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

Image

  • líneas 24-27: [resa] será la plantilla para la vista actual;
  • líneas 38–45: [app.titre] es la plantilla para la siguiente barra de título:

Image

  • línea 47: se muestran dos opciones de menú:

Image

Los métodos del controlador son los siguientes:

Image

El método [resa.valider] se trató en la sección 3.7.9.

3.8.16. Gestión del idioma

Todos los controladores proporcionan el siguiente método [setLang]:

Image

Se podría haber integrado en el controlador [appCtrl].