Guía para principiantes de ngrx y angular

La gestión de estado es un término que siempre vendrá a la mente cuando se trate de una estructura de datos de aplicación. El mayor problema en el desarrollo y mantenimiento...

Introducción

La gestión de estado es un término que siempre vendrá a la mente cuando se trate de una estructura de datos de aplicación.

El mayor problema en el desarrollo y mantenimiento de sistemas de software a gran escala es la complejidad: los sistemas grandes son difíciles de entender.

La programación reactiva es cuando reaccionamos a los datos que se nos transmiten a lo largo del tiempo.

En este artículo, cubriremos los fundamentos de ngrx y sus aplicaciones en Angular.

Código fuente

El código fuente implementado en este artículo se puede encontrar en este repositorio de GitHub.

¿Qué es ngrx?

ngrx es un conjunto de bibliotecas de gestión de estado impulsadas por RxJS para Angular, inspiradas en [redux](https: //github.com/reactjs/redux), un contenedor de administración de estado popular y predecible para aplicaciones de JavaScript. Fue desarrollado por Rob Wormald, un defensor de desarrolladores de Angular en 2013.

Estas son algunas de las ventajas que nos trae ngrx:

  1. ngrx tiene como objetivo traer extensiones reactivas a Angular.
  2. @ngrx/store trae una tienda única similar a Redux para todos los estados de su aplicación en Angular.

ngrx/store es una implementación de Redux que se desarrolló con RxJS manteniendo los conceptos básicos y la API de Redux.

ngrx, inspirado en Redux, comparte los mismos principios con él y lo potencia con RxJS.

Entraremos en el funcionamiento interno de @ngrx/store en las secciones a continuación.

¿Qué es RxJS?

RxJS es una biblioteca de JavaScript para programación reactiva que le permite trabajar con flujos de datos asincrónicos o basados ​​en devolución de llamadas.

Hablando de flujos, un flujo es una secuencia de valores en el tiempo. Estos flujos de eventos y datos en tiempo real, que llamamos Observabless, son una hermosa manera de manejar el código asíncrono.

Usando RxJS, escribiríamos algo como esto:

1
2
3
4
5
6
7
var button = document.querySelector('button');

Rx.Observable.fromEvent(button, 'click')
    .subscribe(() => console.log('Clicked!'));

var arr = Rx.Observable.of(90, 80)
    .subscribe((v) => console.log('Value:', v));

Verá, con RxJS podemos lograr mucho con muy poco código.

¿Qué es Redux? {#lo que se reduce}

Redux, como se indicó anteriormente, es una biblioteca de administración de estado para aplicaciones de JavaScript. Aunque inicialmente se desarrolló para la comunidad React, también se puede usar en JavaScript estándar o con cualquier otro marco de JavaScript.

Redux es una biblioteca que implementa las ideas de Flux. Flux es un patrón de diseño que hizo popular el flujo de datos unidireccional (flujo unidireccional), que fue presentado por primera vez por Facebook.

Los estados de una aplicación en Redux se guardan en la tienda. Los estados se actualizan mediante acciones transportadas a funciones puras denominadas reductores. Los reductores toman el estado y la acción como parámetros y realizan una acción inmutable en el estado y devuelven un nuevo estado.

Conceptos básicos {#conceptos básicos}

Antes de sumergirnos en los aspectos prácticos de cómo/qué hace que @ngrx/store funcione, echemos un vistazo a los conceptos básicos. Las aplicaciones desarrolladas con @ngrx/store deben tratar con Store, Reducers, State y Actions.

Tienda

En pocas palabras, store es la "base de datos" de nuestra aplicación. Se compone de diferentes estados definidos en nuestra aplicación. El estado, por tanto, es inmutable y sólo alterado por las acciones.

La tienda combina todo el estado de la aplicación en una sola entidad, que actúa como una base de datos para la aplicación web. Al igual que una base de datos tradicional, representa el punto de registro para una aplicación, su tienda puede considerarse como una "única fuente de información" del lado del cliente.

Reductor

Si la tienda es la base de datos de la aplicación, los reductores son las tablas. Un reductor es una función pura que acepta dos parámetros: una acción y el estado anterior con un tipo y datos opcionales asociados con el evento.

Reductor de muestra
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export function reducer(state = initialState, action: articles.Actions):State {
    switch(action.type) {
        case 'ADD_ARTICLE':
            return { 
                articles: [...state.articles,action.payload]
            }
        default:
            return state;
    }
}

Estado

El estado es una única estructura de datos inmutable. Los estados son los que componen la tienda. Como se indicó anteriormente, los reductores son como tablas y, por lo tanto, el estado son campos en la tabla.

Acciones

La tienda abarca el estado de nuestra aplicación y los reductores obtienen las porciones o secciones del estado de la tienda, pero ¿cómo actualizamos la tienda cuando surge la necesidad? Ese es el papel de las acciones. Las acciones representan cargas útiles de información que se envían a la tienda desde la aplicación y, por lo general, se desencadenan por la interacción del usuario.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Action interface
export interface Action {
    type: string,
    payload?: any
}

// Action with payload
dispatch({type: 'ADD_ARTICLE', payload: {link: 'github.com/philipszdavido', points:90}})

// Action without payload
dispatch({type:'LOAD_LINKS'})

Cuando se envía una acción, el reductor la toma y aplica la carga útil, según el tipo de acción, y genera el nuevo estado.

Para recapitular algunos puntos: la tienda abarca todo el estado, los reductores devuelven fragmentos del estado y las acciones son eventos predefinidos activados por el usuario que comunican cómo debe cambiar un marco dado del estado.

Ventajas de la Tienda

Hemos visto cuán efectivo y útil es @ngrx/store para administrar estados en nuestra aplicación. Pero antes de pasar a mostrar su aplicación en Angular, vamos a echar un vistazo a sus ventajas.

Las principales ventajas que tiene la tienda son Estado centralizado, Pruebas, Rendimiento y DevTools.

  • Estado centralizado: el estado de una tienda se mantiene en un directorio. Hace que sea más fácil predecir actualizaciones o cambios en la tienda y rastrear problemas
  • Pruebas: Es fácil escribir pruebas para funciones puras. Dado que la tienda está compuesta por reductores, que son funciones puras que usan solo sus entradas para producir sus salidas sin efectos secundarios. Simplemente ingrese y confirme la salida.
  • Rendimiento: los cambios de estado de flujo de datos unidireccionales debido a su reactividad lo hacen muy rápido y eficiente.
  • DevTools: se han creado herramientas increíbles para ayudar a los desarrolladores. Un ejemplo es ngrx/tienda-devtool, que ayuda a los desarrolladores a "viajar en el tiempo" durante el desarrollo. También tiene algunas características interesantes que ayudan a proporcionar un historial de acciones y cambios de estado.

ngrx/store: Detrás de escena

@ngrx/store fue construido con los principios de RxJS. BehaviorSubject, Subject y Observable son tipos centrales de RxJS que componen el motor de @ngrx/store. Entendamos estos conceptos primero, luego podremos usar la biblioteca de manera efectiva.

Para entender muy bien un concepto, hay que mirar el código fuente. @ngrx/store fue, durante mucho tiempo, un misterio para mí hasta que me di cuenta cuando descargué el proyecto de su repositorio de Git y me sumergí en el código fuente. Pude ver cómo la biblioteca estaba brillantemente construida. Al hacerlo, realmente me familiaricé con las cosas.

Mirando el código, verá que @ngrx/store tiene cuatro clases principales Store, State, ActionsSubject y ReducerManager que hacen el trabajo principal dentro de la biblioteca.

  • Store es donde todo comienza, instancia otras clases. Extiende la clase Observable para que podamos suscribirnos a ella para obtener el estado más reciente.
  • ActionsSubject maneja el envío de acciones a Store.
  • State contiene el último valor de estado emitido.
  • ReducerManager contiene la función de reducción y llama a la función de reducción con el valor de estado de State y la acción de la clase ActionsSubject.

ngrx en la práctica

Ahora, es hora de mostrar cómo usar @ngrx/store en Angular. Para demostrar el poder de @ngrx/store, crearemos una "tienda en línea" simple que permitirá a los usuarios hacer lo siguiente:

  • Ver una lista de productos
  • Ver un producto en particular
  • Los usuarios pueden agregar un producto a su carrito
  • Los usuarios pueden eliminar un producto de su carrito

Aplicación de muestra

Usaremos angular/cli para configurar nuestro proyecto, que puede instalar ejecutando el comando:

1
$ npm install angular/cli -g

Aquí, instalamos angular/cli globalmente para que podamos usarlo desde cualquier directorio de nuestro sistema.

Configuración

Ahora estamos listos. Llamaremos a nuestra carpeta de proyecto, "tienda-online". Para montar el proyecto, ejecute el comando:

1
$ ng new online-store --minimal

Observe el uso de la bandera minimal, esto se usa para crear una aplicación Angular barebones. Genera archivos "spec", HTML y CSS. Todo estará en línea (dentro del archivo *.component.ts).

Ahora, nuestra estructura de directorios se verá así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
├── online-store
  ├── src
    ├── app
      ├── app.component.ts
      └── app.module.ts
    ├── assets
      └── .gitkeep
    ├── environment
      ├── environment.prod.ts
      └── environment.ts
    ├── index.html
    ├── main.ts
    ├── polyfills.ts
    ├── style.css
    ├── tsconfig.app.json
    └── typings.d.ts
  ├── .angular-cli.json
  ├── .gitignore
  ├── package.json
  └── tsconfig.json

Ahora, instalaremos Oreja para que nuestra aplicación sea receptiva y atractiva:

1
$ npm install bootstrap -S

Remane "style.css" a "style.scss", luego abra "style.scss" y agregue la siguiente línea:

1
@import "~bootstrap/scss/bootstrap.scss"

A continuación, extraemos la biblioteca @ngrx/store:

1
$ npm install @ngrx/store @ngrx/core -S

Nuestra aplicación tendrá tres componentes:

  • products.component: Esto mostrará listas de productos y sus precios.
  • cart.component: Este componente muestra todos los artículos que hemos añadido al carrito.
  • producto.componente: ​​Este componente mostrará el nombre y los detalles de un producto seleccionado.

Para montar los componentes anteriores, ejecute los siguientes comandos:

1
2
3
$ ng g c products --inline-style=true --spec=false
$ ng g c cart --inline-style=true --spec=false
$ ng g c product --inline-style=true --spec=false

Observe las opciones --inline-style=true --spec=false que pasamos al comando ng g c. La utilidad ng tiene una gran cantidad de opciones que se pueden usar en Angular para satisfacer sus necesidades.

Aquí, pasar --inline-style=true le dice a Angular que genere el estilo del componente dentro del archivo ts. --spec=true omite la generación del archivo de prueba *.spec.ts.

A continuación, vamos a agregar enrutamiento a nuestra aplicación. Crearemos tres rutas:

  • /productos: Esta será nuestra ruta índice. Activará el products.component.
  • /carrito: Esto activará el cart.component para mostrar el carrito del usuario
  • /producto/:id: esta ruta tiene un parámetro id que se usará para mostrar un producto en particular.

Para habilitar el enrutamiento en nuestra aplicación, debemos importar RouterModule y Routes desde @angular/router en app.module.ts:

1
2
3
4
5
6
7
// app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
...
import { Routes, RouterModule } from '@angular/router'
...

A continuación, definimos una variable rutas de tipo Rutas. Contendrá una matriz de nuestras rutas:

 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
// app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
...
import { Routes, RouterModule } from '@angular/router'
...
const routes: Routes = [
    {
        path: '',
        redirectTo: '/products',
        pathMatch: 'full'
    },
    {
        path: 'products',
        component: ProductsComponent
    },
    {
        path: 'cart',
        component: CartComponent
    },
    {
        path: 'product/:id',
        component: ProductComponent
    },
    {
        path: '**',
        redirectTo: '',
        pathMatch: 'full'
    }
];
...

Esto representa todos los estados posibles del enrutador en los que puede estar nuestra aplicación.

Como dijimos anteriormente, nuestra aplicación tiene tres rutas: "products", "products/:id" y "cart". Agregamos algunas configuraciones adicionales aquí:

  • ruta: '': Esto redirige a /products porque /products es nuestra página de índice. De hecho, podemos crear una página de índice "" simplemente agregando la propiedad del componente y asignándola a products.component. Tu decides.
  • ruta:'**': Esto redirigirá a la página /productos si ninguna de las rutas coincide con la solicitud del usuario.

Ahora, para activar el sistema de enrutamiento en nuestra aplicación, llamamos al método forRoot de RouterModule en la matriz imports, pasando la variable routes como parámetro.

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { ProductsComponent } from './products/products.component';
import { CartComponent } from './cart/cart.component';
import { ProductComponent } from './product/product.component';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
    {
        path: '',
        redirectTo: '/products',
        pathMatch: 'full'
    },
    {
        path: 'products',
        component: ProductsComponent
    },
    {
        path: 'cart',
        component: CartComponent
    },
    {
        path: 'product/:id',
        component: ProductComponent
    },
    {
        path: '**',
        redirectTo: '',
        pathMatch: 'full'
    }
];

@NgModule({
  declarations: [
    AppComponent,
    ProductsComponent,
    CartComponent,
    ProductComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes)
  ],
  providers: [],
  bootstrap: [AppComponent]
});

export class AppModule { }

Finalmente, debemos decirle al enrutador angular dónde puede colocar la configuración de enrutamiento de nuestra aplicación en el DOM.

Agregaremos el elemento <router-outlet></router-outlet> a la plantilla de AppComponent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
  <p>
    app works!
    <router-outlet></router-outlet>
  </p>
  `,
  styles: []
});

export class AppComponent {
  title = 'app';
};

El elemento <router-outlet></router-outlet> le dice al enrutador angular dónde insertar el componente coincidente en el DOM.

Definir nuestro Reductor

Ahora vamos a definir nuestra tienda y comenzar a agregarle reductores. Vamos a crear una carpeta central llamada "tienda" para todos nuestros archivos relacionados con la tienda:

1
$ mkdir src/app/store

Bien, ahora vamos a crear nuestro archivo reductor "reducer.ts":

1
$ touch src/app/store/reducer.ts

Este archivo contendrá nuestra función de reducción, a la que llegaremos más adelante. Como se mencionó anteriormente, algunas de las características de esta aplicación son manejar "eliminar un producto del carrito" y "agregar un producto al carrito". Por lo tanto, nuestro reductor se encargará de eliminar y agregar productos al carrito.

Volviendo a lo que decíamos antes, el archivo "reducer.ts" contendrá una función de reducción que aceptará el estado anterior y la acción enviada actualmente como parámetros. Luego, debemos implementar un sistema de cambio de caso para verificar la acción correcta y realizar un nuevo cálculo en el estado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/app/store/reducer.ts

import { CartActionTypes, CartActions } from "./actions";

export let initialState = []

export function reducer(state=initialState, action: CartActions) {
    switch (action.type) {
        case CartActionTypes.ADD_PRODUCT: 
            return [...state, action.payload]
        case CartActionTypes.REMOVE_PRODUCT: 
            let product = action.payload        
            return state.filter((el)=>el.id != product.id)
        default: 
            return state
    }
}

Así que aquí está nuestra función reductora. Tenga en cuenta que no queremos utilizar métodos de cambio de estado. La inmutabilidad es la clave aquí.

Estoy seguro de que te estás preguntando acerca de algunos de los objetos del código anterior, como CartActionTypes y CartActions. No se preocupe, llegaremos a eso en la sección "Configuración de acciones".

Inicializando la tienda

Actualmente solo tenemos un estado en esta aplicación, por lo tanto, solo un reductor 'getter'. Puede ver el ejercicio aquí, cada elemento de estado tiene su propia función reductora. En una aplicación grande y compleja, podemos tener muchos reductores, un reductor para cada elemento de estado en la aplicación. Estos reductores se combinarán en una sola función reductora mediante la función combineReducers.

Ahora pongamos nuestro reductor en la tienda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// app.module.ts

...
import { StoreModule } from "@ngrx/store";
import { reducer } from './store/reducer';

...

@NgModule({
  declarations: [
      ...
  ],
  imports: [
    ...
    StoreModule.forRoot({cart: reducer})
  ],
  providers: [], 
  bootstrap: [AppComponent]
});

export class AppModule { }

Aquí hemos hecho que la tienda de nuestra aplicación esté disponible para toda la aplicación. Podemos acceder a ella desde cualquier lugar. Mirando el código, importamos el StoreModule de @ngrx/store y nuestra función reducer del archivo src/app/store/reducer.ts. Luego, llamamos al método forRoot pasando {cart: reducer} como parámetro en la matriz imports.

Pasamos un objeto con la propiedad cart establecida porque como solo tenemos un estado, no necesitaremos considerar segmentos de estado. Así que solo le decimos a ngrx que solo nos envíe el estado cart.

Proyectando el Estado

Ahora que hemos hecho que nuestra tienda sea accesible, podemos acceder a uno de los estados usando la función select.

 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
// app.componen.ts

import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';

@Component({
  selector: 'app-root',
  template: `
  <p>
    app works !!
    Cart: {{cart.length}}
    <router-outlet></router-outlet>
  </p>
  `,
  styles: []
})

export class AppComponent {
  title = 'app';
  cart: Array<any>

  constructor(private store: Store<any>) {}

  ngOnInit() {
    // Called after the constructor, initializing input properties, and the first call to ngOnChanges.
    // Add 'implements OnInit' to the class.
    this.store.select('cart').subscribe((state => this.cart = state))
  }
}

La función select extrae el estado deseado de la aplicación y devuelve un Observable, para que podamos suscribirnos a los cambios realizados en ese estado.

Configuración de acciones

Las acciones son las consultas en la tienda de la aplicación. Ellos "despachan" las acciones que se realizarán en la tienda.

Primero, necesitaremos un modelo. La base de nuestro estado es una variedad de productos. El producto dentro de la matriz representa un artículo que un usuario puede comprar. El usuario puede agregarlo al carrito o eliminarlo.

Entonces, sabemos cómo nuestro carrito contiene productos/artículos, y un producto puede tener lo siguiente:

  • nombre
  • precio
  • etiqueta
  • identificación
  • descripción

Seleccionaremos solo name, id, price para representar un producto aquí.

A continuación, crearemos un modelo para nuestro producto:

1
2
3
4
5
6
7
// src/app/store/product.model.ts

export class Product {
    id: number
    name: string
    price: number
}

A continuación, definimos nuestras acciones como acciones personalizadas que implementan la clase @ngrx/store Action.

En lugar de enviar acciones como esta:

1
store.dispatch({type: '', payload: ''})

Creamos acciones como una nueva instancia de clase:

1
this.store.dispatch(new Cart.AddProduct(product))

Expresar acciones como clases permite la verificación de tipos en funciones reductoras. Para crear nuestras clases de acciones, primero creamos un enum que contendrá nuestros tipos de acciones. Recuerde, lo único que hace esta aplicación es "Agregar al carrito" y "Eliminar del carrito". Entonces nuestra enumeración se verá así:

1
2
3
4
5
6
// src/app/store/actions.ts

export enum CartActionTypes {
    ADD_PRODUCT = 'ADD_PRODUCT',
    REMOVE_PRODUCT = 'REMOVE_PRODUCT'
}

NB: primero deberá crear el archivo actions: touch src/app/store/actions.ts.

Ahora, defina las acciones implementando la interfaz Action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/app/store/actions.ts

import { Action } from '@ngrx/store'

...
export class AddProduct implements Action {
    readonly type = CartActionTypes.ADD_PRODUCT
    constructor(public payload: any){}
}

export class RemoveProduct implements Action {
    readonly type = CartActionTypes.REMOVE_PRODUCT
    constructor(public payload: any){}
}

Las acciones AddProduct, RemoveProduct implementan la interfaz Action, y debido a que necesitaremos agregar o eliminar un producto, agregamos un constructor para tomar el parámetro payload. Para que podamos pasar el producto al instanciar el objeto usando la palabra clave nuevo:

1
new Cart.AddProduct(product)

Por último, definiremos un alias tipo para todas las acciones definidas anteriormente para que se utilice en nuestra función de reducción:

1
2
3
4
// src/app/store/actions.ts

...
export type CartActions = AddProduct | RemoveProduct

Aquí está el código de acciones completo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// src/app/store/actions.ts

import { Action } from '@ngrx/store'

export enum CartActionTypes {
    ADD_PRODUCT = 'ADD_PRODUCT',
    REMOVE_PRODUCT = 'REMOVE_PRODUCT'
}

export class AddProduct implements Action {
    readonly type = CartActionTypes.ADD_PRODUCT
    constructor(public payload: any){}
}

export class RemoveProduct implements Action {
    readonly type = CartActionTypes.REMOVE_PRODUCT
    constructor(public payload: any){}
}

export type CartActions = AddProduct | RemoveProduct

Configuración de componentes

¡Creo que ya hemos terminado de configurar nuestra tienda! Ahora, veamos cómo utilizarlos en nuestros componentes.

Ya hemos creado todos los componentes que necesitaremos en nuestra aplicación.

productos.componentes.ts

Esto mostrará una lista de productos. Para este artículo, codificaremos nuestra lista de productos. Puede expandir esta aplicación para cargar los productos desde un recurso, pero eso se lo dejamos a usted.

Para codificar nuestra lista de productos, vamos a crear un archivo que contenga nuestra lista de productos. Crearemos un archivo market.ts:

1
$ touch src/app/store/market.ts

A continuación, inicializaremos una variable de matriz PRODUCTOS de tipo Producto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// src/app/store/market.ts

import { Product } from "./product.model";

export const PRODUCTS: Product[] = [
    {
      id: 0,
      name: "HP Inspirion",
      price: 700
    },
    {
      id: 1,
      name: "MacBook Pro 2018",
      price: 15000
    },
    {
      id: 2,
      name: "Dell 5500",
      price: 3000
    }
]

Ahora podemos importar los ‘PRODUCTOS’ en cualquier lugar que lo necesitemos.

Para mostrar la lista de productos en el archivo products.component vamos a importar PRODUCTS.

1
2
3
4
// src/app/products/products.component.ts

import { PRODUCTS } from "./../store/market";
...

A continuación, lo asignaremos a una variable de productos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// src/app/products/products.component.ts

...
export class ProductsComponent implements OnInit {

  products = PRODUCTS

  constructor() { }

  ngOnInit() { }
}

Crearemos un buen HTML para mostrar nuestra lista de productos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/app/products/products.component.ts

...
@Component({
  selector: 'app-products',
  template: `
        <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12" *ngFor="let product of products">
            <div class="my-list">
                <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                <h3>{{product.name}}</h3>
                <span>$</span>
                <span class="pull-right">{{product.price}}</span>
                <div class="offer">Extra 5% Off. Cart value $ {{0.5 * product.price}}</div>
                <div class="detail">
                    <p>{{product.name}} </p>
                    <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                    <a [routerLink]="['/product',product.id]" class="btn btn-info">View</a>
                </div>
            </div>    
  `,
  styles: [ ]
})
...

Usamos la directiva *ngFor para iterar a través de la matriz products y mostrarla en el HTML usando el enlace de expresión.

Reuniéndolo todo, src/app/products/products.componenet.ts se verá así:

 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
33
// src/app/products/products.componenet.ts

import { Component, OnInit } from '@angular/core';
import { PRODUCTS } from "./../store/market";

@Component({
  selector: 'app-products',
  template: `
        <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12" *ngFor="let product of products">
            <div class="my-list">
                <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                <h3>{{product.name}}</h3>
                <span>$</span>
                <span class="pull-right">{{product.price}}</span>
                <div class="offer">Extra 5% Off. Cart value $ {{0.5 * product.price}}</div>
                <div class="detail">
                    <p>{{product.name}} </p>
                    <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                    <a [routerLink]="['/product',product.id]" class="btn btn-info">View</a>
                </div>
            </div>    
  `,
  styles: [ ]
})

export class ProductsComponent implements OnInit {

  products = PRODUCTS

  constructor() { }

  ngOnInit() { }
}

A continuación, abra "src/app/styles.scss" y agregue el siguiente código SCSS:

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// src/app/styles.scss
...
img {
    max-width: 100%;
}

img {
    transition: all .5s ease;
    -moz-transition: all .5s ease;
    -webkit-transition: all .5s ease
}

.my-list {
    width: 100%;
    padding: 10px;
    border: 1px solid #f5efef;
    float: left;
    margin: 15px 0;
    border-radius: 5px;
    box-shadow: 2px 3px 0px #e4d8d8;
    position: relative;
    overflow: hidden;
}

.my-list h3 {
    text-align: left;
    font-size: 14px;
    font-weight: 500;
    line-height: 21px;
    margin: 0px;
    padding: 0px;
    border-bottom: 1px solid #ccc4c4;
    margin-bottom: 5px;
    padding-bottom: 5px;
}

.my-list span {
    float: left;
    font-weight: bold;
}

.my-list span:last-child {
    float: right;
}

.my-list .offer {
    width: 100%;
    float: left;
    margin: 5px 0;
    border-top: 1px solid #ccc4c4;
    margin-top: 5px;
    padding-top: 5px;
    color: #afadad;
}

.detail {
    position: absolute;
    top: -100%;
    left: 0;
    text-align: center;
    background: #fff;
    height: 100%;
    width: 100%;
}

.my-list:hover .detail {
    top: 0;
}

Al hacer esto, el código SCSS estará disponible para todos nuestros componentes.

Lista de aplicaciones de productos{.img-responsive}

producto.componente.ts

Aquí podemos ver un producto, ver su precio, nombre y luego agregarlo al carrito si nos atrae.

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// src/app/product/product.component.ts

import { Component, OnInit } from '@angular/core';
import { PRODUCTS } from "./../store/market";
import { Product } from "./../store/product.model"
import { ActivatedRoute } from "@angular/router";
import { Store } from "@ngrx/store";
import * as Cart from "./../store/actions";

@Component({
  selector: 'app-product',
  template: 
  `
    <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
      <div class="my-list">
          <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
          <h3>{{product.name}}</h3>
          <span>$</span>
          <span class="pull-right">{{product.price}}</span>
          <div class="offer">
            Extra 5% Off. Cart value $ {{0.5 * product.price}}
          </div>
          <div class="offer">
            <a (click)="addToCart(product)" class="btn btn-info">Add To Cart</a>
          </div>
      </div>
    </div>
  `,
  styles: [ ]
})

export class ProductComponent implements OnInit {

  product:Product

  constructor(private route: ActivatedRoute, private store: Store<any>) { }

  ngOnInit() {
    this.route.params.subscribe((p)=>{
        let id = p['id']
        let result = Array.prototype.filter.call(PRODUCTS,(v)=>v.id == id)
        if (result.length > 0) {
          this.product = result[0]
        }
    })
  }

  addToCart(product) {
        this.store.dispatch(new Cart.AddProduct(product))
  }
}

Aquí hemos importado ActivatedRoute para obtener los parámetros id. Se suscribe al flujo de eventos de la ruta y luego los “filtra” a través de la matriz “PRODUCTOS” para obtener el “id” del parámetro coincidente en la matriz.

Importamos Store para enviar la acción ADD_PRODUCT cuando se ejecuta el método addToCart.

App ver producto{.img-responsive}

carrito.componente.ts

Esto muestra los productos en nuestro carrito. Aquí está el código:

 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
33
34
35
36
37
38
39
40
// src/app/cart/cart.component.ts

import { Component, OnInit } from '@angular/core';
import { Store, select } from "@ngrx/store";
import { Observable } from "rxjs/Observable";
import * as Cart from "./../store/actions";

@Component({
  selector: 'app-cart',
  template: 
  `
    <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" *ngFor="let product of cart | async">
      <div class="my-list">
          <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
          <h3>{{product.name}}</h3>
          <span>$</span>
          <span class="pull-right">{{product.price}}</span>
          <div class="offer">
            Extra 5% Off. Cart value $ {{0.5 * product.price}}
            <a (click)="removeFromCart(product)" class="btn btn-info">Remove From Cart</a>
          </div>
      </div>
    </div>
  `,
  styles: []
})

export class CartComponent implements OnInit {

  cart: Observable<Array<any>>
  constructor(private store:Store<any>) { 
    this.cart = this.store.select('cart')
  }

  ngOnInit() { }

  removeFromCart(product) {
    this.store.dispatch(new Cart.RemoveProduct(product))
  }
}

Declaramos una variable cart de tipo Observable. Luego, seleccionamos-ed el estado carrito de la tienda. El método select devuelve un Observable que asignamos a la variable cart previamente declarada. El valor del carrito se suscribe y se recibe mediante AsyncPipe |. Existe el método removeFromCart que envía la acción REMOVE_PRODUCT a la tienda.

Carrito de la aplicación{.img-responsive}

app.component.ts

Este es nuestro componente final:

 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
33
34
35
// src/app/app.component.ts

import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';

@Component({
  selector: 'app-root',
  template: `
  <div class="container">
    <div class="row">
      <div class="col-sm-12">
        <h1 class="text-center">Online Store</h1>
        <h6 class="text-center"><a [routerLink]="['/cart']">Cart: {{(cart | async).length}}</a></h6>
        <hr />
      </div>
    </div>
    <router-outlet></router-outlet>
  </div>
  `,
  styles: []
})

export class AppComponent {
  title = 'app';
  constructor(private store: Store<any>) {}

  cart: Observable<Array<any>>

  ngOnInit() {
    // Called after the constructor, initializing input properties, and the first call to ngOnChanges.
    // Add 'implements OnInit' to the class.
    this.cart = this.store.select('cart')
  }
}

Verá que hemos modificado la plantilla para incluir el nombre de nuestra aplicación "Tienda en línea". Nos suscribimos a la tienda del carrito para obtener la cantidad de productos, luego se mostró usando la expresión {{(cart | async).length}}. Se actualiza en tiempo real cuando agregamos o eliminamos un producto de la tienda carrito.

Usted ve el poder de RxJs en juego aquí en el flujo de datos unidireccional.

Para ver todo lo que hemos hecho, asegúrese de guardar todos los archivos y ejecutar el siguiente comando en su terminal:

1
$ ng serve

¡Voila! Ahora puedes jugar con la aplicación.

Uso de AsyncPipe

Tubería asíncrona es una tubería incorporada que podemos usar dentro de nuestras plantillas para desenvolver datos de Promises u Observables.

La canalización asíncrona, cuando se usa en un componente, lo marca para verificar si hay cambios.

Mirando el componente donde hicimos esto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// src/app/app.component.ts

...
@Component({
  selector: 'app-root',
  template: `
...
        <h6 class="text-center"><a [routerLink]="['/cart']">Cart: {{(cart | async).length}}</a></h6>
...
  `,
  ...
})
...

Podríamos haber usado el método subscribe para lograr el mismo resultado:

 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
33
// src/app/app.component.ts

...
@Component({
  selector: 'app-root',
  template: `
  <div class="container">
    <div class="row">
      <div class="col-sm-12">
        <h1 class="text-center">Online Store</h1>
        <h6 class="text-center"><a [routerLink]="['/cart']">Cart: {{cart.length}}</a></h6>
        <hr />
      </div>
    </div>
    <router-outlet></router-outlet>
  </div>
  `,
  styles: []
})

export class AppComponent {
  title = 'app';
  constructor(private store: Store<any>) {}

  cart: Array<any>

  ngOnInit() {
    // Called after the constructor, initializing input properties, and the first call to ngOnChanges.
    // Add 'implements OnInit' to the class.
    this.cart = this.store.select('cart')
        .subscribe(state => this.cart = state)
  }
}

Puede ver que nuestro código se hizo un poco más largo, ¡pero aún funcionaba!

Entonces, la "tubería asíncrona" realiza la suscripción por nosotros y agrega el valor a nuestra plantilla. Tanto trabajo en solo unas pocas líneas de código. ¡Perfecto!

Depuración con Redux-DevTools

Redux-Devtools es una herramienta de depuración de "viajes en el tiempo" para probar los estados de la interfaz de usuario. Facilita el desarrollo de aplicaciones y aumenta la productividad de su desarrollo.

Con estas herramientas, puedes moverte literalmente al futuro o al estado pasado de tu aplicación (de ahí la descripción de "viaje en el tiempo").

Las funciones de "bloqueo" y "pausa" de Redux-DevTools hacen posible eliminar acciones pasadas del historial o desactivarlas.

Podemos usar Redux-Devtools en una aplicación Angular, pero el equipo de ngrx desarrolló su propia herramienta de desarrollo para usar en cualquier aplicación con tecnología de ngrx.

Se puede instalar ejecutando el siguiente comando:

1
$ npm i @ngrx/store-devtools -S

Aquí está el enlace para descargar la extensión Redux-DevTools.

Importe StoreDevtoolsModule.instrumentOnlyWithExtension() en su app.module.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { StoreDevtoolsModule } from '@ngrx/store-devtools'

@NgModule({
    imports: [
        StoreDevtoolsModule.instrumentOnlyWithExtension({
            maxAge: 6
        })
    ]
})

export AppModule() {}

No entraremos en detalles técnicos sobre cómo utilizar Redux-DevTools. En su lugar, puede tomarlo como una tarea para mejorar agregándolo a este proyecto, ¡me gustaría saber de usted! ¡No dudes en enviarme un PR!

Recursos

¿Desea obtener más información sobre Angular, TypeScript y otras bibliotecas front-end útiles? Recomendaría consultar algunos recursos más detallados, como un curso en línea:

Conclusión

Lo sé, violamos varias mejores prácticas, pero lo más importante es que ha visto cómo usar @ngrx/store para crear aplicaciones Angular.

Si te perdiste o necesitas una referencia al código, puedes bifurcarlo o clonarlo desde mi repositorio de GitHub. Puedes expandir la aplicación para hacer cosas en las que no pensé. Siéntete libre, juega con él.