Actividad entregable
| Sitio: | Campus virtual DAW - damiansu |
| Curso: | Desarrollo Web en Entorno Cliente |
| Libro: | Actividad entregable |
| Imprimido por: | Invitado |
| Día: | jueves, 22 de enero de 2026, 05:06 |
Tabla de contenidos
- 1. ¿Qué trabajamos?
- 2. Creando el proyecto Angular
- 3. ProductService + Primera llamada a la API
- 4. ProductsListComponent + @for
- 5. ProductCardComponent (Tarjetas + Botón Eliminar)
- 6. ProductFormComponent (Formulario para añadir productos)
- 7. Alta y Eliminación de Productos
- 8. ProductFilterComponent (Filtrado de productos en tiempo real)
- 9. Proyecto final
1. ¿Qué trabajamos?
En esta actividad vamos a construir una aplicación completa en Angular 18
Crearemos una SPA para gestionar productos: podremos mostrar, filtrar, añadir y eliminar productos, utilizando la arquitectura moderna de Angular.
Los componentes principales que vamos a desarrollar serán:
- ProductService (servicio para gestionar los datos)
- ProductsListComponent (lista de productos)
- ProductCardComponent (tarjeta individual)
- ProductFormComponent (formulario para crear)
- ProductFilterComponent (panel de filtros)
Vamos a trabajar con datos reales que vienen de una API pública, concretamente de una URL de NPoint:
https://api.npoint.io/1dee63ad8437c82b24fe
Esta API es solo de lectura, no permite crear ni borrar datos en el servidor.
Más adelante veremos cómo solucionamos esto, y por qué tendremos que usar una pequeña ‘trampilla’ para generar identificadores únicos (UUID).
Otro requisito de la actividad es que la maquetación debe hacerse con Bootstrap, así que toda la interfaz —formularios, tarjetas, botones, estructura— la haremos usando clases de Bootstrap.
La actividad también indica que se evaluarán mejoras opcionales.
Eso significa que quien quiera añadir una interfaz más cuidada, validaciones extra o funcionalidades adicionales, podrá hacerlo y obtendrá más puntuación.
Aquí solo vamos a ver la estructura general del proyecto.
2. Creando el proyecto Angular
Vamos a preparar nuestro proyecto de Angular 18 para toda la actividad:
- Crear el proyecto Angular
- Instalar Bootstrap
- Activar HttpClient con la forma moderna de Angular
- Añadir una cabecera y un footer, que son obligatorios en la actividad
2.1. Crear el proyecto Angular
ng new gestion-productos --standalone --skip-tests
Opciones:
-
Routing → no
-
CSS → CSS
Entramos en la carpeta y arrancamos:
cd gestion-productos
ng serve
Mostrar en navegador:
http://localhost:42002.2. Configurar HttpClient
Aquí estamos aplicando el Tema 10, habilitando HttpClient con la API moderna de Angular.
Ya no se usa HttpClientModule ni módulos, sólo provideHttpClient().
A partir de ahora podremos hacer peticiones GET, POST, DELETE a cualquier API.
Abre src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient()
]
};
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient()
]
};2.3. Instalar Bootstrap para diseño
npm install bootstrap@5
Luego abrimos angular.json y en "styles" añadimos:
"styles": [
"bootstrap/dist/css/bootstrap.min.css",
"src/styles.css"
]
“Con esto podemos usar clases como contenedores, filas, botones, tarjetas… que nos permitirá maquetar rápido los componentes visuales.”
2.4. Limpiar AppComponent y probar Bootstrap
Abre src/app/app.ts y deja
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
templateUrl: './app.html'
})
export class AppComponent {}
Abre src/app/app.html y comprobamos que funciona el boostrap
<h1 class="text-center text-primary">Gestión de Productos</h1>
<h2 class="text-center text-primary mt-4">Bootstrap funcionando</h2>
Si el texto aparece centrado y azul, Bootstrap está funcionando.
2.5. Header y footer
Vamos a retocar el app.html e incluiremos el header y el footer, para ello vamos a borrar todo lo que habíamos puesto en el app.html y lo vamos a sustituir por este código que hay header y footer:
<!-- CABECERA -->
<header class="bg-dark text-white py-3 mb-4">
<div class="container d-flex justify-content-between align-items-center">
<h1 class="h3 m-0">Gestión de Productos</h1>
<nav>
<a href="#" class="text-white text-decoration-none me-3">Inicio</a>
<a href="#" class="text-white text-decoration-none">Contacto</a>
</nav>
</div>
</header>
<!-- CONTENIDO PRINCIPAL -->
<main class="container mb-5">
<p class="text-muted text-center">
Proyecto iniciado
</p>
</main>
<!-- FOOTER -->
<footer class="bg-light py-3 text-center border-top mt-5">
<p class="m-0 text-muted">
© 2025 · Actividad DWEC · Angular 18
</p>
</footer>
Las etiquetas <header> y <footer> son HTML normal. Lo que aporta Bootstrap son las clases: bg-dark, container, d-flex, py-3, etc. Este header y footer serán la estructura fija de nuestra SPA.
3. ProductService + Primera llamada a la API
Aquí crearemos el ProductService, conectaremos con la API real, y dejaremos lista la carga inicial de productos en la aplicación (Tema 10)
En este vídeo vamos a crear el servicio ProductService, activar HttpClient en Angular 18 y hacer nuestra primera llamada a la API de productos.
Al final del vídeo veremos los datos de la API en la consola del navegador.
3.1. Crear el servicio ProductService
Creamos un servicio en la carpeta services llamado product.
Angular me genera el archivo product.service.ts dentro de src/app/services
ng generate service services/product --skip-tests
Definir el modelo de datos: interfaz Product
Definimos una interfaz Product que representa la estructura de los productos tal y como vienen de la API.
No es una clase, es una interface: solo sirve para describir datos, no se instancia con new.
Esto ayuda a TypeScript a comprobar tipos y a HttpClient a saber qué esperamos recibir
Abrimos src/app/services/product.service.ts y encima de la clase ProductService, añade:
export interface Product {
_id: string;
name: string;
description: string;
price: number;
category: string;
image: string;
active: boolean;
}
Inyectar HttpClient
Importamos HttpClient y lo inyectamos en el constructor.
Gracias a que en el vídeo anterior configuramos provideHttpClient() en app.config.ts, Angular ya sabe cómo crear este HttpClient.
import { HttpClient } from '@angular/common/http';
Y en la clase:
@Injectable({
providedIn: 'root'
})
export class ProductService {
private url = 'https://api.npoint.io/1dee63ad8437c82b24fe';
constructor(private http: HttpClient) { }
}
He subido los datos de la API en este enlace:
https://api.npoint.io/1dee63ad8437c82b24fe
Crear el método cargarProductos()
Creamos un método muy sencillo llamado cargarProductos().
Este método hace una petición GET a la URL de la API y devuelve un Observable de tipo Product[], es decir, un array de productos.
De momento no vamos a hacer nada más avanzado: simplemente queremos comprobar que Angular se conecta correctamente a la API y recibe los datos
En la clase ProductService, debajo del constructor, añadimos:
cargarProductos() {
return this.http.get<Product[]>(this.url);
}
cargarProductos() {
return this.http.get<Product[]>(this.url);
}
3.2. Servicio completo
Este es nuestro ProductService en su versión simple: Define el tipo de datos, tiene la URL de la API, inyecta HttpClient y expone un método para cargar productos.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export interface Product {
_id: string;
name: string;
description: string;
price: number;
category: string;
image: string;
active: boolean;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private url = 'https://api.npoint.io/1dee63ad8437c82b24fe';
constructor(private http: HttpClient) {}
cargarProductos() {
return this.http.get<Product[]>(this.url);
}
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export interface Product {
_id: string;
name: string;
description: string;
price: number;
category: string;
image: string;
active: boolean;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private url = 'https://api.npoint.io/1dee63ad8437c82b24fe';
constructor(private http: HttpClient) {}
cargarProductos() {
return this.http.get<Product[]>(this.url);
}
}3.3. Probar el servicio
De momento, vamos a probar que el servicio funciona haciendo una llamada en el AppComponent y sacando los datos por consola.
Llamamos a cargarProductos(), nos suscribimos al observable y mostramos el resultado con console.log
Ahora abrimos src/app/app.ts y añadimos el servicio.
import { Component, signal } from '@angular/core';
import { ProductService,Product } from './services/product';
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
protected readonly title = signal('gestion-producto');
constructor (private productService: ProductService){
this.productService.cargarProductos().subscribe(
(datos: Product[]) => {
console.log('Productos cargados desde la API:', datos);
}
);
}
}
Guardamos los cambios, nos aseguramos de que ng serve está en marcha y vamos al navegador.
Abrimos las herramientas de desarrollo:
– En Chrome: F12 o Ctrl + Shift + I (en Mac, Cmd + Option + I)
– Entramos en la pestaña Console.
Veremos un mensaje:
Productos cargados desde la API: (10) [{...}, {...}, ...]
Eso significa que Angular se ha conectado correctamente a la API y está recibiendo los productos.
4. ProductsListComponent + @for
Vamos a crear el componente ProductsListComponent, el encargado de mostrar en pantalla la lista de productos que estamos trayendo desde la API.
Aquí trabajamos el Tema 6, utilizando la sintaxis moderna @for de Angular, y el Tema 8, porque creamos un componente standalone que usa un servicio
4.1. Inyectar ProductService y obtener productos
Creamos el componente dentro de la carpeta components, Lo hacemos standalone y sin tests
ng generate component components/products-list --standalone --skip-tests
Inyectamos el servicio y llamamos a cargarProductos(), como hicimos anteriormente
Este método devuelve un observable, así que nos suscribimos y guardamos los productos en un array local llamado productos.
Este array será el que usaremos en la plantilla para mostrarlos.
import { Component } from '@angular/core';
import { ProductService, Product } from '../../services/product';
@Component({
selector: 'app-products-list',
imports: [],
templateUrl: './products-list.html',
styleUrl: './products-list.css',
})
export class ProductsList {
productos: Product[] = [];
constructor(private productService: ProductService) {
this.productService.cargarProductos().subscribe(datos => {
this.productos = datos;
console.log('Productos recibidos:', datos);
});
}
}4.2. Mostrar productos usando @for
for es la nueva sintaxis moderna de Angular para recorrer listas.
- product of productos recorre la lista
- track product._id mejora el rendimiento
- @empty se ejecuta si la lista está vacía
Abre products-list.component.html
<h2 class="fw-semibold mb-3">Listado de Productos</h2>
<ul class="list-group">
@for (product of productos; track product._id) {
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
<strong>{{ product.name }}</strong>
<span class="text-muted">({{ product.category }})</span>
</span>
<span class="fw-bold text-primary">{{ product.price }} €</span>
</li>
}
@empty {
<li class="list-group-item text-muted">
No hay productos para mostrar…
</li>
}
</ul>
4.3. Añadir ProductsListComponent al App
Colocamos el componente directamente en el AppComponent.
Abrimos app.ts y añade:
import { ProductsListComponent } from './components/products-list/products-list.component';
imports: [CommonModule, ProductsListComponent]
El archivo que tenemos es este:
import { Component, signal } from '@angular/core';
import { ProductService, Product } from './services/product';
import { ProductsList } from './components/products-list/products-list';
@Component({
selector: 'app-root',
imports: [ProductsList],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
protected readonly title = signal('gestion-productos');
productos: Product[] = [];
constructor(private productService: ProductService) {
this.productService.cargarProductos().subscribe(datos => {
console.log('Productos cargados:', datos);
});
}
}
Y en app.html, dentro del <main>
<main class="container mb-5">
<app-products-list></app-products-list>
</main>
Ahora sí, la lista de productos que viene de la API aparecerá en pantalla.
El archivo app.html que nos queda es este:
<!-- CABECERA -->
<header class="bg-dark text-white py-3 mb-4">
<div class="container d-flex justify-content-between align-items-center">
<h1 class="h3 m-0">Gestión de Productos</h1>
<nav>
<a href="#" class="text-white text-decoration-none me-3">Inicio</a>
<a href="#" class="text-white text-decoration-none">Contacto</a>
</nav>
</div>
</header>
<!-- CONTENIDO PRINCIPAL -->
<main class="container mb-5">
<app-products-list></app-products-list>
</main>
<!-- FOOTER -->
<footer class="bg-light py-3 text-center border-top mt-5">
<p class="m-0 text-muted">
© 2025 · Actividad DWEC · Angular 18
</p>
</footer>5. ProductCardComponent (Tarjetas + Botón Eliminar)
Vamos a crear el componente ProductCardComponent, encargado de mostrar la información visual de cada producto en forma de tarjeta.
Según la actividad, desde este componente se debe poder eliminar el producto de la lista. Ahora solo vamos a colocar el botón de eliminar, pero sin implementar la lógica aún.
La eliminación real la haremos más adelante, cuando tengamos preparado el estado en el servicio.
5.1. Crear el componente
Creamos el componente tarjeta-producto dentro de la carpeta componentes.
Será un componente independiente (standalone).
ng generate component components/product-card --standalone --skip-tests
Este componente no carga datos.
Solo recibe un producto mediante @Input(), siguiendo el patrón padre → hijo
Abre product-card.component.ts y déjalo así:
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Product } from '../../services/product.service';
@Component({
selector: 'app-product-card',
standalone: true,
imports: [CommonModule],
templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
@Input() product!: Product; // Recibe un producto desde el padre
}
@Input() nos permite recibir el producto desde el componente padre.
@Output() nos permite emitir eventos hacia arriba, en este caso el ID del producto que queremos eliminar
Abre: tarjeta-producto.component.html
Este template usa Bootstrap para mostrar una tarjeta limpia.
El botón ‘Eliminar’ lanza el método onEliminar(), que subirá un evento al componente padre
<div class="card h-100 shadow-sm border-0">
<img
[src]="producto.image"
class="card-img-top object-fit-cover"
alt="{{ producto.name }}"
style="height: 170px;"
>
<div class="card-body">
<h5 class="fw-semibold">{{ producto.name }}</h5>
<p class="text-muted small mb-2">
{{ producto.description }}
</p>
<p class="fw-bold text-primary">
{{ producto.price | currency:'EUR' }}
</p>
<div class="d-flex gap-2 mb-2">
<span class="badge bg-dark">{{ producto.category }}</span>
<span class="badge"
[class.bg-success]="producto.active"
[class.bg-danger]="!producto.active">
{{ producto.active ? 'Activo' : 'Inactivo' }}
</span>
</div>
</div>
<div class="card-footer bg-white border-0 text-end">
<button class="btn btn-sm btn-outline-danger" (click)="onEliminar()">
Eliminar
</button>
</div>
</div>
Abre: lista-productos.component.ts
Añade la importación:
import { TarjetaProductoComponent } from '../tarjeta-producto/tarjeta-producto.component';
Actualiza el decorador:
imports: [CommonModule, TarjetaProductoComponent],
Ahora edita el HTML (lista-productos.component.html):
Cambia cada tarjeta por el componente:
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
<app-tarjeta-producto
[producto]="producto"
(eliminar)="onEliminar($event)">
</app-tarjeta-producto>
</div>
Y en lista-productos.component.ts añade:
onEliminar(id: string) {
console.log('Producto a eliminar:', id);
// Aquí luego llamaremos al servicio
}
Cada tarjeta recibe un producto y puede emitir eventos.
Ahora todas las tarjetas usan nuestro nuevo componente.
Si pulso eliminar, veo en consola el ID del producto.
Cuando un botón de eliminar se pulse dentro de una tarjeta, el evento sube hasta la lista.
Ahora todas las tarjetas usan nuestro nuevo componente.
Si pulso eliminar, veo en consola el ID del producto.
5.2. Recibir un producto con @Input()
Este componente no carga datos.
Solo recibe un producto mediante @Input(), siguiendo el patrón padre → hijo
Abre product-card.component.ts y déjalo así:
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Product } from '../../services/product.service';
@Component({
selector: 'app-product-card',
standalone: true,
imports: [CommonModule],
templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
@Input() product!: Product; // Recibe un producto desde el padre
}
5.3. Integrar product-card en ProductsListComponent
Por cada producto usamos una tarjeta <app-product-card>.
La lista llama al componente tarjeta y le pasa el producto.
Esto es un ejemplo de comunicación entre componentes:
el listado contiene muchos productos y la tarjeta representa uno.
Abre products-list.component.ts y añade:
import { ProductCardComponent } from '../product-card/product-card.component';
En el decorador:
imports: [CommonModule, ProductCardComponent]
Ahora actualiza el HTML products-list.component.html:
<h2 class="fw-semibold mb-3">Listado de Productos</h2>
<div class="row g-4">
@for (product of productos; track product._id) {
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
<app-product-card [product]="product"></app-product-card>
</div>
}
@empty {
<p class="text-muted">No hay productos para mostrar…</p>
}
</div>
Guardamos y recargamos la aplicación.
Ahora se muestra tantas veces el productCard como productos tengamos.
Nos quedaría dar formato al productoCard para mostrarlo elegante
5.4. Crear la tarjeta visual con Bootstrap
Creamos una tarjeta Bootstrap que muestra los datos del producto: imagen, nombre, descripción, precio, categoría y estado.
Al final añadimos el botón de eliminar.
Según el enunciado, este componente debe permitir eliminar productos, así que dejamos el botón listo.
Pero todavía no implementamos la lógica; eso lo haremos más adelante, cuando preparemos el servicio para modificar la lista.
Abre product-card.component.html y pega:
<div class="card h-100 shadow-sm border-0">
<img [src]="product.image"
class="card-img-top"
alt="{{ product.name }}"
style="object-fit: cover; height: 180px;">
<div class="card-body">
<h5 class="fw-semibold">{{ product.name }}</h5>
<p class="text-muted small mb-2">{{ product.description }}</p>
<p class="fw-bold text-primary">{{ product.price }} €</p>
<span class="badge bg-dark me-2">{{ product.category }}</span>
<span class="badge"
[class.bg-success]="product.active"
[class.bg-danger]="!product.active">
{{ product.active ? 'Activo' : 'Inactivo' }}
</span>
<button class="btn btn-danger btn-sm w-100 mt-3">
Eliminar producto
</button>
</div>
</div>
Ahora cada producto se muestra en una tarjeta visual completa, con su información y su botón de eliminar. De momento ese botón no hace nada.
6. ProductFormComponent (Formulario para añadir productos)
Vamos a crear el componente ProductFormComponent, que nos permitirá añadir productos nuevos a la aplicación.
Vamos a usar Reactive Forms, que es la forma moderna de trabajar con formularios en Angular.
También vamos a aplicar el Tema 8, porque este componente enviará datos al padre mediante un evento, y el Tema 7, ya que el servicio gestionará la creación del producto.
6.1. Crear ProductFormComponent
Creamos un componente standalone llamado product-form.
Aquí el usuario introducirá los datos de un producto.
ng generate component components/product-form --standalone --skip-tests6.2. El ReactiveForms en el componente
En Angular existen dos tipos de formularios: los template-driven y los reactive forms.
En este proyecto usamos formularios reactivos porque son más potentes, más organizados y son el estándar profesional.
Además, para esta actividad necesitamos enviar datos al padre, validar, generar un producto nuevo y resetear el formulario.
Eso se hace mucho mejor con Reactive Forms.
Abrimos product-form.component.ts y añadimos:
import { Component, EventEmitter, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
En el decorador:
imports: [CommonModule, ReactiveFormsModule],
Reactive Forms viene del módulo ReactiveFormsModule, que debemos importar directamente en el componente porque es standalone
Creamos un FormGroup con los campos necesarios para un producto.
Usamos un @Output() llamado productoCreado para enviar los datos al componente padre.
El método enviar() simplemente emite los datos del formulario y luego lo resetea.
Todavía no creamos el producto en el servicio porque eso lo veremos mas adelante junto con el UUID.
export class ProductFormComponent {
@Output() productoCreado = new EventEmitter<any>();
formulario = new FormGroup({ name: new FormControl(''), description: new FormControl(''), price: new FormControl(0), category: new FormControl(''), image: new FormControl(''), active: new FormControl(true) });
enviar() { this.productoCreado.emit(this.formulario.value); this.formulario.reset({ name: '', description: '', price: 0, category: '', image: '', active: true }); }}
export class ProductFormComponent {
@Output() productoCreado = new EventEmitter();
formulario = new FormGroup({
name: new FormControl(''),
description: new FormControl(''),
price: new FormControl(class="tokts-number">0),
category: new FormControl(''),
image: new FormControl(''),
active: new FormControl(true)
});
enviar() {
this.productoCreado.emit(this.formulario.value);
this.formulario.reset({
name: '',
description: '',
price: class="tokts-number">0,
category: '',
image: '',
active: true
});
}
}
6.3. Maquetar el formulario con Bootstrap
Maquetamos el formulario con Bootstrap.
Todos los campos del formulario reactivo están conectados directamente mediante formControlName.
El botón final enviará los datos del formulario al componente padre.
Abrimos product-form.component.html y pega:
<div class="card p-4 shadow-sm">
<h4 class="mb-3 fw-semibold">Añadir Producto</h4>
<form [formGroup]="formulario" (ngSubmit)="enviar()">
<div class="mb-3">
<label class="form-label">Nombre</label>
<input class="form-control" formControlName="name">
</div>
<div class="mb-3">
<label class="form-label">Descripción</label>
<textarea class="form-control" rows="2" formControlName="description"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Precio</label>
<input type="number" class="form-control" formControlName="price">
</div>
<div class="mb-3">
<label class="form-label">Categoría</label>
<input class="form-control" formControlName="category">
</div>
<div class="mb-3">
<label class="form-label">URL Imagen</label>
<input class="form-control" formControlName="image">
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" formControlName="active" id="activoCheck">
<label for="activoCheck" class="form-check-label">Activo</label>
</div>
<button type="submit" class="btn btn-primary w-100">
Crear producto
</button>
</form>
</div>6.4. Integrar el formulario en AppComponent
En este punto solo recibimos los datos del formulario y los mostramos por consola.
No se crea todavía el producto en la lista porque eso requiere un paso adicional en el servicio donde generaremos el UUID.
Abrimos app.ts añadimos:
import { ProductFormComponent } from './components/product-form/product-form.component';
En el decorador:
imports: [CommonModule, ProductsListComponent, ProductFormComponent]
En app.ts, creamos el manejador del evento:
onProductoCreado(producto: any) {
console.log('Producto recibido desde el formulario:', producto);
}
Ahora en app.html, encima del listado:
<main class="container mb-5">
<app-product-form
(productoCreado)="onProductoCreado($event)">
</app-product-form>
<app-products-list></app-products-list>
</main>
6.5. Comprobar funcionamiento
Rellenamos el formulario y pulsamos Crear producto.
En la consola del navegador debemos ver los datos que hemos enviado.
Eso significa que la comunicación hijo → padre está funcionando correctamente.”
7. Alta y Eliminación de Productos
Vamos a completar las dos operaciones más importantes de la aplicación: crear productos nuevos y eliminar productos existentes.
Este vídeo forma parte del Tema 7, porque vamos a modificar nuestro servicio y vamos a trabajar con el estado compartido.
También aplicamos el Tema 8, ya que varios componentes van a comunicarse entre sí.
Aquí también explicamos la famosa ‘trampilla’ del UUID que aparece en la actividad: como no tenemos un backend real, tendremos que simular la generación de IDs para poder crear y borrar productos
7.1. Preparar el ProductService
Vamos a convertir ProductService en el centro del estado de la aplicación.
Es decir, será el responsable de almacenar la lista de productos, añadir nuevos y eliminar los existentes. Es necesario para que todos los componentes tengan la lista actualizada y la pantalla se refresque automáticamente,
BehaviorSubject es el corazón de nuestra aplicación. Es donde vive la lista de productos, y cada vez que la modificamos (añadiendo, eliminando o filtrando) la aplicación entera se actualiza automáticamente.
BehaviorSubject = es como una mini-base-de-datos dentro del servicio
Abrimos product.service.ts y añadimos BehaviorSubject:
import { BehaviorSubject } from 'rxjs';
Debajo del url:
private productosSubject = new
BehaviorSubject<Product[]>([]);productos$ =
this.productosSubject.asObservable();
private productosOriginales: Product[] = [];
private productosSubject = new BehaviorSubject<Product[]>([]);
productos$ = this.productosSubject.asObservable();
private productosOriginales: Product[] = [];
productosOriginales será nuestra lista interna.
productos$ es un observable que enviará los cambios a los componentes automáticamente.
Esto es esencial para que, cuando creemos o eliminemos un producto, la lista se actualice sola.
Modificar cargarProductos() para usar BehaviorSubject
Cuando llegan los productos desde la API, los guardamos internamente y los notificamos a toda la aplicación mediante next().
Reemplaza cargarProductos():
cargarProductos() {
this.http.get<Product[]>(this.url).subscribe({
next: (productos) => { this.productosOriginales =
productos;
this.productosSubject.next(productos);
}, error: (err) => console.error('Error al cargar
productos:', err) });}
cargarProductos() {
this.http.get<Product[]>(this.url).subscribe({
next: (productos) => {
this.productosOriginales = productos;
this.productosSubject.next(productos);
},
error: (err) => console.error('Error al cargar productos:', err)
});
}
Implementar agregarProducto()
Aquí viene algo importante, en un proyecto real, el ID del producto lo genera el backend cuando hacemos un POST.
Pero nuestra API solo permite lectura, no creación y Angular no tiene forma de pedir un ID nuevo al servidor.
Por eso podemos usar UUID. Es una pequeña ‘trampilla’ que sirve para simular el comportamiento real de un backend y poder añadir, identificar y eliminar productos correctamente.
Las versiones modernas de JavaScript incluyen crypto.randomUUID(), que nos genera un ID único automáticamente.
_id: crypto.randomUUID(), // la “trampilla”
Quedaría así:
agregarProducto(datos: any) {
const nuevoProducto: Product = { _id: crypto.randomUUID(), // Generamos un ID único (trampilla) name: datos.name, description: datos.description, price: datos.price, category: datos.category, image: datos.image, active: datos.active };
// Añadimos el nuevo producto al principio de la lista this.productosOriginales = [nuevoProducto, ...this.productosOriginales];
// Emitimos la nueva lista para que Angular actualice la vista this.productosSubject.next(this.productosOriginales);}
agregarProducto(datos: any) {
const nuevoProducto: Product = {
_id: crypto.randomUUID(), // Generamos un ID único (trampilla)
name: datos.name,
description: datos.description,
price: datos.price,
category: datos.category,
image: datos.image,
active: datos.active
};
// Añadimos el nuevo producto al principio de la lista
this.productosOriginales = [nuevoProducto, ...this.productosOriginales];
// Emitimos la nueva lista para que Angular actualice la vista
this.productosSubject.next(this.productosOriginales);
}
Implementar eliminarProducto()
Eliminar un producto es tan simple como filtrar la lista interna y volver a emitir el cambio. Angular actualizará automáticamente la pantalla.
Añadir al servicio:
eliminarProducto(id: string) {
this.productosOriginales = this.productosOriginales.filter(p => p._id !== id);
this.productosSubject.next(this.productosOriginales);
}7.2. ProductList
Como hemos metido el Observable, modificamos la carga de datos.
Ahora el componente de lista ya no llama a cargarProductos().
El servicio carga los datos una vez y a partir de ahí la lista solo escucha al BehaviorSubject mediante productos$
constructor(private productService: ProductService) {
this.productService.productos$.subscribe(productos => {
this.productos = productos;
console.log('Productos recibidos:', productos);
});
}7.3. Conectar el formulario
Ahora, cuando el formulario emite un producto, llamamos a agregarProducto() y la lista se actualiza sola.
El constructor lo dejamos vacío, ya lo tenemos gestionado:
constructor(private productService: ProductService) {
//this.productService.cargarProductos().subscribe(datos => {
// console.log('Productos cargados:', datos);
//});
}
Abrimos app.ts y cambiamos el método:
onProductoCreado(producto: any) {
this.productService.agregarProducto(producto);
}
Verificamos que funciona en el navegador
7.4. ProductCard elimine productos
Ahora sí, el botón elimina el producto de la lista.
El componente tarjeta no toca el estado directamente: llama al servicio, y es el servicio quien actualiza la lista. Este es el patrón recomendado en Angular
Abrimos product-card.component.ts:
import { ProductService } from '../../services/product.service';
Constructor
constructor(private productService: ProductService) {}
Añadir método:
eliminar() {
this.productService.eliminarProducto(this.product._id);
}
Ahora en el HTML de la tarjeta:
<button class="btn btn-danger btn-sm w-100 mt-3"
(click)="eliminar()">
Eliminar producto
</button>
Verificamos que funciona en el navegador
8. ProductFilterComponent (Filtrado de productos en tiempo real)
Ahora que ya podemos añadir y eliminar productos, tiene todo el sentido implementar filtros, porque vamos a trabajar siempre sobre una lista dinámica.
Aplicamos principalmente el Tema 6, con control de flujo y renderizado condicional, y el Tema 7, usando el servicio como gestor del estado.
BehaviorSubject
Un BehaviorSubject es un tipo especial de observable que guarda un valor interno y emite ese valor cada vez que cambia.”
Cada vez que el valor cambia, todos los componentes que están suscritos se actualizan automáticamente
Hasta ahora tenemos varios componentes: el formulario, la lista, las tarjetas y, dentro de poco, los filtros.
Todos ellos necesitan trabajar con la misma lista de producto
¿Dónde guardamos esa lista para que todos la vean actualizada? En el servicio, no en los componentes
En una aplicación real, el backend sería quien guarda los datos. Como aquí no tenemos backend que permita crear y borrar productos, usamos BehaviorSubject como una pequeña base de datos en memoria
BehaviorSubject nos permite gestionar el estado compartido de la aplicación de forma reactiva. Cada vez que el estado cambia, todas las vistas se actualizan automáticamente.
8.1. Crear ProductFilterComponent
Creamos un componente standalone que se encargará únicamente de recoger criterios de filtrado.
ng generate component components/product-filter --standalone --skip-tests8.2. Añadir métodos de filtrado en ProductService
Siempre filtramos a partir de productosOriginales, no de la lista filtrada.
Así evitamos que los filtros se encadenen de forma incorrecta.
Abrimos product.service.ts y añadimos estos métodos:
filtrarPorNombre(nombre: string) {
const filtrados = this.productosOriginales.filter(p =>
p.name.toLowerCase().includes(nombre.toLowerCase())
);
this.productosSubject.next(filtrados);
}
filtrarPorCategoria(categoria: string) {
const filtrados = this.productosOriginales.filter(p =>
p.category.toLowerCase().includes(categoria.toLowerCase())
);
this.productosSubject.next(filtrados);
}
filtrarPorActivo(soloActivos: boolean) {
const filtrados = soloActivos
? this.productosOriginales.filter(p => p.active)
: this.productosOriginales;
this.productosSubject.next(filtrados);
}8.3. Lógica del filtro en el componente
Este componente no filtra directamente la lista.
Solo recoge los valores del formulario y se los comunica al servicio.
El servicio es el único responsable de modificar el estado
Abrimos product-filter.component.ts y lo dejamos así
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductService } from '../../services/product.service';
@Component({
selector: 'app-product-filter',
standalone: true,
imports: [CommonModule],
templateUrl: './product-filter.component.html'
})
export class ProductFilterComponent {
constructor(private productService: ProductService) {}
onNombreChange(event: Event) {
const valor = (event.target as HTMLInputElement).value;
this.productService.filtrarPorNombre(valor);
}
onCategoriaChange(event: Event) {
const valor = (event.target as HTMLInputElement).value;
this.productService.filtrarPorCategoria(valor);
}
onActivoChange(event: Event) {
const marcado = (event.target as HTMLInputElement).checked;
this.productService.filtrarPorActivo(marcado);
}
}8.4. Colocar el filtro en la vista principal
El filtro no modifica directamente la vista.
Modifica el estado del servicio, y ese estado se propaga automáticamente a todos los componentes que están suscritos.
Esta es la razón por la que BehaviorSubject es tan importante en Angular.
Abrimos app.ts importamos el filtro:
import { ProductFilterComponent } from './components/product-filter/product-filter.component';
En el decorador
imports: [
CommonModule,
ProductFormComponent,
ProductFilterComponent,
ProductsListComponent
]
En app.html, colocamos el filtro entre el formulario y la lista:
<main class="container mb-5">
<app-product-form
(productoCreado)="onProductoCreado($event)">
</app-product-form>
<app-product-filter></app-product-filter>
<app-products-list></app-products-list>
</main>
La estructura ahora es clara:
Formulario -> Filtros -> Lista
Probamos la aplicación:
- Escribimos un nombre -> la lista se filtra
- Escribimos una categoría -> se actualiza
- Activamos "solo activos" -> desaparecen los inactivos
Todo esto ocurre en tiempo real gracias al BehaviorSubject.
8.5. Maquetar el filtro con Bootstrap
El filtro que vamos a crear es un formulario sencillo:
- un campo de texto para el nombre
- otro para la categoría
- y un checkbox para mostrar solo productos activos.
Abrimos product-filter.component.html y ponemos:
<div class="card p-3 mb-4 shadow-sm">
<h5 class="fw-semibold mb-3">Filtrar productos</h5>
<div class="row g-3">
<div class="col-md-4">
<input
type="text"
class="form-control"
placeholder="Buscar por nombre"
(input)="onNombreChange($event)">
</div>
<div class="col-md-4">
<input
type="text"
class="form-control"
placeholder="Filtrar por categoría"
(input)="onCategoriaChange($event)">
</div>
<div class="col-md-4 d-flex align-items-center">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="soloActivos"
(change)="onActivoChange($event)">
<label class="form-check-label" for="soloActivos">
Solo activos
</label>
</div>
</div>
</div>
</div>9. Proyecto final
Aquí os dejo el proyecto final para ir comprobando con el vuestro