5. Filtros

Un filtro se encarga de formater el valor de una expresión, para ofrecérsela al usuario sin modificar el valor original. Puede usarse en vistas, controladores o servicios y son muy sencillos de declarar y programar.

La API subyacente es el filterProvider.

5.1. Cómo usar un filtro

Un mismo filtro se puede utilizar de dos maneras diferentes, en función de si lo hacemos desde una vista o desde código JavaScript.

5.1.1. En una vista

Para usar un filtro en una vista, podemos aplicar una expresión con la siguiente sintaxis:

1 2
{{ expression | filter }} {{ 12 | currency }} <!-- 12€ -->

Los filtros, además, pueden encadenarse. En el siguiente ejemplo, la salida del primer filtro se pasa como entrada del segundo.

1 2
{{ expression | filter1 | filter2 }} {{ 12 | number:2 | currency }} <!-- 12.00€ -->

5.1.2. En controladores, servicios y directivas

Para usar un filtro en un controlador/servicio/directiva, debemos inyectar una dependencia con <nombreDelFiltro>Filter. Por ejemplo, para usar el filtro number que hemos visto en el ejemplo anterior (que formatea un número con los decimales indicados en el segundo parámetro), inyectaríamos numberFilter.

1 2 3 4
angular.module('FilterInControllerModule', []). controller('FilterController', ['$scope', 'numberFilter', function($scope, numberFilter) { $scope.filteredText = numberFilter(12,2); }]);

Otra manera consiste en inyectar el servicio $filter en nuestro código javascript. Con él, podemos llamar a todos los filtros de la siguiente manera:

1 2 3 4
angular.module('FilterInControllerModuleV2', []). controller('FilterController', ['$scope', '$filter', function($scope, $filter) { $scope.filteredText = $filter('number')(12,2); }]);

5.2. Filtros predefinidos en AngularJS

5.2.1. filter

El filtro filter selecciona un subset de ítems dentro de un array, devolviéndolo en un nuevo array

En una plantilla HTML lo usaremos de la forma:

1
{{ filter_expression | filter : expression : comparator}}

En nuestro código javascript lo usaremos de la siguiente forma:

1
$filter('filter')(array, expression, comparator)

El parámetro array se corresponde con el array a filtrar. Por su parte, expression puede ser una cadena (búsqueda en todos los objetos), o bien un objeto (que servirá de "ejemplo" para hacer las búsquedas en el array). El último parámetro es opcional nos permite implementar una función de comparación customizada.

Ejemplo:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
<div ng-app> <div ng-init="friends = [ {name:'John', phone:'555-1276'}, {name:'Mary', phone:'800-BIG-MARY'}, {name:'Mike', phone:'555-4321'}, {name:'Adam', phone:'555-5678'}, {name:'Julie', phone:'555-8765'}, {name:'Juliette', phone:'555-5678'} ]"></div> Search: <input ng-model="searchText"> <table id="searchTextResults"> <tr><th>Name</th><th>Phone</th></tr> <tr ng-repeat="friend in friends | filter:searchText"> <td>{{friend.name}}</td> <td>{{friend.phone}}</td> </tr> </table> <hr> Any: <input ng-model="search.$"> <br> Name only <input ng-model="search.name"><br> Phone only <input ng-model="search.phone"><br> Equality <input type="checkbox" ng-model="strict"><br> <table id="searchObjResults"> <tr><th>Name</th><th>Phone</th></tr> <tr ng-repeat="friendObj in friends | filter:search:strict"> <td>{{friendObj.name}}</td> <td>{{friendObj.phone}}</td> </tr> </table> </div>

5.2.2. currency

El filtro currency nos permite expresar un número en formato moneda. En una plantilla HTML lo usaremos de la forma:

1
{{ currency_expression | currency : symbol : fractionSize}}

En nuestro código javascript lo usaremos de la siguiente forma:

1
$filter('currency')(amount, symbol, fractionSize)

Tanto currency como fractionSize son opcionales. Si no ponemos nada, se expresará en dólares con el formato de separación de miles y decimales estadounidense. Para expresarlo a una región en concreto, lo mejor es instalar el módulo ngLocale que corresponda, en nuestro caso sería angular-locale_es-es.js. Según la versión de AngularJS que estemos utilizando, tendremos que añadirlo a nuestro módulo principal.

1
angular.module('myModule', ['ngLocale'])

Ejemplo:

1 2 3 4
<script src="http://path/to/angular-locale_es-es.js"></script> <div ng-app> {{ 1234567890.25 | currency }} </div>

5.2.3. date

El filtro date nos permite formatear una fecha de la manera deseada. En una plantilla HTML lo usaremos de la forma:

1
{{ date_expression | date : format : timezone}}

En nuestro código javascript lo usaremos de la siguiente forma:

1
$filter('date')(date, format, timezone)

El parámetro date puede ser de varios tipos, aunque lo más habitual es que sea un objeto Date o una fecha en milisegundos. Tanto format como timezone son opcionales. En la referencia de este filtro en la web de AngularJS podemos ver todas las formas que acepta.

El filtro date también se ve afectado por el módulo ngLocale. Ejemplo:

1 2 3 4 5 6 7 8 9 10 11
<script src="http://path/to/angular-locale_es-es.js"></script> <div ng-app> <span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>: <span>{{1288323623006 | date:'medium'}}</span><br> <span ng-non-bindable>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span>: <span>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span><br> <span ng-non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>: <span>{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}</span><br> <span ng-non-bindable>{{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}</span>: <span>{{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}</span><br> </div>

5.2.4. json

El filtro json recibe un objeto como entrada, y devuelve una cadena representando dicho objeto en formato JSON. En nuestro código HTML se usa de la siguiente forma:

1
{{ json_expression | json : spacing}}

En nuestro código javascript lo usaremos de la siguiente forma:

1
$filter('json')(object, spacing)

El parámetro spacing es opcional, e indica el número de espacios que se utilizará en la indetación (el valor por defecto es 2). En el este enlace podemos ver el siguiente ejemplo funcionando:

1 2 3 4 5 6
<div ng-app> <h3>Default spacing</h3> <pre id="default-spacing">{{ {nombre:'Alejandro', asignatura:'Frameworks JavaScript: AngularJS'} | json }}</pre> <h3>Custom spacing</h3> <pre id="custom-spacing">{{ {nombre:'Domingo', asignatura: 'Frameworks de persistencia: JPA'} | json:4 }}</pre> </div>

5.2.5. limitTo

El filtro limitTo recibe como entrada un array, del que tomará un número de elementos igual al parámetro recibido (limit). Estos elementos se tomaran del principio si el número es positivo, y del final si es negativo. En nuestro código HTML se usa de la siguiente forma:

1
{{ limitTo_expression | limitTo : limit}}

En nuestro código javascript lo usaremos de la siguiente forma:

1
$filter('limitTo')(input, limit)
<script>
  angular.module('limitToExample', [])
    .controller('ExampleController', ['$scope', function($scope) {
      $scope.numbers = [1,2,3,4,5,6,7,8,9];
      $scope.letters = "abcdefghi";
      $scope.longNumber = 2345432342;
      $scope.numLimit = 3;
      $scope.letterLimit = 3;
      $scope.longNumberLimit = 3;
    }]);
</script>
<div ng-app="limitToExample">
  <div ng-controller="ExampleController">
    Limit {{numbers}} to: <input type="number" step="1" ng-model="numLimit">
    <p>Output numbers: {{ numbers | limitTo:numLimit }}</p>
    Limit {{letters}} to: <input type="number" step="1" ng-model="letterLimit">
    <p>Output letters: {{ letters | limitTo:letterLimit }}</p>
    Limit {{longNumber}} to: <input type="number" step="1" ng-model="longNumberLimit">
    <p>Output long number: {{ longNumber | limitTo:longNumberLimit }}</p>
  </div>
</div>

5.2.6. lowercase

El filtro lowercase convierte una cadena a minúsculas. En nuestro código HTML se usa de la siguiente forma:

1
{{ lowercase_expression | lowercase}}

En nuestro código javascript lo usaremos de la siguiente forma:

1
$filter('lowercase')(lowercase_expression)

Ejemplo:

1
{{ 'HOLA MUNDO' | lowercase }}

5.2.7. uppercase

El filtro uppercase convierte una cadena a mayúsculas. En nuestro código HTML se usa de la siguiente forma:

1
{{ uppercase_expression | uppercase}}

En nuestro código javascript lo usaremos de la siguiente forma:

1
$filter('uppercase')(uppercase_expression)

Ejemplo:

1
{{ 'hola mundo' | uppercase }}

5.2.8. number

El filtro number formatea un número en el locale que hayamos importado, y con el número de decimales indicado en su segundo parámetro, que es opcional.

En nuestro código HTML se usa de la siguiente forma:
1
{{ number_expression | number : fractionSize}}

En nuestro código javascript lo usaremos de la siguiente forma:

1
$filter('number')(number, fractionSize)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<script> angular.module('numberFilterExample', []) .controller('ExampleController', ['$scope', function($scope) { $scope.val = 1234.56789; }]); </script> <div ng-app="numberFilterExample"> <div ng-controller="ExampleController"> Enter number: <input ng-model='val'><br> Default formatting: <span id='number-default'>{{val | number}}</span><br> No fractions: <span>{{val | number:0}}</span><br> Negative number: <span>{{-val | number:4}}</span> </div> <div>

5.2.9. orderBy

El filtro orderBy nos permite ordenar un array por una propiedad, en sentido normal o inverso. En nuestro código HTML se usa de la siguiente forma:

1
{{ orderBy_expression | orderBy : expression : reverse}}

En nuestro código javascript lo usaremos de la siguiente forma:

1
$filter('orderBy')(array, expression, reverse)

Veamos un ejemplo de cómo funciona, y lo sencillo que es el uso de expression:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
<script> angular.module('orderByExample', []) .controller('ExampleController', ['$scope', function($scope) { $scope.friends = [{name:'John', phone:'555-1212', age:10}, {name:'Mary', phone:'555-9876', age:19}, {name:'Mike', phone:'555-4321', age:21}, {name:'Adam', phone:'555-5678', age:35}, {name:'Julie', phone:'555-8765', age:29}]; $scope.predicate = '-age'; }]); </script> <div ng-app="orderByExample"> <div ng-controller="ExampleController"> <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> <hr/> [ <a href="" ng-click="predicate=''">unsorted</a> ] <table class="friend"> <tr> <th><a href="" ng-click="predicate = 'name'; reverse=false">Name</a> (<a href="" ng-click="predicate = '-name'; reverse=false">^</a>)</th> <th><a href="" ng-click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th> <th><a href="" ng-click="predicate = 'age'; reverse=!reverse">Age</a></th> </tr> <tr ng-repeat="friend in friends | orderBy:predicate:reverse"> <td>{{friend.name}}</td> <td>{{friend.phone}}</td> <td>{{friend.age}}</td> </tr> </table> </div> </div>

La documentación de AngularJs dice que expression puede ser una función que podemos getionar, por ejemplo, en nuestro controlador. Ejemplo:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
angular.module('orderByExample', []) .controller('ExampleController', ['$scope', function($scope) { $scope.friends = [{name:'John', phone:'555-1212', age:10}, {name:'Mary', phone:'555-9876', age:19}, {name:'Mike', phone:'555-4321', age:21}, {name:'Adam', phone:'555-5678', age:35}, {name:'Julie', phone:'555-8765', age:29}]; var predicate = 'age'; var reverse = false; $scope.predicate = predicate; $scope.reverse = reverse; $scope.setPredicate = function(_predicate){ if(predicate === _predicate) { reverse = !reverse; } else { predicate = _predicate; reverse = false; } $scope.predicate = predicate; $scope.reverse = reverse; }; $scope.getPredicate = function(){ return (reverse?'-':'') + predicate; }; }]);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<div ng-app="orderByExample"> <div ng-controller="ExampleController"> <pre>Ordenando por = {{predicate}}; reverse = {{reverse}}</pre> <hr/> [ <a href="" ng-click="setPredicate(null)">unsorted</a> ] <table class="friend"> <tr> <th><a href="" ng-click="setPredicate('name')">Name</a></th> <th><a href="" ng-click="setPredicate('phone')">Phone Number</a></th> <th><a href="" ng-click="setPredicate('age')">Age</a></th> </tr> <tr ng-repeat="friend in friends | orderBy:getPredicate():reverse"> <td>{{friend.name}}</td> <td>{{friend.phone}}</td> <td>{{friend.age}}</td> </tr> </table> </div> </div>

5.3. Cómo crear un filtro personalizado

Para crear un filtro, hay que registrar una nueva función factoría de tipo filter en nuestro módulo. Esta factoría debe devolver una función, que recibe como parámetro el elemento de entrada. Se pueden pasar los parámetros adicionales que haga falta.

En el siguiente ejemplo, construiremos un filtro que se encargue de formatear un número de teléfono [1]:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
angular .module('telephoneSample', []) .filter('telephone', function(){ return function(input) { var number = input || ''; number = number.trim().replace(/[-\s\(\)]/g, ''); if(number.length === 11) { var area = ['(', '+', number.substr(0,2) ,')'].join(''); var local = [number.substr(2, 3), number.substr(5, 3), number.substr(8, 11)].join('-'); return [area,local].join(' ') } if(number.length === 9) { var local = [number.substr(0, 3), number.substr(3, 3), number.substr(6, 11)].join('-'); return local } return number; }; });

Para probarlo en nuestra vista:

1 2 3 4 5
<div ng-app="telephoneSample"> <div>{{'965123' | telephone}}</div> <div>{{'965123456' | telephone}}</div> <div>{{'34965123456' | telephone}}</div> </div>

Para crear un filtro parametrizado, actuaríamos de la misma manera. El siguiente ejemplo muestra cómo sería un filtro que introduce un valor por defecto en caso de que el elemento que pasemos esté vacío. Éste texto será '-', o bien la cadena que pasemos como primer parámetro [2]:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
angular .module('textOrDefaultSample', []) .controller('mainCtrl', function($scope){ $scope.data = { nullValue : null, notNullValue: 'Hello world' }; }) .filter('textOrDefault', function () { return function (input, defaultValue) { defaultValue = defaultValue || '-'; if (!input) { return defaultValue; } if (!angular.isString()) { if (input.toString) { input = input.toString(); } else { return defaultValue; } } if (input.trim().length > 0) { return input; } return defaultValue; }; });
1 2 3 4 5 6
<div ng-app="textOrDefaultSample" ng-controller="mainCtrl"> <div>{{ data.nullValue | textOrDefault}}</div> <div>{{ data.nullValue | textOrDefault:'N/D'}}</div> <div>{{ data.notNullValue | textOrDefault}}</div> <div>{{ data.notNullValue | textOrDefault:'N/D'}}</div> </div>

Vemos que hemos hecho uso de la función angular.isString. El objeto angular nos proporciona una serie de utilidades que pueden sernos de gran ayuda, y que podemos ver listados en https://docs.angularjs.org/api/ng/function.

5.4. Tip: cómo acceder a un array filtrado fuera de ng-repeat

Como hemos dicho varias veces, un filtro genera una salida nueva, sin modificar el elemento original. Así, supongamos el siguiente ejemplo [3]:

1 2 3 4 5 6 7 8 9 10 11 12 13 14
angular .module('app', []) .controller('mainCtrl', function($scope){ $scope.teachers = [ 'Domingo', 'Otto', 'Eli', 'Miguel Ángel', 'Aitor', 'Fran', 'José Luís', 'Álex' ]; });
1 2 3 4 5 6 7
<div ng-app="app" ng-controller="mainCtrl"> <label>Filtrar: <input type="text" ng-model="search" /></label> <ul> <li ng-repeat="it in teachers | filter:search">{{it}}</li> </ul> Profesores filtrados: {{ teachers.length }}. </div>

El resultado de profesores filtrados, filtremos los ítems que filtremos, siempre será 8.

Una cosa que se nos podría ocurrir para corregir esto, sería volver a filtrar el resultado:

1
Profesores filtrados: {{ teachers.length | filter:search }}.

Es una opción válida, pero no óptima: recordemos que un filtro es una operación que puede resultar bastante costosa, en función de lo que hayamos programado. Además, podemos encadenar filtros, lo que hace que su complejidad aumente en cada filtro:

1
Profesores filtrados: {{ teachers.length | filter:search | sort | uppercase | prepend:'--->' | append: '<---' }}.

Lo ideal para este caso sería emplear el mismo array filtrado para ambos casos. Y lo podemos conseguir, simplemente inicializando una variable al valor de los elementos filtrados [4] :

1 2 3 4 5 6 7
<div ng-app="app" ng-controller="mainCtrl"> <label>Filtrar: <input type="text" ng-model="search" /></label> <ul> <li ng-repeat="it in filteredTeachers = (teachers | filter:search)">{{it}}</li> </ul> Profesores filtrados: {{ filteredTeachers.length }}. </div>

Para el caso de los filtros encadenados que hemos visto antes, sería exactamente igual:

1 2 3 4 5 6 7
<div ng-app="app" ng-controller="mainCtrl"> <label>Filtrar: <input type="text" ng-model="search" /></label> <ul> <li ng-repeat="it in filteredTeachers = ( | filter:search | sort | uppercase | prepend:'--->' | append: '<---')">{{it}}</li> </ul> Profesores filtrados: {{ filteredTeachers.length }}. </div>

5.5. Ejercicios

Aplica el tag filters a la versión que quieres que se corrija.

5.5.1. Adición de filtro de moneda (0,66 puntos)

Modifica el ejemplo del carrito de la compra para que los importes se formateen en euros y formato español de decimales. Revisa la documentación del filtro currency, porque tendremos que asegurarnos que siempre se muestren dos decimales, haya más o menos en el importe determinado para un prodcto en el servicio.

5.5.2. Custom filter (0,67 puntos)

Vamos a crear un filtro propio que nos devuelva el nombre del producto. Este filtro se llamará productFullName.

Recibirá como parámetro un objeto de tipo producto, con lo que en la vista lo pasaremos de la siguiente manera:

1 2 3 4 5
<p> {{ product | productFullName }} <br/> {{ product.price }} &euro; </p>

Este filtro mostrará, concatenados, la marca y el nombre del producto. La marca deberá mostrarse en mayúsculas, y no deberemos usar la función toUpperCase() de JavaScript, sino inyectar el filtro upperCaseFilter en nuestro filtro y hacer uso de él.

5.5.3. Custom filter con parámetros (0,67 puntos)

Vamos a crear un filtro personalizado para la puntuación de los productos. Mostrará la puntuación en forma de estrellas. Se mostrarán tantas estrellas rellenas como puntuación tenga el producto, y luego se mostrarán estrellas vacías hasta llegar a un total de cinco. Por ejemplo, una puntuación de 3 mostrará ★★★☆☆.

Para ello, vamos a crear un filtro de propósito general. Éste, por tanto, no recibirá un objeto como parámetro sino un valor. También, recibirá un parámetro adicional, que será el máximo de puntuación del producto. En este caso la puntuación máxima son cinco estrellas, pero de esta manera e filtro también nos valdrá para cuando puntuemos sobre diez.

Modificaremos nuestra plantilla para mostrar la puntuación debajo del importe:

1 2 3 4 5 6 7
<p> {{ product | productFullName }} <br/> {{ product.price }} &euro; <br/> {{ product.rating | rating:5 }} </p>

Para el ejercicio, pegaremos directamente el carácter de las estrellas, en lugar de emplear su código HTML. Si quisiéramos usar código html, tendríamos que hacer uso del servicio $sce y de la directiva ngBindHtml:

1 2 3 4 5 6 7
<p> {{ product | productFullName }} <br/> {{ product.price }} &euro; <br/> <span ng-bind-html="product.rating | rating:5"></span> </p>
1 2 3 4 5 6 7 8 9 10 11
.filter('rating', function($sce){ return function(rating,maxVal){ var result = ''; var solidStar = "&#9733;"; //★ var outlineStar = "&#9734;" //☆ //TODO: implement filter return $sce.trustAsHtml(result); }; })

1. http://codepen.io/alexsuch/pen/prqtn
2. http://codepen.io/alexsuch/pen/cnmlJ
3. Demo en http://codepen.io/alexsuch/pen/qJCpd
4. Demo en http://codepen.io/alexsuch/pen/olJnD