Ejemplo Ciudades

Vamos a desarrollar una aplicación front-end angular para acceder a los servicios REST de ciudades que desarrollamos en la sección: Ejemplo Implementación Rest.

La implementación de estos servicios está en el repo en la rama master.

Este ejemplo se trata de una aplicación que tiene un único módulo llamado cities. Debemos hacer una interfaz que permita ver el listado de ciudades, crear una nueva, editar una y borrar una.

El diseño

Los estados que puede tener esta aplicación son:

La siguiente figura muestra varios elementos de la solución:

  1. La aplicación principal contenida en el módulo mainApp (dentro del archivo app.js), depende del módulo externo (ui-router) y del módulo interno CityModule.
  2. El módulo CityModule (dentro del archivo cities.mod.js) define un controlador llamado citiesCtrl y configura los estados del módulo.
  3. El controlador citiesCtrl (dentro del archivo cities.ctrl.js) se va a ocupar de las invocaciones al API REST de City.
  4. La configuración de los estados se encuentra en el mismo archivo cities.mod.js y define los estados de la figura arborescente anterior:
  5. Cada estado indica el template (html), el controlador (que en este ejemplo es el mismo para todos) y la url de la navegación.
    1. En el caso del estado citiesList, el template está definido en el archivo cities.list.html.
    2. En el caso de los estados cityCreate y cityEdit, el template (que es el mismo para ambos) está definido en el archivo cities.create.html.

El proyecto del ejemplo se puede clonar de repo y utilizar la rama master.

El código

La estructura del proyecto es la que se muestra en la figura anterior. En total hay 7 archivos. Tres html:

  1. El index.html
  2. La plantilla para desplegar la lista cities.list.html
  3. La plantilla para crear/editar una ciudad cities.create.html.

Cuatro archivos javascript:

  1. El archivo principal app.js
  2. El archivo que define el módulo de ciudades cities.mod.js
  3. El archivo que define el controlador de las plantillas de ciudades cities.ctrl.js.
  4. El archivo http-interceptor.factory.js que define el maneo de error cuando algo sale mal en los llamados http.
Importante
Tanto las convenciones de organización del proyecto como de nombramiento de los archivos se convierten en muy importantes cuando hay varias personas trabajando sobre el mismo proyecto y cuando éste empieza a crecer. Estas convenciones facilitan la repartición del trabajo evitando conflictos y también la localización y modificación de las partes de la aplicación.

La siguiente figura muestra la navegación en la aplicación. La primera vez aparece un menú desplegable con dos opciones. Una lleva al estado de crear una nueva ciudad y la otra al estado de mostrar las ciudades actuales. Desde el estado CitiesList se navega hacia el estado de edición. Asociados con Cancel o Save se regresa al estado de listar las ciudades.

Arranque

Ahora vamos a explicar dónde y cómo en el código se define la navegación.

  • Es en el index.html donde se definen dos cosas:
    • Los enlaces al estado cityCreate y citiesList y
    • La vista principal o mainView que será utilizada por esos estados.
<html ng-app="mainApp"> <head> // Código suprimido para mostrar el ejemplo </head> <body style="padding-top: 20px;"> <div class="container-fluid col-md-12"> <h3>Ejemplo Cities</h3> <ul> <li><a ui-sref="cityCreate">Create</a></li> <li><a ui-sref="citiesList">List</a></li> </ul> <alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert()">{{alert.msg}} </alert> <div ui-view="mainView"> </div> </div> </body> </html>

El enlace a los dos estados se encuentra en los tags "a", utilizando la directiva ui-sref:

<a ui-sref="cityCreate">Create</a>

<a ui-sref="citiesList">List</a>

El valor que toma ui-sref es el nombre del estado al que se debe llegar.

La definición de estos dos estados se encuentra dentro del archivo cities.mod.js donde se define el módulo CitiesModule.

Estado crear una ciudad

En este estado se despliega una forma básica para crear una ciudad donde sólo se ingresa el nombre. Con respecto a la navegación, es decir las maneras de salir de la forma, la forma tiene dos botones: Cancel y Save.

<div class="col-md-6 well"> <form novalidate name="form" id="city-form" role="form" ng-submit="form.$valid && ctrl.saveRecord(currentRecord.id)"> <div ng-messages="form.$error"> <div ng-message="required">Fill the required fields!</div> </div> <fieldset> <input id="id" class="form-control" type="hidden" ng-model="currentRecord.id" /> <div class="form-group col-md-12" ng-class="{'has-success': form.name.$valid && form.name.$dirty, 'has-error': form.name.$invalid && (form.name.$dirty || form.$submitted)}"> <label for="name" class="col-md-2 control-label">Name</label> <div class="col-md-10"> <input id="name" name="name" class="form-control" type="text" ng-model="currentRecord.name" ng-init="currentRecord.name" required /> </div> </div> <button type="button" ui-sref='citiesList'>Cancel</button> <button type="submit">Save</button> </fieldset> </form>

En el caso de Cancel, la decisión que se tomó es regresar al estado de listar ciudades. Esto se puede ver en el código:

<button type="button" ui-sref='citiesList'>Cancel</button>

En el caso de Save, tienen que suceder dos cosas: primero, efectivamente salvar el registro invocando el servicio Rest (POST) para este propósito y segundo, ir al estado de listar ciudades en donde debe aparecer en la lista la nueva ciudad.

La responsabilidad de salvar la nueva ciudad es del controlador de la forma. Podemos ver el siguiente código, donde se está invocando el método save Record que recibe como argumento el registro actual, el que se acaba de crear y está definido en el controlador de este estado:

<ng-submit="form.$valid &&(ctrl.saveRecord(currentRecord.id)">

Más adelante haremos una explicación completa del controlador; por ahora queremos ver cómo sucede la navegación cuando al salvar la forma y todo sale bien se va al estado de listar ciudades. La instrucción que nos permite cambiar de estado:

$state.go('citiesList');

Estado listar ciudades

Lo que sucede cuando se llega al estado citiesList está configurado en la definición de los estados que se encuentra dentro del archivo cities.mod.js y que explicaremos en un momento. La configuración para este estado dice que:

En la vista mainView desplegar el template contenido en cities.list.html.

<div class="container-fluid"> <table class="table table-striped" class="col-md-4"> <thead> <tr> <th>Name</th> <th>Actions</th> </tr> </thead> <tbody> <tr ng-repeat="record in records"> <td id="{{$index}}-name">{{record.name}}</td> <td> <button id="{{$index}}-edit-btn" class="btn btn-default btn-sm" ui-sref="cityEdit({cityId: record.id})"><span class="glyphicon glyphicon-edit"></span> Edit</button> <button id="{{$index}}-delete-btn" class="btn btn-default btn-sm" ng-click="ctrl.deleteRecord(record)"><span class="glyphicon glyphicon-minus"></span> Remove</button> </td> </tr> </tbody> </table> </div>

Asociado con el botón Edit encontramos la referencia al nuevo estado:

ui-sref="cityEdit({cityId: record.id})"

En este caso el estado recibe un argumento de nombre cityId y de valor el identificador de id de la ciudad que queremos editar.

Estado editar una ciudad

La diferencia entre este estado y el estado crear una ciudad está en que el estado recibe un identificador válido de registro que le permite saber que la ciudad en cuestión ya existe y que se va a editar y no a crear.

La Configuración de los Estados

La configuración de los estados consiste de un nombre y un objeto de configuración. ver la explicación en Los Estados.

En este ejemplo tenemos 3 estados: citiesList, cityCreate y cityEdit. Note que todos configuran el mismo controlador: citiesCtrl. El estado citiesList configura el template para desplegar la lista de las ciudades. Los otros dos estados el template para crear/editar una ciudad. Las url del estado cityEdit incluye el id de la ciudad que se va a editar. Esta información se puede acceder por el controlador a través de una variable Angular que se llama $stateParams. Para que ese valor quede dentro de esa variable, hacemos la asignación:

Param: {cityId: null }

var mod = ng.module("citiesModule", ["ngMessages"]); mod.constant("citiesContext", "api/cities"); mod.config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { var basePath = 'src/modules/cities/'; $urlRouterProvider.otherwise("/citiesList"); $stateProvider.state('citiesList', { url: '/cities', views: { 'mainView': { controller: 'citiesCtrl', controllerAs: 'ctrl', templateUrl: basePath + 'cities.list.html' } } }).state('cityCreate', { url: '/cities/create', views: { 'mainView': { controller: 'citiesCtrl', controllerAs: 'ctrl', templateUrl: basePath + 'cities.create.html' } } }).state('cityEdit', { url: '/cities/:cityId', param: { cityId: null }, views: { 'mainView': { controller: 'citiesCtrl', controllerAs: 'ctrl', templateUrl: basePath + 'cities.create.html' } } }); }]);

El Controlador

Cuando se entra a un estado se ejecuta su controlador. En este ejemplo, por simplicidad, solo tenemos un controlador para todos los estados. En caso de que el código que se va a manejar en un estado sea complicado o muy extenso, vale la pena separar el controlador para que sea más fácil de manejar el código.

Este controlador tiene varias responsabilidades: cargar todas las ciudades, salvar una nueva ciudad, salvar la modificación a una ciudad que ya existía.

Cargar todas las ciudades

Get /cities

Cuando el controlador se ejecuta, carga las ciudades utilizando el servicio $http. El código de este llamado está entre las líneas 7 y 10 del listado. Como ya explicamos, la función $http.get retorna un promise. Si el llamado fue exitoso, la respuesta del GET se retorna en una variable response. Esta variable tiene varias informaciones relacionadas con el llamado pero por ahora nos interesa saber que en response.data se encuentra el objeto json con los datos, en este caso, una colección de objetos city donde cada uno tiene un id y un name.

Si el llamado no fue exitoso se ejecutará el código de la función definida en responseError en un interceptor global de las promesas que está definido en el archivo http-interceptor.factory.js.

Cargar una ciudad

Get /cities/:id

En la línea 19 está el código que trae la ciudad con identificador igual al pasado por argumento. Si todo sale bien actualiza el currentRecord y si no, despliega un mensaje de error. Note que este llamado se hace únicamente si se recibió (vía $stateParams) un identificador de la ciudad (línea 25). Esto se hace en el caso de editar una ciudad.

Crear una ciudad nueva

Post /cities

La función saveRecord (línea 39) se ocupa de crear una nueva ciudad o de modificar una existente. la nueva ciudad se cree si esta no tiene id. En este caso se ejecuta:

$http.post(context, currentRecord).then(function () { $state.go('citiesList'); }, responseError)

Fijese que si la promesa regresa con éxito, se cambia de estado al estado citiesList con la instrucción:

$state.go('citiesList')

Modificar una ciudad

Put /cities

La función saveRecord salva un registro existente cuando el id es conocido. Para esto llama:

$http.put(context + "/" + currentRecord.id, currentRecord).then(function () { $state.go('citiesList');}, responseError);

Note la construcción de la url: context + "/" + currentRecord.id. Si queremos cambiar la información de la ciudad con identificador 1, el path será:

api/cities/1.

var mod = ng.module("citiesModule"); mod.controller("citiesCtrl", ['$scope', '$state', '$stateParams', '$http', 'citiesContext', function ($scope, $state, $stateParams, $http, context) { // inicialmente el listado de ciudades está vacio $scope.records = {}; // carga las ciudades $http.get(context).then(function(response){ $scope.records = response.data; }, responseError); // el controlador recibió un cityId ?? // revisa los parámetros (ver el :cityId en la definición de la ruta) if ($stateParams.cityId !== null && $stateParams.cityId !== undefined) { // toma el id del parámetro id = $stateParams.cityId; // obtiene el dato del recurso REST $http.get(context + "/" + id) .then(function (response) { // $http.get es una promesa // cuando llegue el dato, actualice currentRecord $scope.currentRecord = response.data; }, responseError); // el controlador no recibió un cityId } else { // el registro actual debe estar vacio $scope.currentRecord = { id: undefined /*Tipo Long. El valor se asigna en el backend*/, name: '' /*Tipo String*/, }; $scope.alerts = []; } this.saveRecord = function (id) { currentRecord = $scope.currentRecord; // si el id es null, es un registro nuevo, entonces lo crea if (id == null) { // ejecuta POST en el recurso REST return $http.post(context, currentRecord) .then(function () { // $http.post es una promesa // cuando termine bien, cambie de estado $state.go('citiesList'); }, responseError); // si el id no es null, es un registro existente entonces lo actualiza } else { // ejecuta PUT en el recurso REST return $http.put(context + "/" + currentRecord.id, currentRecord) .then(function () { // $http.put es una promesa // cuando termine bien, cambie de estado $state.go('citiesList'); }, responseError); }; };

results matching ""

    No results matching ""