Carrito Reactivo con Angular y RxJS
¡Hola a todos! Hace rato vengo prometiendo como crear nuestro Carrito Reactivo con Angular y RxJS, así que aquí vamos.
Antes que nada, vamos a definir algunos términos.
Carrito rea... ¿¡Que!? ¿Qué es algo reactivo?
El término reactivo trata de algo que, mediante una acción, cambia y ese cambio es propagado por todo el sistema. Por ejemplo: Tengo un sitio web con un botón que envía cierto parámetro a una variable que es "escuchada" en distintas partes del sistema, al apretarlo, lo que va a pasar, es que va a cambiar esa variable y por consecuente en donde la escuchen también van a cambiar. Esto se lo llama programación reactiva. En este post en medium dan un ejemplo claro de este tipo de cambio.
Una definición mas formal podría ser: "La programación Reactiva es la programación orientada al manejo de streams de datos asíncronos y la propagación del cambio" (Ref: enmilocalfunciona)
Esto, a gran escala, es un poco molesto o tedioso de aplicar, por eso existe RxJS
¿Qué es RxJS?
RxJS es una librería de programación reactiva usando Observables. Realmente, es una adaptación de Reactivex , no es exclusiva de Angular ni de JS, es más se utiliza en PHP,.NET y un largo etc.
En nuestro caso lo vamos a utilizar para comunicar nuestro carrito con la lista de productos y viceversa.
Manos a la obra
Vamos a empezar a diseñar nuestra aplicación, cualquier cosa o error que tengan pueden verificarlo con el código completo que lo pueden buscar en mi Github o en su repositorio correspondiente.
1 - Creamos la estructura
Primero debemos generar nuestro proyecto, para eso abrimos la console y escribimos (cuando nos pregunte que hoja de estilo usamos ponemos scss):
ng new "shopping-cart"
Una vez que lo tengamos creado, nos paramos sobre la carpeta de nuestro proyecto y vamos a generar un componente para el carrito y otro para los productos
ng g c cart
ng g c product
También vamos a generar un servicio que más adelante voy a explicar para que lo usaremos:
ng g s services/cart
Por último vamos a crear una carpeta llamada interfaces, allí creamos nuestra interface item. Dentro colocamos el siguiente código:
export interface IItem {
id:number,
img:string,
name:string,
description:string,
price:number,
quantity:number
}
Esto lo vamos a utilizar como interface para nuestros items del carrito y los productos
2 - Diseñamos nuestra app
Vamos a diseñar el carrito para eso tomé este ejemplo ya hecho y lo modifiqué a mi gusto (no voy a explicar mucho del CSS ni HTML para no desviarnos del tema principal).
Lo que vamos a hacer primero para este diseño es agregar bootstrap por CDN (para diseñar fácil las cosas) dentro del index.html
Luego, vamos a empezar a modificar cada componente para darle el styling necesario.
app.component.html
<nav>
<div class="container">
<ul class="navbar-left">
<li><a href="#">Home</a></li>
<li><a href="#about">About</a></li>
</ul> <!--end navbar-left -->
<ul class="navbar-right">
<li><a href="#" id="cart" (click)="cart()"><i class="fa fa-shopping-cart"></i> Cart</a></li>
</ul> <!--end navbar-right -->
</div> <!--end container -->
</nav>
<app-cart *ngIf="openCart"></app-cart> <!--Nuestro componente carrito -->
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'shopping-cart';
public openCart:boolean = false;
public cart(){ //Se usa para abrir o cerrar el carrito
this.openCart = !this.openCart;
}
}
app.component.scss
$main-color: #6394F8;
$light-text: #ABB0BE;
@import url(https://fonts.googleapis.com/css?family=Lato:300,400,700);
@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css);
*, *:before, *:after {
box-sizing: border-box;
}
body {
font: 14px/22px "Lato", Arial, sans-serif;
background: #6394F8;
}
.lighter-text {
color: #ABB0BE;
}
.main-color-text {
color: $main-color;
}
nav {
padding: 20px 0 40px 0;
background: #F8F8F8;
font-size: 16px;
.navbar-left {
float: left;
}
.navbar-right {
float: right;
}
ul {
li {
display: inline;
padding-left: 20px;
a {
color: #777777;
text-decoration: none;
&:hover {
color: black;
}
}
}
}
}
.container {
margin: auto;
width: 80%;
}
Ahora modificamos el cart para darle estilo.
cart.component.html
<div class="container">
<div class="shopping-cart">
<div class="shopping-cart-header">
<i class="fa fa-shopping-cart cart-icon"></i><span class="badge">{{totalQuantity}}</span>
<div class="shopping-cart-total">
<span class="lighter-text">Total:</span>
<span class="main-color-text">${{totalPrice}}</span>
</div>
</div> <!--end shopping-cart-header -->
<ul class="shopping-cart-items">
<li class="clearfix" *ngFor="let item of items">
<img [src]="item.img" alt="item1" />
<span class="item-name">{{item.name}}</span>
<span class="item-price">${{item.price}}</span>
<span class="item-quantity">Quantity: {{item.quantity}}</span>
<button type="button" class="btn btn-danger ml-4" (click)="remove(item)">Remove</button>
</li>
</ul>
<a href="#" class="button">Checkout</a>
</div> <!--end shopping-cart -->
</div> <!--end container -->
cart.component.scss
$main-color: #6394F8;
$light-text: #ABB0BE;
@import url(https://fonts.googleapis.com/css?family=Lato:300,400,700);
@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css);
*, *:before, *:after {
box-sizing: border-box;
}
.container {
margin: auto;
width: auto;
}
.badge {
background-color: #6394F8;
border-radius: 10px;
color: white;
display: inline-block;
font-size: 12px;
line-height: 1;
padding: 3px 7px;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.shopping-cart {
background: white;
width: 320px;
position: relative;
border-radius: 3px;
padding: 20px;
.shopping-cart-header {
border-bottom: 1px solid #E8E8E8;
padding-bottom: 15px;
.shopping-cart-total {
float: right;
}
}
.shopping-cart-items {
padding-top: 20px;
li {
margin-bottom: 18px;
}
img {
float: left;
margin-right: 12px;
width: 100%;
}
.item-name {
display: block;
padding-top: 10px;
font-size: 16px;
}
.item-price {
color: $main-color;
margin-right: 8px;
}
.item-quantity {
color: $light-text;
}
}
}
.shopping-cart:after {
bottom: 100%;
left: 89%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-bottom-color: white;
border-width: 8px;
margin-left: -8px;
}
.cart-icon {
color: #515783;
font-size: 24px;
margin-right: 7px;
float: left;
}
.button {
background: $main-color;
color:white;
text-align: center;
padding: 12px;
text-decoration: none;
display: block;
border-radius: 3px;
font-size: 16px;
margin: 25px 0 15px 0;
&:hover {
background: lighten($main-color, 3%);
}
}
.clearfix:after {
content: "";
display: table;
clear: both;
}
// Agregué estos styles para que quede mejor
li {
list-style-type: none;
}
ul {
padding-inline-start: 0px;
}
.container{
position: absolute;
z-index: 1;
left: 63%;
}
cart.component.ts
import { Component, OnInit } from '@angular/core';
import { IItem } from '../interfaces/item.interface';
@Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.scss']
})
export class CartComponent implements OnInit {
public items: Array<IItem>
constructor() { }
ngOnInit() {
}
public remove(producto:IItem)
{
//Ya vamos a ver que hacemos acá
}
}
Con esto, deberíamos tener un carrito lindo a la vista, pero los botones no van a funcionar, ya que aún no hicimos las lógica.
Por último vamos a agregar el estilo al componente de productos y agregar unos productos mock para nosotros.
Agregamos la etiqueta de nuestro componente de productos a app.component.html
app.component.html
<!-- COSAS -->
<!-- MÁS COSAS -->
<!-- Y MÁS COSAS -->
<!-- SOBRE EL FINAL AGREGAMOS EL COMPONENTE PRODUCTO QUE VA A CONTENER LA LISTA DE PRODUCTOS-->
<app-product></app-product>
Ahora vamos al componente producto:
product.component.html
<div class="card-group">
<div class="card" *ngFor="let product of listProducts">
<img [src]="product.img" class="card-img-top" alt="arduino">
<div class="card-body">
<h5 class="card-title">{{product.name}}</h5>
<p class="card-text">{{product.description}}</p>
<p class="card-text"><small class="text-muted">Precio: ${{product.price}}</small></p>
<button type="button" class="btn btn-primary" (click)="addCart(product)">Añadir al carrito</button>
</div>
</div>
</div>
product.component.scss
img {
max-height: 290px;
}
product.component.ts
import { Component, OnInit } from '@angular/core';
import { IItem } from '../interfaces/item.interface';
import { CartService } from '../services/cart.service';
@Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {
//Creamos una lista de productos
public listProducts:Array<IItem> = [{
id: 0,
img: "https://i.blogs.es/d5526e/arduino-uno/450_1000.jpg",
name: "Arduino",
price: 500,
description: "Éste nuevo modelo Arduino UNO (rev3) es practicamente igual que su predecesor Duemilanove y 100% compatible pero incorpora ésta vez una autoselección del voltaje de alimentacion (DC/USB) gracias a un chip MOSFET incluido en la placa.",
quantity : 1
},
{
id: 1,
img: "https://electronilab.co/wp-content/uploads/2016/02/NodeMCU-%E2%80%93-Board-de-desarrollo-con-m%C3%B3dulo-ESP8266-WiFi-y-Lua-1.jpg",
name: "ESP8266 NodeMCU",
price: 350,
description: "El ESP8266 es un chip Wi-Fi de bajo coste que funciona mediante el protocolo TCP/IP. Incluye un microcontrolador (Tensilica Xtensa LX106) para manejar dicho protocolo y el software necesario para la conexión 802.11. Además la mayoría de modelos dispone de entradas/salidas digitales de propósito general (GPIO), así como una entrada analógica (ADC de 10bit).",
quantity : 1
},
{
id: 2,
img: "https://createc3d.com/shop/1244-thickbox_default/comprar-modulo-rele-5v-compatible-con-arduino-1-canal-precio-oferta.jpg",
name: "Modulo Relay Rele De 1 Canal 5v 10a Arduino Pic Avr Robotica",
price: 120,
description: "Módulo de relevadores (reles) para conmutación de cargas de potencia. Los contactos de los relevadores están diseñados para conmutar cargas de hasta 10 A y 250VAC (30VDC), aunque recomendamos dejar un márgen hacia abajo de estos límites. La señal de control puede provenir de cualquier circuito de control TTL o CMOS como un microcontrolador.",
quantity : 1
}]
constructor() { }
ngOnInit() {
}
public addCart(product:IItem)
{
//Ahora vemos que hacemos acá
}
}
Hasta ahora tenemos las funciones básicas, lo que se viene es ponerle RxJS. Vamos con eso entonces:
3 - Agreguemos RxJS
Antes de comenzar, debo explicar tres términos importantes la diferencia entre Observer y Observables, Subject y BehaviorSubject.
Diferencia entre Observer y Observable
La diferencia más importante es que el Observer esta todo el tiempo a la espera de cambios del Observable. Cuando nosotros nos subscribimos a un observable, esa subscripción es un Observer
Subject
Es un tipo de observable especial, que, una de sus funciones es permitirnos el multicasting. Esto quiere decir que podemos compartir nuestros datos entre todas las suscripciones a nuestro observable. (Esto tiene que ver con la propagación)
BehaviorSubject
Es un tipo de Subject que tiene la ventaja de poder recordar el ultimo valor emitido. Nos va a ser útil para el carrito de compras cuando necesitemos obtener la lista de items en él.
Para saber más les recomiendo este blog.
Vamos a empezar a implementarlos en nuestro código:
Primero vamos a modificar nuestro cart.service.ts
cart.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { IItem } from '../interfaces/item.interface';
@Injectable({
providedIn: 'root'
})
export class CartService {
private cart = new BehaviorSubject<Array<IItem>>(null); //Definimos nuestro BehaviorSubject, este debe tener un valor inicial siempre
public currentDataCart$ = this.cart.asObservable(); //Tenemos un observable con el valor actual del BehaviourSubject
constructor() { }
public changeCart(newData: IItem) {
//Obtenemos el valor actual
let listCart = this.cart.getValue();
//Si no es el primer item del carrito
if(listCart)
{
//Buscamos si ya cargamos ese item en el carrito
let objIndex = listCart.findIndex((obj => obj.id == newData.id));
//Si ya cargamos uno aumentamos su cantidad
if(objIndex != -1)
{
listCart[objIndex].quantity += 1;
}
//Si es el primer item de ese tipo lo agregamos derecho al carrito
else {
listCart.push(newData);
}
}
//Si es el primer elemento lo inicializamos
else {
listCart = [];
listCart.push(newData);
}
this.cart.next(listCart); //Enviamos el valor a todos los Observers que estan escuchando nuestro Observable
}
public removeElementCart(newData:IItem){
//Obtenemos el valor actual de carrito
let listCart = this.cart.getValue();
//Buscamos el item del carrito para eliminar
let objIndex = listCart.findIndex((obj => obj.id == newData.id));
if(objIndex != -1)
{
//Seteamos la cantidad en 1 (ya que los array se modifican los valores por referencia, si vovlemos a agregarlo la cantidad no se reiniciará)
listCart[objIndex].quantity = 1;
//Eliminamos el item del array del carrito
listCart.splice(objIndex,1);
}
this.cart.next(listCart); //Enviamos el valor a todos los Observers que estan escuchando nuestro Observable
}
}
Allí dentro explico paso por paso que hace el servicio.
En resumen, tenemos que tener en cuenta lo siguiente:
public changeCart(newData: IItem) {
//Esta función se encarga de recibir el item que debemos agregar al carrito, nos fijamos si ya existe aumentamos su cantidad, sino lo agregamos y volvemos a enviar el valor a todos los observers
}
public removeElementCart(newData:IItem){
//Con esta función removemos un elemento del carrito y volvemos a envíar la lista de esos elementos para que se propague entre los observer
}
Ahora nos queda agregar ese servicio donde lo usemos:
En cart.component.ts va a tener un observer escuchando los cambios que haya en el observable para así agregar o eliminar items.
Así nos quedaría el componente entero con el nuevo cambio:
cart.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { CartService } from '../services/cart.service';
import { IItem } from '../interfaces/item.interface';
@Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.scss']
})
export class CartComponent implements OnInit {
public items: Array<IItem>
public totalPrice:number = 0;
public totalQuantity:number = 0;
constructor(private _cartService:CartService) { }
ngOnInit() {
this._cartService.currentDataCart$.subscribe(x=>{
if(x)
{
this.items = x;
this.totalQuantity = x.length;
this.totalPrice = x.reduce((sum, current) => sum + (current.price * current.quantity), 0);
}
})
}
public remove(producto:IItem)
{
this._cartService.removeElementCart(producto);
}
}
Hay varias cosas acá:
Inyectamos nuestro servicio CartService
Nos subscribimos al currentDataCart que nos da el valor actual del carrito y alli dentro lo usamos para sacar la cantidad de items y los totales
Tenemos la función remove que elimina un item del carrito
Todo esto funciona reactivamente y se actualiza a medida que realizamos las acciones ya que siempre escuchamos sobre currentDataCart
Por ultimo, modificamos el componente producto para agregar los items al carrito:
product.component.ts
import { Component, OnInit } from '@angular/core';
import { IItem } from '../interfaces/item.interface';
import { CartService } from '../services/cart.service';
@Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {
public listProducts:Array<IItem> = [{
id: 0,
img: "https://i.blogs.es/d5526e/arduino-uno/450_1000.jpg",
name: "Arduino",
price: 500,
description: "Éste nuevo modelo Arduino UNO (rev3) es practicamente igual que su predecesor Duemilanove y 100% compatible pero incorpora ésta vez una autoselección del voltaje de alimentacion (DC/USB) gracias a un chip MOSFET incluido en la placa.",
quantity : 1
},
{
id: 1,
img: "https://electronilab.co/wp-content/uploads/2016/02/NodeMCU-%E2%80%93-Board-de-desarrollo-con-m%C3%B3dulo-ESP8266-WiFi-y-Lua-1.jpg",
name: "ESP8266 NodeMCU",
price: 350,
description: "El ESP8266 es un chip Wi-Fi de bajo coste que funciona mediante el protocolo TCP/IP. Incluye un microcontrolador (Tensilica Xtensa LX106) para manejar dicho protocolo y el software necesario para la conexión 802.11. Además la mayoría de modelos dispone de entradas/salidas digitales de propósito general (GPIO), así como una entrada analógica (ADC de 10bit).",
quantity : 1
},
{
id: 2,
img: "https://createc3d.com/shop/1244-thickbox_default/comprar-modulo-rele-5v-compatible-con-arduino-1-canal-precio-oferta.jpg",
name: "Modulo Relay Rele De 1 Canal 5v 10a Arduino Pic Avr Robotica",
price: 120,
description: "Módulo de relevadores (reles) para conmutación de cargas de potencia. Los contactos de los relevadores están diseñados para conmutar cargas de hasta 10 A y 250VAC (30VDC), aunque recomendamos dejar un márgen hacia abajo de estos límites. La señal de control puede provenir de cualquier circuito de control TTL o CMOS como un microcontrolador.",
quantity : 1
}]
constructor(private _cartService:CartService) { }
ngOnInit() {
}
public addCart(product:IItem)
{
this._cartService.changeCart(product);
}
}
También aca nos tenemos que fijar dos cosas
Inyectamos el servicio CartService.
El método addCart añade un producto al carrito, este cambio se propaga por toda la aplicación.
Bueno, con esto tendríamos funcionando nuestro Carrito Reactivo con Angular y RxJS, sé que es un poco extenso el tutorial pero intenté explicar y abarcar lo máximo posible.
Cualquier duda pueden comentar en este post o enviarme un email.
Carrito Reactivo con Angular y RxJS