====== 10.7 AngularJS ====== Ya tenemos un API REST funcionando en el servidor, así que vamos a volver a la parte de AngularJS para por fin tener una aplicación funcionando capaz de hacer un CRUD. ===== remoteResource ===== Lo primero es acabar el servicio ''remoteResource'' para que haga uso del API REST que hemos creado.El servicio se basa en la función ''RemoteResource'' que es la que tiene toda la funcionalidad , la hemos modificado para que los 2 métodos que ya hay hagan uso del API REST. function RemoteResource($http, $q, baseUrl) { this.get = function(idSeguro) { var defered = $q.defer(); var promise = defered.promise; $http({ method: 'GET', url: baseUrl + '/api/SeguroMedico/' + idSeguro }).success(function(data, status, headers, config) { defered.resolve(data); }).error(function(data, status, headers, config) { if (status === 400) { defered.reject(data); } else { throw new Error("Fallo obtener los datos:" + status + "\n" + data); } }); return promise; }; this.list = function() { var defered = $q.defer(); var promise = defered.promise; $http({ method: 'GET', url: baseUrl + '/api/SeguroMedico' }).success(function(data, status, headers, config) { defered.resolve(data); }).error(function(data, status, headers, config) { if (status === 400) { defered.reject(data); } else { throw new Error("Fallo obtener los datos:" + status + "\n" + data); } }); return promise; }; } * Linea 31: Hemos puesto la URL correcta para acceder al servidor, incluyendo el valor del ''idSeguro'' y así obtener solo un seguro médico. * Línea 12: Vamos ahora a tratar de forma distinta si se ha retornado un 400 o cualquier otra cosa. * Línea 13: Si se ha retornado un 400, lo que hay en data es un array de objetos ''BussinessMessage'' y es lo que retornamos como resultado de rechazar la promesa. Notar que ahora hemos cambiado el interzaf de la promesa. Cuando es rechazada se retorna un array de objetos ''BussinessMessage''. * Línea 15: En caso de que no sea un 400, será algún error del servidor, así que lanzamos un error de JavaScript ya que lo que ocurrió en el servidor fue un error así que aquí seguimos tratándolo como un error. * Linea 29: Hemos puesto la URL correcta para acceder al servidor para poder obtener todos los seguros médicos. * Línea 33: Vamos ahora a tratar de forma distinta si se ha retornado un 400 o cualquier otra cosa. * Línea 34: Si se ha retornado un 400, lo que hay en data es un array de objetos ''BussinessMessage'' y es lo que retornamos como resultado de rechazar la promesa. Notar que ahora hemos cambiado el interzaf de la promesa. Cuando es rechazada se retorna un array de objetos ''BussinessMessage''. * Línea 36: En caso de que no sea un 400, será algún error del servidor, así que lanzamos un error de JavaScript ya que lo que ocurrió en el servidor fue un error así que aquí seguimos tratándolo como un error. Como vemos los cambios han sido mínimos, solo la nueva URL y la gestión del error ''400 Bad Request'' que ahora es lo que hace que se rechace la promesa. Pasemos ahora a ver el resto de los métodos. Son casi todos iguales y solo cambia el método HTTP, la URL y los datos que se envían. this.insert = function(seguroMedico) { var defered = $q.defer(); var promise = defered.promise; $http({ method: 'POST', url: baseUrl + '/api/SeguroMedico', data: seguroMedico }).success(function(data, status, headers, config) { defered.resolve(data); }).error(function(data, status, headers, config) { if (status === 400) { defered.reject(data); } else { throw new Error("Fallo obtener los datos:" + status + "\n" + data); } }); return promise; }; this.update = function(idSeguro, seguroMedico) { var defered = $q.defer(); var promise = defered.promise; $http({ method: 'PUT', url: baseUrl + '/api/SeguroMedico/' + idSeguro, data: seguroMedico }).success(function(data, status, headers, config) { defered.resolve(data); }).error(function(data, status, headers, config) { if (status === 400) { defered.reject(data); } else { throw new Error("Fallo obtener los datos:" + status + "\n" + data); } }); return promise; }; this.delete = function(idSeguro) { var defered = $q.defer(); var promise = defered.promise; $http({ method: 'DELETE', url: baseUrl + '/api/SeguroMedico/' + idSeguro }).success(function(data, status, headers, config) { defered.resolve(data); }).error(function(data, status, headers, config) { if (status === 400) { defered.reject(data); } else { throw new Error("Fallo obtener los datos:" + status + "\n" + data); } }); return promise; }; * Línea 6: Usamos el método HTTP de ''POST'' para enviar los datos, en los otros métodos usaremos ''PUT'' y ''DELETE''. * Líneas 8 y 29: Se usa la propiedad ''data''. La propiedad ''data'' contiene el objeto JavaScript que se transforma en un String JSON y se envía en la petición HTTP en el cuerpo de la misma. Esta String JSON es el que posteriormente se obtiene desde el servidor en Java con la anotación [[unidades:10_servidor:06_controlador#accediendo_al_json_de_entrada|@RequestBody]]. * Línea 22: Hemos optado por pasar el parámetro ''idSeguro'' aunque también se podría obtener del mismo objeto JavaScript ''seguroMedico'' mediante ''seguroMedico.idSeguro''. Se ha hecho así para destacar en el update se envía el ''idSeguro'' tanto en la URL como en el propio objeto. Esta clase con poco de trabajo la podríamos hacer genérica y poder reusarla en otros proyectos , sin embargo en vez de usar esta clase existen en AngularJS dos alternativas a crearte tu propia clase: * [[https://docs.angularjs.org/api/ngResource/service/$resource|$resource]] * [[https://github.com/mgonto/restangular|Restangular]] Aunque nuestra clase funcione perfectamente, estas 2 últimas disponen de muchas mas funcionalidades con lo que deberías usarse en proyectos reales y decir que el equipo de AngularJS está trabajando en mejorar aun mas su clase [[https://docs.angularjs.org/api/ngResource/service/$resource|$resource]]. Aun así nuestra clase ''remoteResource'' ha cumplido perfectamente su papel para entender como funciona todo. ===== La aplicación ===== Como ya tenemos acabado el servicio ''remoteResource'' ya podemos modificar la aplicación para poder hacer las siguientes operaciones: * Borrar un seguro médico * Actualizar los datos de un seguro médico * Añadir un nuevo seguro médico ==== Borrar seguro médico ==== Para borrar un seguro médico vamos a añadir un botón de borrado en la tabla en la que se muestran todos los seguros médicos. Estebotón llamará a una función del ''$scope'' que es la que realizará la tarea.
NIF Nombre Apellido Sexo Accion
El Nº de seguros medicos es:
{{seguro.nif}} {{seguro.nombre}} {{seguro.ape1}} Hombre Mujer Desconocido
* Linea 8: Nueva columna con las acciones a realizar para cada seguro médico * Línea 29: Añadimos u nuevo botón que al pulsarlo llama a la función ''borrar(idSeguro)''. Vemos como al usar la expresión ''seguro.idSeguro'' no es ncesario usar las llaves $${{ %%}} ya que estmaos en una expresión que es de JavaScript. El controlador ''ListadoSeguroController'' con el nuevo método ''borrar(idSeguro)'' queda de la siguiente forma: app.controller("ListadoSeguroController", ['$scope', 'seguros', 'remoteResource', function($scope, seguros, remoteResource) { $scope.seguros = seguros; $scope.borrar = function(idSeguro) { remoteResource.delete(idSeguro).then(function() { remoteResource.list().then(function(seguros) { $scope.seguros = seguros; }); }); }; }]); * Linea 1: Inyectamos el servicio ''remoteResource'' ya que nos va a hacer falta. * Línea 5: Llamamos al método ''delete'' para borrar el seguro médico cuya clave sea el parámetro ''idSeguro''. * Línea 6: Si se ha borrado correctamente el seguro médico, volvemos a llamar al método ''list'' para obtenga otra vez los seguros de la base de datos y se refresque la tabla y se vea el seguro borrado en la pantalla. En vez de hacer una llamada a la base de datos podríamos optimizarlo borrando el elemento directamente del array. ==== Actualizar los datos de un seguro médico ==== Vamos ahora actualizar los datos de un seguro médico que ya existe. Lo primera que vamos a hacer es cambiar el nombre del controlador ''DetalleSeguroController'' a ''EditSeguroController'' ya que así queda mas claro que vamos a editar los datos del seguro médico. Lo único que vamos a hacer es modificar la función ''guardar'' para que finalmente llame a la base de datos. app.controller("EditSeguroController", ['$scope', 'seguro', 'remoteResource', '$location', function($scope, seguro, remoteResource, $location) { $scope.filtro = { ape1: "" }; $scope.sexos = [{ codSexo: "H", descripcion: "Hombre" }, { codSexo: "M", descripcion: "Mujer" }]; $scope.seguro = seguro; $scope.guardar = function() { if ($scope.form.$valid) { remoteResource.update($scope.seguro.idSeguro, $scope.seguro).then(function() { $location.path("/seguro/listado"); }); } else { alert("Hay datos inválidos"); } }; }]); * Linea 1: Hemos cambiado el nombre del controlador y también inyectamos los servicios [[unidades:07_rutas:02_routeprovider#servicio_location_desde_javascript|$locate]] y ''remoteResource'' que usaremos en el controlador. * Línea 15: Hemos borrado la creación del objeto seguro vacío puesto que ya no nos hace falta. Lo hemos quitado por fin ya que ahora loa vamos a usar al crear un nuevo seguro médico. * Línea 21: Llamamos al servidor para que actualice los datos del seguro médico en la base de datos del servidor. * Línea 22: En caso de que se hayan actualizado los datos , navegamos a la página del listado de seguros médicos usando el servicio ''$location''. ==== Añadir un nuevo seguro médico ==== Para añadir un nuevo seguro es necesario añadir una nueva ruta y crear un nuevo controlador. El path de la nueva ruta será ''/seguro/new'' y el controlador se llamará ''NewSeguroController''. Añadiremos el siguiente código debajo de las rutas que habíamos definido en el bloque de configuración: $routeProvider.when('/seguro/new', { templateUrl: "detalle.html", controller: "NewSeguroController" }); * Línea 2: Hay que destacar que estamos usando la misma página HTML que para la edición. No hay ningún problema en reutilizar una misma página en dos rutas distintas o también reutilizar incluso el controlador. El controlador quedará de la siguiente forma: app.controller("NewSeguroController", ['$scope', 'remoteResource', '$location', function($scope, remoteResource, $location) { $scope.filtro = { ape1: "" }; $scope.sexos = [{ codSexo: "H", descripcion: "Hombre" }, { codSexo: "M", descripcion: "Mujer" }]; $scope.seguro = { nif: "", nombre: "", ape1: "", edad: undefined, sexo: "", casado: false, numHijos: undefined, embarazada: false, coberturas: { oftalmologia: false, dental: false, fecundacionInVitro: false }, enfermedades: { corazon: false, estomacal: false, rinyones: false, alergia: false, nombreAlergia: "" }, fechaCreacion: new Date() }; $scope.guardar = function() { if ($scope.form.$valid) { remoteResource.insert($scope.seguro).then(function() { $location.path("/seguro/listado"); }); } else { alert("Hay datos inválidos"); } }; }]); * Linea 1:Inyectamos los servicios [[unidades:07_rutas:02_routeprovider#servicio_location_desde_javascript|$location]] y ''remoteResource'' que usaremos en el controlador. * Línea 16: Definimos todos los campos del ''$scope.seguro'' con los valores que queremos que tengan al mostrarse la página. * Línea 41: El método debe obligatoriamente llamarse ''guardar'' ya que ese nombre se llama desde la página ''detalle.html'' la cual compartimos con el controlador ''EditSeguroController''. * Línea 43: Llamamos al servidor para que inserte los datos del seguro médico en la base de datos del servidor. * Línea 44: En caso de que se hayan insertado los datos , navegamos a la página del listado de seguros médicos usando el servicio ''$location''. Para acceder a esta nueva //página// ((realmente a la ruta)) añadimos un nuevo link en la en la página del listado de seguros médicos, en ''listado.html'' Nuevo Seguro Medico Recuerda que al usar los enlaces de HTML se debe poner la almohadilla "#" mientras que usando el servicio [[unidades:07_rutas:02_routeprovider#servicio_location_desde_javascript|$location]] no es necesario. Si ejecutamos la aplicación e insertamos un nuevo seguro médico se producirá la excepción: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "fechaCreacion" Esto es debido a que aun no estamos tratando la propiedad "fechaCreacion" para solucionarlo debemos indicar a la librería Jackson que no pasa nada si falta algún campo. Ello se hace mediante la línea: objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Así que el método ''fromJson'' de la clase ''JsonTransformerImplJackson'' quedará de la siguiente forma: @Override public Object fromJson(String json, Class clazz) { try { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return objectMapper.readValue(json, clazz); } catch (IOException ex) { throw new RuntimeException(ex); } } ===== Gestión de errores ===== Ahora nos falta hacer la gestión de errores que nos retorna el servidor. Como sabemos el servidor puede retorna un ''400 Bad Request'' si la petición no es correcta y en ese caso nos retorna un array de ojetos ''BussinessMessage''. Ya hemos modificado el servicio ''remoteResource'' para que solo en el caso de retornar un ''400 Bad Request'' se rechace la promesa y nos revuelva el array de ojetos ''BussinessMessage''. Ahora tenemos que modificar los controladores para que en caso de rechazo de la promesa nos guardemos el array de ojetos ''BussinessMessage'' en la propiedad ''bussinessMessages'' del ''$scope'' del controlador. Hacemos esto porque de esa forma , al estar en el ''$scope'', podremos mostrar el array en la página HTML. ¿que cambios tenemos que hacer? Simplemente en todos los ''then'' de las promesas , añadir como segundo parámetro la siguiente función: function(bussinessMessages) { $scope.bussinessMessages = bussinessMessages; } Entonces las llamadas a ''remoteResource'' quedan de la siguiente forma: Al insertar: $scope.guardar = function() { if ($scope.form.$valid) { remoteResource.insert($scope.seguro).then(function() { $location.path("/seguro/listado"); }, function(bussinessMessages) { $scope.bussinessMessages = bussinessMessages; }); } else { alert("Hay datos inválidos"); } }; Al actualizar: $scope.guardar = function() { if ($scope.form.$valid) { remoteResource.update($scope.seguro.idSeguro, $scope.seguro).then(function() { $location.path("/seguro/listado"); }, function(bussinessMessages) { $scope.bussinessMessages = bussinessMessages; }); } else { alert("Hay datos inválidos"); } }; Al borrar: $scope.borrar = function(idSeguro) { remoteResource.delete(idSeguro).then(function() { remoteResource.list().then(function(seguros) { $scope.seguros = seguros; }, function(bussinessMessages) { $scope.bussinessMessages = bussinessMessages; }); }, function(bussinessMessages) { $scope.bussinessMessages = bussinessMessages; }); }; Como acabamos de explicar simplemente es poner la función como segundo parámetro del ''then'' ya que es la que se ejecutará si la promesa se rechaza, cosa que solo ocurrirá si el servidor retorna un estado ''400 Bad Request''. Como ya comentamos en [[unidades:07_rutas:04_resolve#el_uso_desde_el_controlador]] no vamos a tratar si falla la llamada a ''remoteResource.list()'' en el ''resolve''. ==== Mostrar los mensajes ==== Por último nos queda mostrar los mensajes en la pantalla. Aparentemente es sencillo ya que solo tenemos que mostrar una lista de objetos por pantalla, pero debe cumplir las siguiente condiciones: * Si no hay ningún mensaje no debe verse nada * Debe haber un botón para poder quitar los mensajes * Debe tratarse el caso de que la propiedad ''fieldName'' sea ''null''. La solución a ésto es el siguiente código HTML:
Se han producido los siguientes errores
* Línea 1: No no hay mensajes todo el ''
'' está oculto. * Línea 4: Recorremos la lista de los mensajes * Línea 5: Somo mostramos el campo ''fieldName'' y los dos puntos ":" si su valor no es null * Línea 6: Mostramo el valor del propio mensaje * Línea 9: Ponemos el botón para que el usuario pueda en cualquier momento quitar los mensajes. Hay que fijarse que realmente el botón no oculta los mensajes sino que borra la lista de mensajes y al hacerlo la directiva ''ng-show'' del ''
'' es la que lo oculta. Si ves el resultado del HTML te puede parecer muy cutre pero por ejemplo con [[http://getbootstrap.com/Bootstrap|Bootstrap]] y simplemente usando [[http://getbootstrap.com/components/#alerts-dismissible|Dismissible alerts]] puede quedar tan elegante como en la siguiente imagen: {{:unidades:10_servidor:dismissiblealerts.png?nolink|}} Y quitamos el horrible botón de "Quitar mensajes" substituyendolo por el aspa de la parte superior derecha. ===== Ejemplo ===== El ejemplo de esta unidad es exactamente lo que acabamos de contar pero en un nuevo proyecto llamado "segurosv1". Para poder probar los mensajes que llegan del servidor mediante un error ''400 Bar Request'' hemos modificado la clase ''SeguroMedicoDAOImplJDBC'' para incluir 2 fallos, que son: * No se permite borrar el seguro médico con ''idSeguroMedico==1'' que es el que tiene el NIF con valor "12345678Z" * No se permite añadir un seguro médico con NIF igual a "11111111A". Por supuesto estas restricciones no tienen ningún sentido y están puestas por un motivo puramente pedagógico. Al principio del método ''insert'' se han añadido las siguientes líneas: if ("11111111A".equals(seguroMedico.getNif())) { throw new BussinessException(new BussinessMessage("nif","La letra no es válida")); } Al principio del método ''delete'' se han añadido las siguientes líneas: if (idSeguroMedico==1) { throw new BussinessException(new BussinessMessage(null,"No es posible borrar el seguro médico")); } Este ejemplo se encuentra en git en [[https://github.com/logongas/cursoangularjs/tree/master/segurosv1]] ===== Referencias ===== * [[https://docs.angularjs.org/api/ngResource/service/$resource|$resource]] * [[https://github.com/mgonto/restangular|Restangular]] * [[http://getbootstrap.com/Bootstrap|Bootstrap]]