Listado simple - Angular
Ahora que ya tenemos listo el proyecto frontend de Angular (en el puerto 4200), ya podemos empezar a codificar la solución.
Primeros pasos
Antes de empezar
Quiero hacer hincapié que Angular tiene una documentación muy extensa y completa, así que te recomiendo que hagas uso de ella cuando tengas cualquier duda. Tanto en la propia web de documentación de Angular como en la web de componentes Angular Material puedes buscar casi cualquier ejemplo que necesites.
Si abrimos el proyecto con el IDE que tengamos (Visual Studio Code en el caso del tutorial) podemos ver que en la carpeta src/app
existen unos ficheros ya creados por defecto. Estos ficheros son:
app.component.ts
→ contiene el código inicial del proyecto escrito en TypeScript.app.component.html
→ contiene la plantilla inicial del proyecto escrita en HTML.app.component.scss
→ contiene los estilos CSS privados de la plantilla inicial.
Vamos a modificar este código inicial para ver como funciona. Abrimos el fichero app.component.ts
y modificamos la línea donde se asigna un valor a la variable title
.
...
title = 'Tutorial de Angular';
...
Ahora abrimos el fichero app.component.html
, borramos todo el código de la plantilla y añadimos el siguiente código:
<h1>{{title}}</h1>
Las llaves dobles permiten hacen un binding entre el código del componente y la plantilla. Es decir, en este caso irá al código TypeScript y buscará el valor de la variable title
.
Consejo
El binding también nos sirve para ejecutar los métodos de TypeScript desde el código HTML. Además, si el valor que contiene la variable se modificará durante la ejecución de algún método, automáticamente el código HTML refrescaría el nuevo valor de la variable title
Si abrimos el navegador y accedemos a http://localhost:4200/
podremos ver el resultado del código.
Layout general
Crear componente
Lo primero que vamos a hacer es escoger un tema y una paleta de componentes para trabajar. Lo más cómodo es trabajar con Material
que ya viene perfectamente integrado en Angular. Ejecutamos el comando y elegimos la paleta de colores que más nos guste o bien creamos una custom:
ng add @angular/material
Recuerda
Al añadir una nueva librería tenemos que parar el servidor y volver a arrancarlo para que compile y recargue las nuevas dependencias.
Una vez añadida la dependencia, lo que queremos es crear una primera estructura inicial a la página. Si te acuerdas cuál era la estructura (y si no te acuerdas, vuelve a la sección Contexto de la aplicación
y lo revisas), teníamos una cabecera superior con un logo y título y unas opciones de menú.
Para agrupar los componentes comunes de nuestra aplicación vamos a crear una carpeta llamada "core" dentro de la carpeta "src", e iremos creando los componentes que necesitemos. Empecemos con el header. Desde la raíz de nuestro proyecto introducimos el siguiente comando:
ng generate component core/header
Código de la pantalla
Esto nos creará una carpeta con los ficheros del componente, donde tendremos que copiar el siguiente contenido:
<mat-toolbar>
<mat-toolbar-row>
<div class="header_container">
<div class="header_title">
<mat-icon>storefront</mat-icon> Ludoteca Tan
</div>
<div class="header_separator"> | </div>
<div class="header_menu">
<div class="header_button">
<a routerLink="/games" routerLinkActive="active">Catálogo</a>
</div>
<div class="header_button">
<a routerLink="/categories" routerLinkActive="active">Categorías</a>
</div>
<div class="header_button">
<a routerLink="/authors" routerLinkActive="active">Autores</a>
</div>
</div>
<div class="header_login">
<mat-icon>account_circle</mat-icon> Sign in
</div>
</div>
</mat-toolbar-row>
</mat-toolbar>
.mat-toolbar {
background-color: blue;
color: white;
}
.header_container {
display: flex;
width: 100%;
.header_title {
.mat-icon {
vertical-align: sub;
}
}
.header_separator {
margin-left: 30px;
margin-right: 30px;
}
.header_menu {
flex-grow: 4;
display: flex;
flex-direction: row;
.header_button {
margin-left: 1em;
margin-right: 1em;
font-size: 16px;
a {
font-weight: lighter;
text-decoration: none;
cursor: pointer;
color: white;
}
a:hover {
color: grey;
}
a.active {
font-weight: normal;
text-decoration: underline;
color: lightyellow;
}
}
}
.header_login {
font-size: 16px;
cursor: pointer;
.mat-icon {
vertical-align: sub;
}
}
}
Al utilizar etiquetas de material como mat-toolbar
o mat-icon
y routerLink
necesitaremos importar las dependencias. Al tratarse de un standalone component lo tendremos que hacer directamente en el atributo "imports" de nuestro component en el fichero header.component.ts
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
@Component({
selector: 'app-header',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatIconModule,
MatToolbarModule,
],
templateUrl: './header.component.html',
styleUrl: './header.component.scss'
})
export class HeaderComponent {
}
Ahora, para poder usar nuestro componente en las páginas del componente AppComponent
tendremos también que añadir en el atributo "imports" de app.component.ts
nuestro nuevo componente:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HeaderComponent } from '../core/header/header.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, HeaderComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'Tutorial de Angular';
}
Ya por último solo nos queda modificar la página general de la aplicación app.component.html
para añadirle el componente HeaderComponent
.
<div>
<app-header></app-header>
<div>
<router-outlet></router-outlet>
</div>
</div>
Vamos al navegador y refrescamos la página, debería aparecer una barra superior (Header) con las opciones de menú. Algo similar a esto:
Recuerda
Cuando se añaden componentes a los ficheros html
, siempre se deben utilizar los selectores definidos para el componente. En el caso anterior hemos añadido app-header
que es el mismo nombre selector que tiene el componente en el fichero header.component.ts
.
Además, recuerda que para poder utilizar componentes, los debes importar en el componente donde vayan a ser utilizados.
Creando un listado básico
Crear componente
Ya tenemos la estructura principal, ahora vamos a crear nuestra primera pantalla. Vamos a empezar por la de Categorías
que es la más sencilla, ya que se trata de un listado, que muestra datos sin filtrar ni paginar.
Como categorías es un dominio funcional de la aplicación, vamos a crear una carpeta que contenga toda la funcionalidad de ese dominio y todas las pantallas, componentes y servicios que creemos referidos a este dominio funcional, deberán ir dentro de esta nueva carpeta.
Vamos a crear un primer componente que será un listado de categorías. Para ello vamos a ejecutar el siguiente comando desde src:
ng generate component category/category-list
Para terminar de configurar la aplicación, vamos a añadir la ruta del componente dentro del componente routing de Angular, para poder acceder a él, para ello modificamos el fichero app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: 'categories', loadComponent: () => import('../category/category-list/category-list.component').then(m => m.CategoryListComponent)},
];
Si abrimos el navegador y accedemos a http://localhost:4200/
podremos navegar mediante el menú Categorías
el cual abrirá el componente que acabamos de crear.
Código de la pantalla
Ahora vamos a construir la pantalla. Para manejar la información del listado, necesitamos almacenar los datos en un objeto de tipo model
. Para ello crearemos un fichero en category\model\Category.ts
donde implementaremos la clase necesaria. Esta clase será la que utilizaremos en el código html y ts de nuestro componente.
export class Category {
id: number;
name: string;
}
También, escribiremos el código de la pantalla de listado.
<div class="container">
<h1>Listado de Categorías</h1>
<mat-table [dataSource]="dataSource">
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.id}} </mat-cell>
</ng-container>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef> Nombre categoría </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
</ng-container>
<ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let element">
<button mat-icon-button color="primary"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="accent"><mat-icon>clear</mat-icon></button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<div class="buttons">
<button mat-flat-button color="primary">Nueva categoría</button>
</div>
</div>
.container {
margin: 20px;
mat-table {
margin-top: 10px;
margin-bottom: 20px;
.mat-header-row {
background-color:#f5f5f5;
.mat-header-cell {
text-transform: uppercase;
font-weight: bold;
color: #838383;
}
}
.mat-column-id {
flex: 0 0 20%;
justify-content: center;
}
.mat-column-action {
flex: 0 0 10%;
justify-content: center;
}
}
.buttons {
text-align: right;
}
}
import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { Category } from '../model/Category';
import { CommonModule } from '@angular/common';
import { MatTableModule } from '@angular/material/table';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-category-list',
standalone: true,
imports: [
MatButtonModule,
MatIconModule,
MatTableModule,
CommonModule
],
templateUrl: './category-list.component.html',
styleUrl: './category-list.component.scss'
})
export class CategoryListComponent implements OnInit{
dataSource = new MatTableDataSource<Category>();
displayedColumns: string[] = ['id', 'name', 'action'];
constructor() { }
ngOnInit(): void {
}
}
El código HTML es fácil de seguir, pero por si acaso:
- Línea 4: Creamos la tabla con la variable
dataSource
definida en el fichero .ts - Línea 5: Definición de la primera columna, su cabecera y el dato que va a contener
- Línea 10: Definición de la segunda columna, su cabecera y el dato que va a contener
- Línea 15: Definición de la tercera columna, su cabecera vacía y los dos botones de acción
- Línea 23 y 24: Construcción de la cabecera y las filas
Si abrimos el navegador y accedemos a http://localhost:4200/
y pulsamos en el menú de Categorías
obtendremos una pantalla con un listado vacío (solo con cabeceras) y un botón de crear Nueva Categoría que aún no hace nada.
Añadiendo datos
En este punto y para ver como responde el listado, vamos a añadir datos. Si tuviéramos el backend implementado podríamos consultar los datos directamente de una operación de negocio de backend, pero ahora mismo no lo tenemos implementado, así que para no bloquear el desarrollo vamos a mockear los datos.
Creando un servicio
En angular, cualquier acceso a datos debe pasar por un service
, así que vamos a crearnos uno para todas las operaciones de categorías. Vamos a la consola y ejecutamos:
ng generate service category/category
Esto nos creará un servicio, que además podemos utilizarlo inyectándolo en cualquier componente que lo necesite.
Implementando un servicio
Vamos a implementar una operación de negocio que recupere el listado de categorías y lo vamos a hacer de forma reactiva (asíncrona) para simular una petición a backend. Modificamos los siguientes ficheros:
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Category } from './model/Category';
@Injectable({
providedIn: 'root'
})
export class CategoryService {
constructor() { }
getCategories(): Observable<Category[]> {
return new Observable();
}
}
import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { Category } from '../model/Category';
import { CommonModule } from '@angular/common';
import { MatTableModule } from '@angular/material/table';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { CategoryService } from '../category.service';
@Component({
selector: 'app-category-list',
standalone: true,
imports: [
MatButtonModule,
MatIconModule,
MatTableModule,
CommonModule
],
templateUrl: './category-list.component.html',
styleUrl: './category-list.component.scss'
})
export class CategoryListComponent implements OnInit{
dataSource = new MatTableDataSource<Category>();
displayedColumns: string[] = ['id', 'name', 'action'];
constructor(
private categoryService: CategoryService,
) { }
ngOnInit(): void {
this.categoryService.getCategories().subscribe(
categories => this.dataSource.data = categories
);
}
}
Mockeando datos
Como hemos comentado anteriormente, el backend todavía no está implementado, así que vamos a mockear datos. Nos crearemos un fichero mock-categories.ts
dentro de model, con datos ficticios y modificaremos el servicio para que devuelva esos datos. De esta forma, cuando tengamos implementada la operación de negocio en backend, tan solo tenemos que sustituir el código que devuelve datos estáticos por una llamada http.
import { Category } from "./Category";
export const CATEGORY_DATA: Category[] = [
{ id: 1, name: 'Dados' },
{ id: 2, name: 'Fichas' },
{ id: 3, name: 'Cartas' },
{ id: 4, name: 'Rol' },
{ id: 5, name: 'Tableros' },
{ id: 6, name: 'Temáticos' },
{ id: 7, name: 'Europeos' },
{ id: 8, name: 'Guerra' },
{ id: 9, name: 'Abstractos' },
]
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Category } from './model/Category';
import { CATEGORY_DATA } from './model/mock-categories';
@Injectable({
providedIn: 'root'
})
export class CategoryService {
constructor() { }
getCategories(): Observable<Category[]> {
return of(CATEGORY_DATA);
}
}
Si ahora refrescamos la página web, veremos que el listado ya tiene datos con los que vamos a interactuar.
Simulando las otras peticiones
Para terminar, vamos a simular las otras dos peticiones, la de editar y la de borrar para cuando tengamos que utilizarlas. El servicio debe quedar más o menos así:
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Category } from './model/Category';
import { CATEGORY_DATA } from './model/mock-categories';
@Injectable({
providedIn: 'root'
})
export class CategoryService {
constructor() { }
getCategories(): Observable<Category[]> {
return of(CATEGORY_DATA);
}
saveCategory(category: Category): Observable<Category> {
return of(null);
}
deleteCategory(idCategory : number): Observable<any> {
return of(null);
}
}
Añadiendo acciones al listado
Crear componente
Ahora nos queda añadir las acciones al listado: crear, editar y eliminar. Empezaremos primero por las acciones de crear y editar, que ambas deberían abrir una ventana modal con un formulario para poder modificar datos de la entidad Categoría
.
Como siempre, para crear un componente usamos el asistente de Angular, esta vez al tratarse de una pantalla que solo vamos a utilizar dentro del dominio de categorías, tiene sentido que lo creemos dentro de ese módulo:
ng generate component category/category-edit
Ahora vamos a hacer que se abra al pulsar el botón Nueva categoría
. Para eso, vamos al fichero category-list.component.ts
y añadimos un nuevo método:
...
import { MatDialog } from '@angular/material/dialog';
import { CategoryEditComponent } from '../category-edit/category-edit.component';
...
imports: [
MatButtonModule,
MatIconModule,
MatTableModule,
CommonModule,
],
...
constructor(
private categoryService: CategoryService,
public dialog: MatDialog,
) { }
...
createCategory() {
const dialogRef = this.dialog.open(CategoryEditComponent, {
data: {}
});
dialogRef.afterClosed().subscribe(result => {
this.ngOnInit();
});
}
...
Para poder abrir un componente dentro de un diálogo necesitamos obtener en el constructor un MatDialog. De ahí que hayamos tenido que añadirlo como import y en el constructor.
Dentro del método createCategory
lo que hacemos es crear un diálogo con el componente CategoryEditComponent
en su interior, pasarle unos datos de creación, donde podemos poner estilos del dialog y un objeto data
donde pondremos los datos que queremos pasar entre los componentes. Por último, nos suscribimos al evento afterClosed
para ejecutar las acciones que creamos oportunas, en nuestro caso volveremos a cargar el listado inicial.
Y ya por último enlazamos el click en el botón con el método que acabamos de crear para abrir el diálogo. Modificamos el fichero category-list.component.html
y añadimos el evento click:
...
<div class="buttons">
<button mat-flat-button color="primary" (click)="createCategory()">Nueva categoría</button>
</div>
</div>
Si refrescamos el navegador y pulsamos el botón Nueva categoría
, veremos como se abre una ventana modal de tipo Dialog con el componente nuevo que hemos creado, aunque solo se leerá category-edit works!
que es el contenido por defecto del componente.
Código del diálogo
Ahora vamos a darle forma al formulario de editar y crear. Para ello vamos al html, ts y css del componente y pegamos el siguiente contenido:
<div class="container">
<h1>Crear categoría</h1>
<form>
<mat-form-field>
<mat-label>Identificador</mat-label>
<input type="text" matInput placeholder="Identificador" [(ngModel)]="category.id" name="id" disabled>
</mat-form-field>
<mat-form-field>
<mat-label>Nombre</mat-label>
<input type="text" matInput placeholder="Nombre de categoría" [(ngModel)]="category.name" name="name" required>
<mat-error>El nombre no puede estar vacío</mat-error>
</mat-form-field>
</form>
<div class="buttons">
<button mat-stroked-button (click)="onClose()">Cerrar</button>
<button mat-flat-button color="primary" (click)="onSave()">Guardar</button>
</div>
</div>
.container {
min-width: 350px;
max-width: 500px;
padding: 20px;
form {
display: flex;
flex-direction: column;
margin-bottom:20px;
}
.buttons {
text-align: right;
button {
margin-left: 10px;
}
}
}
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CategoryService } from '../category.service';
import { Category } from '../model/Category';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-category-edit',
standalone: true,
imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule ],
templateUrl: './category-edit.component.html',
styleUrl: './category-edit.component.scss'
})
export class CategoryEditComponent implements OnInit {
category: Category;
constructor(
public dialogRef: MatDialogRef<CategoryEditComponent>,
private categoryService: CategoryService
) {}
ngOnInit(): void {
this.category = new Category();
}
onSave() {
this.categoryService.saveCategory(this.category).subscribe(() => {
this.dialogRef.close();
});
}
onClose() {
this.dialogRef.close();
}
}
Si te fijas en el código TypeScript, hemos añadido en el método onSave
una llamada al servicio de CategoryService
que aunque no realice ninguna operación de momento, por lo menos lo dejamos preparado para conectar con el servidor.
Además, como siempre, al utilizar componentes matInput
, matForm
, matError
hay que añadir los módulos correspondientes como dependencias en el atributo imports.
Ahora podemos navegar y abrir el cuadro de diálogo mediante el botón Nueva categoría
para ver como queda nuestro formulario.
Utilizar el diálogo para editar
El mismo componente que hemos utilizado para crear una nueva categoría, nos sirve también para editar una categoría existente. Tan solo tenemos que utilizar la funcionalidad que Angular nos proporciona y pasarle los datos a editar en la llamada de apertura del Dialog.
Vamos a implementar funcionalidad sobre el icono editar
, tendremos que modificar unos cuantos ficheros:
<div class="container">
<h1>Listado de Categorías</h1>
<mat-table [dataSource]="dataSource">
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef> Identificador </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.id}} </mat-cell>
</ng-container>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef> Nombre categoría </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
</ng-container>
<ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let element">
<button mat-icon-button color="primary" (click)="editCategory(element)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="accent"><mat-icon>clear</mat-icon></button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<div class="buttons">
<button mat-flat-button color="primary" (click)="createCategory()">Nueva categoría</button>
</div>
</div>
export class CategoryListComponent implements OnInit {
dataSource = new MatTableDataSource<Category>();
displayedColumns: string[] = ['id', 'name', 'action'];
constructor(
private categoryService: CategoryService,
public dialog: MatDialog,
) { }
ngOnInit(): void {
this.categoryService.getCategories().subscribe(
categories => this.dataSource.data = categories
);
}
createCategory() {
const dialogRef = this.dialog.open(CategoryEditComponent, {
data: {}
});
dialogRef.afterClosed().subscribe(result => {
this.ngOnInit();
});
}
editCategory(category: Category) {
const dialogRef = this.dialog.open(CategoryEditComponent, {
data: { category }
});
dialogRef.afterClosed().subscribe(result => {
this.ngOnInit();
});
}
}
Y los Dialog:
<div class="container">
@if (category.id) {
<h1>Modificar categoría</h1>
} @else {
<h1>Crear categoría</h1>
}
<form>
<mat-form-field>
...
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CategoryService } from '../category.service';
import { Category } from '../model/Category';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-category-edit',
standalone: true,
imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule ],
templateUrl: './category-edit.component.html',
styleUrl: './category-edit.component.scss'
})
export class CategoryEditComponent implements OnInit {
category: Category;
constructor(
public dialogRef: MatDialogRef<CategoryEditComponent>,
@Inject(MAT_DIALOG_DATA) public data: {category : Category},
private categoryService: CategoryService
) {}
ngOnInit(): void {
this.category = this.data.category != null ? this.data.category : new Category();
}
onSave() {
this.categoryService.saveCategory(this.category).subscribe(() => {
this.dialogRef.close();
});
}
onClose() {
this.dialogRef.close();
}
}
Navegando ahora por la página y pulsando en el icono de editar, se debería abrir una ventana con los datos que hemos seleccionado, similar a esta imagen:
Si te fijas, al modificar los datos dentro de la ventana de diálogo se modifica también en el listado. Esto es porque estamos pasando el mismo objeto desde el listado a la ventana diálogo y al ser el listado y el formulario reactivos los dos, cualquier cambio sobre los datos se refresca directamente en la pantalla.
Hay veces en la que este comportamiento nos interesa, pero en este caso no queremos que se modifique el listado. Para solucionarlo debemos hacer una copia del objeto, para que ambos modelos (formulario y listado) utilicen objetos diferentes. Es tan sencillo como modificar category-edit.component.ts
y añadirle una copia del dato
...
ngOnInit(): void {
this.category = this.data.category ? Object.assign({}, this.data.category) : new Category();
}
...
Cuidado
Hay que tener mucho cuidado con el binding de los objetos. Hay veces que al modificar un objeto NO queremos que se modifique en todas sus instancias y tenemos que poner especial cuidado en esos aspectos.
Acción de borrado
Por norma general, toda acción de borrado de un dato de pantalla requiere una confirmación previa por parte del usuario. Es decir, para evitar que el dato se borre accidentalmente, el usuario tendrá que confirmar su acción. Por tanto, vamos a crear un componente que nos permita pedir una confirmación al usuario.
Como esta pantalla de confirmación va a ser algo común a muchas acciones de borrado de nuestra aplicación, vamos a crearla dentro del módulo core
. Como siempre, ejecutamos el comando en consola:
ng generate component core/dialog-confirmation
E implementamos el código que queremos que tenga el componente. Al ser un componente genérico vamos a aprovechar y leeremos las variables que le pasemos en data
.
<div class="container">
<h1>{{title}}</h1>
<div [innerHTML]="description" class="description"></div>
<div class="buttons">
<button mat-stroked-button (click)="onClose()">No</button>
<button mat-flat-button color="primary" (click)="onClose(true)">Sí</button>
</div>
</div>
.container {
min-width: 350px;
max-width: 500px;
padding: 20px;
.description {
margin-bottom: 20px;
}
.buttons {
text-align: right;
button {
margin-left: 10px;
}
}
}
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-dialog-confirmation',
standalone: true,
imports: [MatButtonModule],
templateUrl: './dialog-confirmation.component.html',
styleUrl: './dialog-confirmation.component.scss',
})
export class DialogConfirmationComponent {
title: string;
description: string;
constructor(
public dialogRef: MatDialogRef<DialogConfirmationComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) {}
ngOnInit(): void {
this.title = this.data.title;
this.description = this.data.description;
}
onClose(value = false) {
this.dialogRef.close(value);
}
}
Ya por último, una vez tenemos el componente genérico de diálogo, vamos a utilizarlo en nuestro listado al pulsar el botón eliminar:
...
<ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let element">
<button mat-icon-button color="primary" (click)="editCategory(element)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="accent" (click)="deleteCategory(element)">
<mat-icon>clear</mat-icon>
</button>
</mat-cell>
</ng-container>
...
...
import { DialogConfirmationComponent } from '../../core/dialog-confirmation/dialog-confirmation.component';
...
deleteCategory(category: Category) {
const dialogRef = this.dialog.open(DialogConfirmationComponent, {
data: { title: "Eliminar categoría", description: "Atención si borra la categoría se perderán sus datos.<br> ¿Desea eliminar la categoría?" }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.categoryService.deleteCategory(category.id).subscribe(result => {
this.ngOnInit();
});
}
});
}
...
Aquí también hemos realizado la llamada a categoryService
, aunque no se realice ninguna acción, pero así lo dejamos listo para enlazarlo.
Llegados a este punto, ya solo nos queda enlazar las acciones de la pantalla con las operaciones de negocio del backend.
Conectar con Backend
Antes de seguir
Antes de seguir con este punto, debes implementar el código de backend en la tecnología que quieras (Springboot o Nodejs). Si has empezado este capítulo implementando el frontend, por favor accede a la sección correspondiente de backend para poder continuar con el tutorial. Una vez tengas implementadas todas las operaciones para este listado, puedes volver a este punto y continuar con Angular.
El siguiente paso, como es obvio, será hacer que Angular llame directamente al servidor backend para leer y escribir datos y eliminar los datos mockeados en Angular.
¡Manos a la obra!
Llamada del listado
La idea es que el método getCategories()
de category.service.ts
en lugar de devolver datos estáticos, realice una llamada al servidor a la ruta http://localhost:8080/category
.
Abrimos el fichero y sustituimos la línea que antes devolvía los datos estáticos, por esto:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Category } from './model/Category';
@Injectable({
providedIn: 'root'
})
export class CategoryService {
constructor(
private http: HttpClient
) { }
private baseUrl = 'http://localhost:8080/category';
getCategories(): Observable<Category[]> {
return this.http.get<Category[]>(this.baseUrl);
}
saveCategory(category: Category): Observable<Category> {
return of(null);
}
deleteCategory(idCategory : number): Observable<any> {
return of(null);
}
}
Como hemos añadido un componente nuevo HttpClient
tenemos que configurar nuestro proyecto para poder realizar llamadas, para eso modificamos el fichero app.config.ts
.
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimationsAsync(),
provideHttpClient(withFetch())
]
};
Si ahora refrescas el navegador (recuerda tener arrancado también el servidor) y accedes a la pantalla de Categorías
debería aparecer el listado con los datos que vienen del servidor.
Llamada de guardado / edición
Para la llamada de guardado haríamos lo mismo, pero invocando la operación de negocio put
.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Category } from './model/Category';
@Injectable({
providedIn: 'root'
})
export class CategoryService {
constructor(private http: HttpClient) {}
private baseUrl = 'http://localhost:8080/category';
getCategories(): Observable<Category[]> {
return this.http.get<Category[]>(this.baseUrl);
}
saveCategory(category: Category): Observable<Category> {
const { id } = category;
const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;
return this.http.put<Category>(url, category);
}
deleteCategory(idCategory : number): Observable<any> {
return of(null);
}
}
Ahora podemos probar a modificar o añadir una nueva categoría desde la pantalla y debería aparecer los nuevos datos en el listado.
Llamada de borrado
Y ya por último, la llamada de borrado, deberíamos cambiarla e invocar a la operación de negocio delete
.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Category } from './model/Category';
@Injectable({
providedIn: 'root'
})
export class CategoryService {
constructor(private http: HttpClient) {}
private baseUrl = 'http://localhost:8080/category';
getCategories(): Observable<Category[]> {
return this.http.get<Category[]>(this.baseUrl);
}
saveCategory(category: Category): Observable<Category> {
const { id } = category;
const url = id ? `${this.baseUrl}/${id}` : this.baseUrl;
return this.http.put<Category>(url, category);
}
deleteCategory(idCategory : number): Observable<any> {
return this.http.delete(`${this.baseUrl}/${idCategory}`);
}
}
Ahora podemos probar a modificar o añadir una nueva categoría desde la pantalla y debería aparecer los nuevos datos en el listado.
Como ves, es bastante sencillo conectar server y client.
Depuración
Una parte muy importante del desarrollo es tener la capacidad de depurar nuestro código, en este apartado vamos a explicar como se realiza debug
en Front.
Esta parte se puede realizar con nuestro navegador favorito, en este caso vamos a utilizar Chrome.
El primer paso es abrir las herramientas del desarrollador del navegador presionando F12
.
En esta herramienta tenemos varias partes importantes:
- Elements: Inspector de los elementos del DOM de nuestra aplicación que nos ayuda identificar el código generado.
- Console: Consola donde podemos ver mensajes importantes que nos ayudan a identificar posibles problemas.
- Source: El navegador de ficheros que componen nuestra aplicación.
- Network: El registro de peticiones que realiza nuestra aplicación.
Identificados los elementos importantes, vamos a depurar la operación de crear categoría.
Para ello nos dirigimos a la pestaña de Source
, en el árbol de carpetas nos dirigimos a la ruta donde está localizado el código de nuestra aplicación webpack://src/app
.
Dentro de esta carpeta esté localizado todo el código fuente de la aplicación, en nuestro caso vamos a localizar componente category-edit.component
que crea una nueva categoría.
Dentro del fichero ya podemos añadir puntos de ruptura (breakpoint), en nuestro caso queremos comprobar que el nombre introducido se captura bien y se envía al service correctamente.
Colocamos el breakpoint en la línea de invocación del service (click sobre el número de la línea) y desde la interfaz creamos una nueva categoría.
Hecho esto, podemos observar que a nivel de interfaz, la aplicación se detiene y aparece un panel de manejo de los puntos de interrupción:
En cuanto a la herramienta del desarrollador, nos lleva al punto exacto donde hemos añadido el breakpoint y se para en este punto ofreciéndonos la posibilidad de explorar el contenido de las variables del código:
Aquí podemos comprobar que efectivamente la variable category
tiene el valor que hemos introducido por pantalla y se propaga correctamente hacia el service.
Para continuar con la ejecución basta con darle al botón de play
del panel de manejo de interrupción o al que aparece dentro de la herramienta de desarrollo (parte superior derecha).
Por último, vamos a revisar que la petición REST se ha realizado correctamente al backend, para ello nos dirigimos a la pestaña Network
y comprobamos las peticiones realizadas:
Aquí podemos observar el registro de todas las peticiones y haciendo click sobre una de ellas, obtenemos el detalle de esta.
- Header: Información de las cabeceras enviadas (aquí podemos ver que se ha hecho un PUT a la ruta correcta).
- Payload: El cuerpo de la petición (vemos el cuerpo del mensaje con el nombre enviado).
- Preview: Respuesta de la petición normalizada (vemos la respuesta con el identificador creado para la nueva categoría).