Cómo crear un filtro con checkboxes usando Angular Material

Última actualización: 22 noviembre, 2020

El otro día en el trabajo tuve que construir un filtro para filtrar (valga la redundancia) datos de una tabla y hacer que se muestren sólo los seleccionados. Algo así como el filtro que utiliza JIRA: 

filtersJira

Aunque en la documentación de Angular Material encontré cosas útiles, evidentemente no iba a encontrar una solución a medida. Así que la tuve que construir. Aquí te enseño cómo. ¡Vamos allá!

Qué vamos a construir

En este post vamos a construir los filtros desde el punto de vista de la UI. Es decir, nos vamos a centrar en la parte que viene antes de la integración con la tabla, no en la integración en sí.

Vamos a hacer dos filtros lo más parecidos a los de JIRA que ves arriba, usando Angular Material. Para hacer este ejemplo lo más sencillo posible, imaginemos que tenemos una tabla con información que recoge todas las películas que has visto en tu vida. Y queremos diseñar filtros para filtrar por:

  • actor principal
  • un rango de presupuesto que costó hacer la peli (es decir, entre XXX € y XXX €)

Cómo vamos a contruirlo

Nuestros filtros se van a basar en dos componentes de Angular Material: el expansion panel y la listVamos utilizar esos componentes de base y combinarlos con los siguientes aspectos:

  • clases de CSS dinámicas (ngClass) en conjunto con el tertiary operator y el or operator ( || )
  • local references para acceder a partes de nuestra template HTML
  • ngFor para generar una lista con datos ficticios (en inglés, dummy data)
  • ngIf para ocultar partes del DOM cuando nos convenga
  • ngModel para vincular nuestros datos (en inglés, data binding)

Construcción del filtro del actor principal

1. En tu proyecto de Angular ya arrancado, instalamos Angular Material y lanzamos el servidor desde la terminal con ng serve -o.

Vamos a empezar con una de las estructuras modelo que nos da Angular Material en la sección del Expansion Panel. Trabajaremos directamente en el app.component.html.


<mat-expansion-panel (opened)="actorPanelOpenState = true" (closed)="actorPanelOpenState = false"
    <mat-expansion-panel-header>
        <mat-panel-title>
            Self aware panel
        </mat-panel-title>
        <mat-panel-description>
            Currently I am {{panelOpenState ? 'open' : 'closed'}}
        </mat-panel-description>
    </mat-expansion-panel-header>
    <p>I'm visible because I am open</p>
</mat-expansion-panel>

El selector mat-expansion-panel viene con una serie de propiedades, como opened y closed, que son las que vamos a usar. Estas propiedades son eventos que se disparan cada vez que el expansion panel se abre y cierra, respectivamente. 

Angular Material nos proporciona esos eventos para que podamos reaccionar cada vez que el expansion panel se abre (evento opened) o se cierra (evento closed). Aquí está una de las claves. Pues al tener acceso a esos eventos, podemos configurar lo que queramos. Por ejemplo, podemos decirle al panel: "Cada vez que te cierres, comprueba si alguien ha añadido algo dentro del expansion panel, y de ser así, cambia de color". Veremos cómo hacer esto más adelante. 🙂

2. Importamos el MatExpansionModule y el MatSelectionList en el app.module.ts.

import { MatExpansionModule } from '@angular/material/expansion';
import { MatListModule } from '@angular/material/list';


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatExpansionModule,
    MatListModule
    

⚠️ Si ves que la terminal te da errores, es normal, pero ya deberías ver al menos el expansion panel en tu navegador. De no ser así, cancela el local server escribiendo ctrl+c en tu terminal y vuelve a lanzarlo. A mí me pasa a menudo y al principio me obsesionaba pensado qué podía estar fallando. Ahora no me complico. Ctrl+c cuando antes y problema resuelto. 🙋

3. Creamos la propiedad actorPanelOpenState en el app.component.ts.

export class AppComponent {
  actorPanelOpenState = false;
}

Su valor es false porque el estado inicial del panel es siempre cerrado (por lógica, aunque tú puedes necesitar que nada más mostrarse el panel, se muestre directamente abierto. Pero yo lo voy a dejar así).

4. Nos deshacemos del texto entre el mat-panel-description porque no lo vamos a necesitar.

        <mat-panel-description>
        </mat-panel-description>

5. En el app.component.css, creamos clases que posteriormente aplicaremos de manera dinámica. Vamos a crear clases de diferente anchura para mostrar el filtro cuando esté vacío y sólo se lea algo tipo:

Actor principal       🔽

y para cuando el filtro esté activo y haya algún elemento añadido (algún actor), tipo: 

Actor principal : Bradley Cooper, Jennifer Lawr... +3     🔽

Estas anchuras cumplen el objetivo:

.no-filter-width {
    width: 12vw;
}

.active-filter-width {
    width: 20vw;
}

6. Creamos un array con nombres de actores en el app.component.ts, que constituirá nuestra dummy data.

  actors: string[] = ['Bradley Cooper', 'Jennifer Lawrence', 'Penelope Cruz', 'Javier Bardem', 'Winona Ryder'];

7. Justo después del </mat-expansion-panel-header> del template, añadimos la lista de actores, utilizando un ngFor para mostrarla. Ya podemos deshacernos del <p> que venía con la plantilla y cambiarle el título al mat-panel-title.


        <mat-panel-title> Actor principal </mat-panel-title>
        <mat-panel-description>
        </mat-panel-description>
    </mat-expansion-panel-header>
    <mat-selection-list>
        <mat-list-option *ngFor="let actor of actors"> {{actor}} </mat-list-option>

Si guardas y vas a tu navegador, deberías ver un expansion panel con una lista de actores que se despliega al hacer clic. ¡Chachi 😄! El siguiente paso es cambiar de color el panel cuando haya algún actor seleccionado, para mostrarle al usuario fácilmente que se está aplicando un filtro.

Cómo cambiar el panel de color según haya algún filtro seleccionado

En el app.component.css añadimos unas clases que coloreen el texto de blanco y el fondo de verde cuando un filtro contenga algún actor seleccionado.

.active-filter-bg {
    background-color: teal !important;
}

.active-filter-text {
    color: white;
}

🙄 Si no añadimos el !important, Angular Material sustuirá nuestra clase al hacer hover, de ahí que usemos el !important para sobreescribir la intención de Angular Material.

Para averiguar cuándo una opción de la lista está seleccionada, plantamos una "bandera" en el mat-selection-list usando local references (en el app.component.html)La llamamos actorsList. Eso nos abre un maravilloso mundo de posibilidades. Por ejemplo, ahora podemos acceder a propiedades como selectedOptions. Como su nombre indica, es una propiedad que se aplica cuando hay alguna opción de la lista seleccionada

Esta propiedad nos ofrece otras como la propiedad selected, que identifica la opción que marquemos de la lista. Yendo un paso más allá, tenemos la propiedad length, que nos dice cuántas opciones de la lista hemos marcado. Para entender todo esto, creamos un <p> debajo de la mat-selection-list y aplicamos todas estas propiedades a nuestra local reference. 


    <mat-selection-list #actorsList>
        <mat-list-option *ngFor="let actor of actors"> {{actor}} </mat-list-option>
    </mat-selection-list>
    <p> {{actorsList.selectedOptions.selected.length}} </p>

¡Y ahí lo tenemos! Al final de la lista, cada vez que haces clic en un actor, el contador se activa, mostrando cuántos actores has seleccionado. Esto nos es tremendamente útil, porque, por ejemplo, podemos configurar el código para que compruebe si hay algún actor seleccionado, y de ser así, aplicar las clases active-filter-width, active-filter-bg active-filter-text.

Para eso utilizamos clases dinámicas con el ngClass, tanto en su versión normal como con el tertiary operator, algo que aprendí en profundidad en este curso de Angular.


<mat-expansion-panel (opened)="actorPanelOpenState = true" (closed)="actorPanelOpenState = false"
    [ngClass]="actorsList.selectedOptions.selected.length > 0 ? 'active-filter-width' : 'no-filter-width'">
    <mat-expansion-panel-header [ngClass]="{'active-filter-bg': actorsList.selectedOptions.selected.length > 0}">
        <mat-panel-title [ngClass]="{'active-filter-text': actorsList.selectedOptions.selected.length > 0}">
            Actor principal
        </mat-panel-title>
        <mat-panel-description [ngClass]="{'active-filter-text': actorsList.selectedOptions.selected.length > 0}">
        </mat-panel-description>

¡Y listo! Ahora si haces clic en el panel y seleccionas alguna opción, verás que el panel cambia de color siempre y cuando haya como mínimo una opción seleccionada. Eso es lo que hemos configurado al hacer que, de la lista de actores, compruebe si hay alguno seleccionado (es decir, si la length es mayor que 0). Espero que se entienda mi traducción de "código" a "humano" 😋.

filterActor

Cómo mostrar el filtro seleccionado

Vamos a añadirle el toque final a nuestro filtro, haciendo que se vea la opción seleccionada a continuación del título del filtro. De ser demasiadas las opciones, mostraremos unos puntos suspensivos y en cualquier caso, mostraremos la cantidad de opciones (actores) que hemos seleccionado.

1. Para eso, usamos una propiedad que viene por defecto: valueLa añadimos al mat-list-option y le damos el valor de la variable local actor. Con esta pequeña magia, ahora podemos obtener y mostrar el actor/es seleccionado/s.

Podemos hacer esto porque la variable local actor muestra, usando un loop, todos los elementos de un array. Por tanto, podemos acceder a ellos usando su indexPor ejemplo, si queremos mostrar los dos primeros, usamos los index [0] y [1]. Vamos a mostrarlos en el mat-panel-description.


        <mat-panel-description [ngClass]="{'active-filter-text': actorsList.selectedOptions.selected.length > 0}">
            {{ actorsList.selectedOptions.selected[0]?.value }}, {{ actorsList.selectedOptions.selected[1]?.value }}
        </mat-panel-description>
    </mat-expansion-panel-header>
    <mat-selection-list #actorsList>
        <mat-list-option *ngFor="let actor of actors" [value]="actor"> {{actor}} </mat-list-option>

2. Añadimos dos puntos cuando haya algún actor seleccionado, arreglamos la coma para que sólo se vea cuando haya más de un actor seleccionado y acortamos el texto para que salgan puntos suspensivos.


<mat-panel-title [ngClass]="{'active-filter-text': actorsList.selectedOptions.selected.length > 0}">
            <span>Actor principal</span>
            <span *ngIf="actorsList.selectedOptions.selected.length > 0">:</span>
        </mat-panel-title>
        <mat-panel-description [ngClass]="{'active-filter-text': actorsList.selectedOptions.selected.length > 0}"
            *ngIf="actorsList.selectedOptions.selected.length > 0">
            <span class="text-truncate">
                {{ actorsList.selectedOptions.selected[0]?.value }}
                <span *ngIf="actorsList.selectedOptions.selected.length > 1">,</span>
                {{ actorsList.selectedOptions.selected[1]?.value }}
            </span>

.text-truncate {
  width: 9vw;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

3. Echamos mano del contador que habíamos usado arriba, esta vez para añadirlo a continuación de los actores seleccionados para saber cuántos actores hemos seleccionado. Para eso, usamos la misma propiedad que antes (length), pero pidiéndole que nos muestre los valores seleccionados a partir de los nombres que ya vemos.

O sea, como máximo vamos a mostrar dos nombres y a partir del tercero ya mostraríamos el contador, que empezaría a contar desde +1.

Lo añadimos al final del mat-panel-description.

4. Nos deshacemos del contador que habíamos utilizado para pruebas (el <p>).

            <span *ngIf="actorsList.selectedOptions.selected.length > 2">
                +{{actorsList.selectedOptions.selected.length - 2}}
            </span>

¡Y voilà, nuestro filtro está listo! 👏

filtroActor

Construcción del filtro del rango de presupuesto

Vamos a seguir algunos de los patrones del filtro anterior y añadir cosas nuevas. Lo más destacable es que para este filtro vamos a usar la directiva ngModeldirectiva que estudié en profundad  en este curso de Angular.

1. Necesitaremos igualmente un mat-expansion-panel, con las propiedades opened closed, vinculadas a una nueva propiedad: budgetPanelOpenStateLa propiedad budgetPanelOpenState tiene el mismo objetivo que en el filtro del actor principal, esto es, establecer el estado inicial del panel, y, combinada con las propiedades opened closed, reaccionar cuando el panel se abra o se cierre.


<mat-expansion-panel (opened)="budgetPanelOpenState = true" (closed)="budgetPanelOpenState = false">
    <mat-expansion-panel-header>
        <mat-panel-title>
Budget range
        </mat-panel-title>
    </mat-expansion-panel-header>
</mat-expansion-panel>

2. Creamos la propiedad budgetPanelOpenState en el app.component.ts, y le asignamos el valor de false

3. Creamos también dos propiedades más: minAmount maxAmount. Nos servirán para establecer el rango de presupuesto que ha costado una película, con el objetivo de poder filtrar películas que hayan costado entre 1 millón y 10 millones de €, por ejemplo.

budgetPanelOpenState = false;

minAmount: number;
maxAmount: number;

4. Importamos los módulos FormsModule, MatInputModule MatIconModule en el app.module.ts.

import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { FormsModule } from '@angular/forms';


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatExpansionModule,
    MatListModule,
    MatInputModule,
    MatIconModule,
    FormsModule

5. Debajo del mat-expansion-header, creamos una sección donde irán dos inputs (el de minAmount y el de maxAmount).


    <div class="budget-range">
        <mat-form-field appearance="outline">
            <mat-label>Minimum</mat-label>
            <input type="number" matInput placeholder="1M">
            <mat-icon matPrefix>euro_symbol</mat-icon>
        </mat-form-field>
        <mat-form-field appearance="outline">
            <mat-label>Maximum</mat-label>
            <input type="number" matInput placeholder="10M">
            <mat-icon matPrefix>euro_symbol</mat-icon>
        </mat-form-field>
    </div>

6. Utilizamos el ngModel para vincular las propiedades minAmount maxAmount que hemos creado en el archivo TS. Las usamos sobre los inputs.


            <input type="number" matInput placeholder="1M" [(ngModel)]="minAmount">
            <mat-icon matPrefix>euro_symbol</mat-icon>
        </mat-form-field>
        <mat-form-field appearance="outline">
            <mat-label>Maximum</mat-label>
            <input type="number" matInput placeholder="10M" [(ngModel)]="maxAmount">

7. Mostramos el contenido de los inputs (cuando exista) en el mat-panel-title.


        <mat-panel-title>
            <span> Budget range</span>
            <span *ngIf="minAmount || maxAmount">:&nbsp;</span>
            <span *ngIf="minAmount">{{minAmount}}€</span>
            <span *ngIf="minAmount">&nbsp;-&nbsp;</span>
            <span *ngIf="maxAmount">{{maxAmount}}€</span>
        </mat-panel-title>

8. Utilizamos un tertiary operator para comprobar si el panel está abierto o si el usuario ha escrito alguna cantidad. De ser así, aplicamos la anchura adecuada.


<mat-expansion-panel (opened)="budgetPanelOpenState = true" (closed)="budgetPanelOpenState = false"
    [ngClass]="minAmount || maxAmount || budgetPanelOpenState === true ? 'active-filter-width' : 'no-filter-width' ">

9. Por último, aplicamos un color cuando haya alguna cantidad añadida, para destacar que el filtro está activo.


    <mat-expansion-panel-header [ngClass]="{'active-filter-bg': minAmount || maxAmount}">
        <mat-panel-title [ngClass]="{'active-filter-text': minAmount || maxAmount}">

¡Y ya lo tenemos! Ahora un poquito de chapa y pintura... 💅

.budget-range {
  display: flex;
  justify-content: space-between;
  margin-top: 20px;
}

.budget-range mat-form-field {
  width: 45%;
}

¡Y tacháááán! 🎊 🍾 🎈

budgetFilter

THE END!

¡Y hasta aquí el post de hoy! Espero que hayas aprendido algo nuevo 😊.  Si te queda alguna duda, nos vemos en los comentarios, porque me encantaría saber si tienes otra manera más eficiente de conseguir esto. Seguro que la hay, porque en programación hay mil maneras de llegar al mismo resultado. ¡El único límite es tu imaginación!

Sobre la autora de este post

Soy Rocío, una abogada reconvertida en programadora. Soy una apasionada de aprender cosas nuevas y ferviente defensora de que la única manera de ser feliz es alcanzando un equilibrio entre lo que te encanta hacer y lo que te saque de pobre. Mi historia completa, aquí. 

Si quieres ayudar a hacer este blog sostenible, puedes invitarme a un café digital ツ
¡Gracias!¡Gracias!

Más recursos de aprendizaje

En mi experiencia, la manera más eficaz para aprender Angular es combinando varias vías de aprendizaje. Uno de mis métodos favoritos son los vídeo-cursos y mi plataforma predilecta para eso es Udemy. He hecho varios cursos pero sólo recomiendo aquellos que verdaderamente me han sido útiles. Aquí van:

angular the complete guide - curso Max S.

  Max Schwarzmüller

curso angular fernando herrera

   Fernando Herrera

Si necesitas apoyo en forma de libro, puede que éstos te sirvan de ayuda:

libro 1 angular
libro 2 angular

La programación es un mundo que evoluciona a una velocidad de vértigo. Los autores de estos libros lo saben, por eso suelen encargarse de actualizar su contenido regularmente. Asegúrate de que así sea antes de adquirirlos 😌.

Participo en el programa de afiliados de Udemy y Amazon, lo que significa que, si compras alguno de estos cursos y/o libros, yo me llevaré una pequeña comisión y a ti no costará nada extra. Vamos, lo que se dice un win-win 😊.

Otros artículos que pueden interesarte

Guía de iniciación al data binding en Angular
¿Qué es el databinding?El databinding es la forma que tiene Angular para permitirnos mostrar contenido dinámico en lugar de estático (en inglés, hardcoded). Podríamos[...]
Días del 353 al 386
Objetivos versus realidad Y nuevamente, llegó otro día clave. Llegó…y pasó. El pasado 4 de marzo este Reto Computer Geek[...]
Angular: Entendiendo la Directiva ngModel
Angular es un framework que nos permite, entre otras cosas, añadir contenido dinámico a nuestros archivos HTML. Una de las formas[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Como toda web legal que se precie, utilizamos cookies para asegurar que damos la mejor experiencia al usuario en nuestro sitio web. Si continúas utilizando este sitio asumiremos que estás de acuerdo. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar