lunes, 22 de diciembre de 2014

AngularJS: ng-repeat & filter

Hoy veremos cómo funciona el objeto filter en AngularJS. A priori comentar que filter no sólo se usa para filtrar los elementos de una lista en base a unos criterios sino que también se usa para dar un formato diferente a un valor. Puedes ver las diferentes aplicaciones de filter en la doc oficial de angular. En este artículo veremos ejemplos sobre como filtrar una lista (ng-repeat) en base a unos valores introducidos en uno o varios campos. Lo he enfocado desde un punto de vista práctico, explicado con algunos ejemplos sencillos que pueden ayudarte bastante a entender filter y tomar la decisión acertada en tu aplicación.

Si ya has visto algo de ng-repeat y el filtrado básico verás que por defecto puedes aplicar filter en ng-repeat y a medida que escribes en el campo de texto se va aplicando el filtro al resultado.

<input type=”text” ng-model=”query” placeHolder=”introduce un valor”>
<div ng-repeat=”ítem in ítems” | filter: query>
…
</div>
Si nos fijamos en la sintaxi de ng-repeat, podemos más o menos deducir que lo que está haciendo aquí es comparar cada uno de los atributos de ítem con el valor que hay en el campo asociado a “query” y devolver true/false en función de si encuentra dicho valor contenido en alguno de los valores asociados a las propiedades de item. Imaginemos que ítem tiene una propiedad id y otra nombre. Si escribimos “1” en el campo asociado a query buscará el valor 1 tanto en id como en nombre de cada elemento y si el ítem contiene el valor 1 la fila se seguirá mostrando, sino se dejará de ver este ítem… Este funcionamiento por defecto puede valernos en muchos casos y está bien.

Ahora imaginemos que nos interesa crear un campo de texto que no filtre por todas las propiedades del modelo.. en el ejemplo anterior si el ítem tiene id = 11 seguiría apareciendo si introducimos “1” en el campo de filtro (aunque no estemos mostrando el id por pantalla). En este caso podrías salvarlo fácilmente. Si quieres que el filtro aplique únicamente a una propiedad del modelo puedes hacerlo del siguiente modo:
<input type=”text” ng-model=”query” placeHolder=”introduce un nombre”>
<div ng-repeat=”ítem in ítems” | filter: {Name : query} >
Vamos a complicarlo un poco. Tenemos una lista de dos usuarios:
[
{
Id:1,
Name: Carlos,
HairColor: Moreno
Edad = 32
}, 
{
Id:2,
Name: Alba,
HairColor: Rubio, 
Edad = 29
}
]
Queremos tener 2 campos de texto encima de la lista, uno que filter por Name y el otro que filtre por HairColor. Bien, también podríamos hacerlo siguiendo el patrón del ejemplo anterior:
<input type=”text” ng-model=”queryNombre” placeHolder=”introduce un nombre”>
<input type=”text” ng-model=”queryPelo” placeHolder=”introduce un color de pelo”>
<div ng-repeat=”ítem in ítems” | filter: {Name : queryNombre, HairColor : queryPelo } >
Entonces si introducimos los valores “Car” en el filtro nombre sólo se debería mostrar el usuario que contenga Car en el nombre…

¿Qué pasaría si quiero filtrar, por ejemplo, aquellos usuarios que tengan más de 30 años?

Llegado este punto te diría que no sigas complicando la vista... Crea tu propio filter que para eso está. Antes de crearlo veamos un último ejemplo que nos ayudará a entender cómo funciona filter. Podríamos crearnos una función en el scope y llamarla de este modo:
$scope.Mayor30 = function(item){
 var filter = true;
 If (filterMayor30)
           filter = ítem.Edad > 30;
 return filter;
}
<input type=”checkbox” ng-model=”filterMayor30”>Mayor 30
<div ng-repeat=”ítem in ítems” | filter:  Mayor30 >
Verás que siguen apareciendo los 2 porque en el momento que ng-repeat ha filtrado el checkbox estaba desmarcado. Ahora el problema es que si marcas la casilla no se dispara de nuevo ng-repeat porque filterMayor30 no está vinculado en ningún momento a la lista.

¿Qué hay que hacer en este caso? Olvidarse de trabajar en el controlador específico de la vista y crear un filtro. Las buenas prácticas de diseño de software recomiendan crear objetos con responsabilidad única.. por tanto no uses el controlador para incluir funciones de filtrado y evita también complicar la vista como estábamos haciendo al principio del ejemplo… crea tus filters!

Angular permite crear tus propios filtros, vamos a crearnos el filtro del ejemplo anterior, las buenas prácticas recomiendan que crees un module específico para los filters:

(function () {
    'use strict';

    var filterModule = angular.module('cmFilter', []);
    
    filterModule.filter('Mayor30', function () {
        return function (items) {
            var result = [];

            if (items) {
                items.forEach(function (item) {
                    if (item.Edad > 30) {
                        result.push(item);
                    }
                });
            }

            return result;
        }
    });
}());
Tendrás que inyectar el module de filters que has creado en el mismo module donde tengas los controladores que están gestionando la vista donde necesitas el filtro. Aquí asumo que tu controlador está en el módulo principal para no liar la cosa (mala práctica):
    //AngularJS
    var app = angular.module('app', [
         //Angular modules 
         'cmFilter',
    ]);
Finalmente para usarlo en la vista se haría del siguiente modo:
<div ng-repeat="user in users | Mayor30 ">
Puedes pasar parámetros, imagínate que en vez de fijar mayor30 te interesa que el 30 sea un campo donde puedas introducir un valor. El filtro quedaría así:
    filterModule.filter('MayorX', function () {
        return function (items, edad) {
            var result = [];
            if (!edad) edad = 0;

            if (items) {
                items.forEach(function (item) {
                    if (item.Edad > edad) {
                        result.push(item);
                    }
                });
            }

            return result;
        }
    });
En vista creamos un input para recoger el valor que introducirá el usuario:
    <input type="text" ng-model="FilterEdad" placeholder="introduce un valor para edad" /> 
    <div ng-repeat="user in users | MayorX : FilterEdad| orderBy:orderProp" class="row list-item">
Siguiendo esta pauta, podrías llegar a concatenar tantos parámetros como necesites incluso puedes combinar varios filtros.

Te paso algunos links interesantes que pueden ayudarte con este tema:

Hasta aquí el artículo de hoy, posiblemente sea el último ya de este año 2014. A ver qué tal se nos presenta 2015, por lo pronto yo he decidido emprender una aventurilla y cambiar de trabajo tras 10 años en mi actual empresa... espero tocar temas interesantes en mi nuevo puesto y seguir escribiendo a menudo por areaTIC!

Hasta pronto!

martes, 2 de diciembre de 2014

AngularJS rest CRUD $resource vs $http, ejemplo práctico implementado con $resource.

Buenas a todos, hoy veremos cómo realizar un acceso a datos típico CRUD en angular.

Angular como todo framework javascript moderno ya proporciona mecanismos que hace sencilla la integración contra un web api. Al plantearme cómo hacerlo en mis proyectos siempre tenía la duda si crearme servicios inyectando $http e implementar las acciones en cada servicio o bien usar directamente $resource ya que evitas trabajo y simplificas código (resource de base ya implementa un CRUD contra una api rest). Cabe destacar que no siempre tendrás un api rest pura en servidor o tal vez necesitas un control especial de la petición y respuesta donde tal vez sí que tendría alguna ventaja usar $http frente a $resource.

En este artículo usamos $resource porque proporciona una abstracción un nivel por encima de $http, es decir $resource se vale internamente del objeto $http y ya implementa como base un CRUD. Puedes extender la base, el ejemplo más usado es cambiar el verbo del método update para que vaya por PUT. También trabaja internamente $q (promises en angular), por tanto podemos usar promises siempre que hagamos una operación contra servidor.

Vamos al lío, si no dispones de una api contra la que testear este ejemplo, podéis usar alguna pública como esta que os servirá para testear los get. Asumo que tenéis funcionando una aplicación angular con lo básico.

En primer lugar veremos cómo configurar ng-resource:

Tendrás que descargar el paquete e inyectar la dependencia a ng-resource. Puedes obtenerlo aquí.

Ya sabréis que no es recomendable ni óptimo por diversos motivos trabajar directamente sobre el core module, aunque es posible... aquí para simplificar el tutorial inyectamos directamente en el principal.
    //AngularJS
    var app = angular.module('app', [
         //modules 
            'ngResource'
    ]);
El siguiente paso sería crear un nuevo $resource:

Decido crearme un archivo angular.config.resources.js que contendrá toda la definición de recursos que iré necesitando. Podrías optar por organizar el código diferente, tal vez no interese tener un único archivo sino que quieras tener un archivo diferente por recurso… en definitiva para gustos colores.
(function () {
    'use strict';

    var app = angular.module("app");
    // users Resource
    app.factory("UserResource", function ($resource) {
        return $resource(
            "http://jsonplaceholder.typicode.com/users/:Id",
            { Id: "@Id" },
            {
                "update": { method: "PUT" }
            }
        );
    });

//… más resources …

}());
En este bloque de código es importante asegurarse que estás añadiendo los servicios en el module que toca (el que hayas inyectado la dependencia de ng-resource en este caso ‘app’).

Si os fijáis al definir el resource estamos pasándole la url con los posibles queryString de uri que acepta con el prefijo “:” delante, en el segundo parámetro los mapeamos a variables y el tercer parámetro permite modificar el comportamiento de alguno de los métodos base o bien definir nuevos métodos. En este ejemplo hemos especificado que la acción update irá por verbo PUT.

Podéis customizar prácticamente cualquier aspecto de la petición http echarle un ojo a la doc!

Finalmente veremos cómo realizar una llamada al servicio mediante el recurso que acabamos de crearnos.
(function () {

var app = angular.module('app');
app.controller('userController', ['$scope', '$routeParams', 'UserResource', function ($scope, $routeParams, userResource,) { 
        var currentId = $routeParams.id;
        
        $scope.setDinamicContentLoading(true);
        if (currentId) {
            userResource.get({}, { 'Id': currentId }).$promise.then(function (user) {
                $scope.setDinamicContentLoading(false);
                $scope.user = user;
            }, function (error) {
                $scope.setDinamicContentLoading(false);
                alert('Error retrieving user item: ' + error);
            });
        } else {
            userResource.query().$promise.then(function (users) {
                $scope.setDinamicContentLoading(false);
                $scope.users = users;
            }, function (error) {
                $scope.setDinamicContentLoading(false);
                alert('Error retrieving user list: ' + error);
            });
        }
}());
Esta sería la definición de mi controlador para las vistas de usuario, en mi caso uso el mismo controlador para la lista de usuarios que para la vista edición de un usuario concreto. Comentar que no es recomendable añadir controladores directamente al módulo principal de angular, aquí trabajo de este modo por seguir la coherencia del primer paso donde ya hemos inyectado la dependencia en el principal por simplificar el ejemplo.

Si os fijáis, en función de si la ruta contiene un id o no, sé que estoy en una vista u otra y por tanto debería llamar a una acción u otra del servicio.

He visto bastantes tutoriales que ofrecen ejemplos donde no usan promises… úsalos!

Una vez resuelta la llamada asigno el resultado que viene de server al modelo user o users del scope y así ya los tengo disponibles en vista.

Para hacer un Create, Update y Delete sería llamar a userResource.Save(modelo) y ya tendríamos el create y así respectivamente... en la doc está bien explicado.

El tema de setDinamicContentLoading lo uso para el típico loading, comento un poco por encima como lo tengo implementado por si a alguien le interesase la idea. Trabajo con un controlador de layout que gestiona la vista principal (header, mainContent , footer,).. en mainContent cargará la vista que toque según la ruta que vayas navegando con el menú o como quiera que hayas montado la navegación.. Pues bien se trataría de añadir esto a la vista principal dentro del ámbito del layoutController donde "Ajax-loader.gif" es el típico gif animado que puedes personalizarte en internet.
<div ng-show="loading" class="dinamicContentLoading">
   <img src="./img/ajax-loader.gif" style="position:absolute;top:40%;left:45%" />
</div>
La clase css (añadimos comportamiento modal a la capa donde está contenido el gif animado):
.dinamicContentLoading {
    z-index:100;
    border : 1px solid #c0c0c0;
    background:#f0f0f0;
    padding: 0px 10px 10px 10px;
    position:fixed;
    top:0px;
    left:0px;
    height:100%;
    width:100%;
    opacity:0.7;
    filter:alpha(opacity=70);
}
layoutController.js:
  $scope.setDinamicContentLoading = function (value) {
    $scope.loading = value;
  }
Es importante entender que podemos “llamar” al método $scope.setDinamicContentLoading desde el controlador de usuario debido a Angular los contextos están comunicados jerárquicamente lo cual una vez se entiende es bastante potente (…) Lo que hacemos aquí no tiene demasiado misterio ya que es ng-show el que en función de variable $scope.loading mostrará el modal de cargando o no lo mostrará.

Hasta aquí el post de hoy, cualquier duda o discrepancia no te cortes en comentarla a través del blog o si nos sigues en redes sociales. Puedes visitar el archivo de areaTIC tal vez encuentres algún artículo que pueda interesarte.