Skip to content

Commit e25acc7

Browse files
authored
Merge pull request #26 from ccsw-csd/feature/angular17-signals
feat: angular 17 tutorial with signals
2 parents 5688577 + fe02615 commit e25acc7

9 files changed

Lines changed: 521 additions & 458 deletions

File tree

docs/develop/basic/angular17.md

Lines changed: 89 additions & 79 deletions
Large diffs are not rendered by default.

docs/develop/filtered/angular17.md

Lines changed: 90 additions & 93 deletions
Large diffs are not rendered by default.

docs/develop/paginated/angular17.md

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Como ya conocemos como se debe desarrollar, en este ejemplo vamos a ir más ráp
1313
Vamos a desarrollar el listado de `Autores` así que, debemos crear los componentes:
1414

1515
```
16-
ng generate component author/author-list
16+
ng generate component author/author-list --type=page
1717
ng generate component author/author-edit
1818
1919
ng generate service author/author
@@ -41,8 +41,8 @@ Añadimos la ruta al menú para que podamos acceder a la pantalla:
4141
import { Routes } from '@angular/router';
4242

4343
export const routes: Routes = [
44-
{ path: 'categories', loadComponent: () => import('../category/category-list/category-list.component').then(m => m.CategoryListComponent)},
45-
{ path: 'authors', loadComponent: () => import('../author/author-list/author-list.component').then(m => m.AuthorListComponent)},
44+
{ path: 'categories', loadComponent: () => import('../category/category-list/category-list.page').then(m => m.CategoryListPage)},
45+
{ path: 'authors', loadComponent: () => import('../author/author-list/author-list.page').then(m => m.AuthorListPage)},
4646
];
4747
```
4848

@@ -70,7 +70,18 @@ Por defecto, el esquema de datos de Spring para la paginación es como el siguie
7070
}
7171
```
7272

73-
Así que necesitamos poder enviar y recuperar esa información desde Angular, nos hace falta crear esos objetos. Los objetos de paginación, al ser comunes a toda la aplicación, vamos a crearlos en `core/model/page`, mientras que la paginación de `AuthorPage.ts` la crearé en su propio model dentro de `author/model`.
73+
Así que necesitamos poder enviar y recuperar esa información desde Angular, nos hace falta crear esos objetos. Los objetos de paginación, al ser comunes a toda la aplicación, vamos a crearlos en `core/model/page`.
74+
75+
!!! info "¿Por qué en `core/model/page` y no dentro del componente `author`?"
76+
`SortPage`, `Pageable` y `PaginatedData` **no pertenecen al dominio de negocio de `Author`**; son contratos técnicos que describen cómo se comunica el frontend con el backend cuando los datos vienen paginados.
77+
78+
Cualquier otro listado de la aplicación —categorías, préstamos, juegos…— necesitará exactamente estos mismos objetos. Si los metiéramos dentro de `author/`, el día que quisiéramos paginar otro listado tendríamos dos opciones igual de malas: duplicar el código o importar clases de un módulo que no tiene nada que ver con tu entidad.
79+
80+
Colocarlos en `core/` sigue el principio de **reutilización y responsabilidad única**:
81+
82+
- Un único lugar donde mantener el contrato de paginación.
83+
- Si el backend cambia el esquema (p. ej. renombra `pageNumber` a `page`), sólo hay que tocar un fichero.
84+
- Cualquier desarrollador que llegue al proyecto sabrá dónde buscarlos sin tener que adivinar en qué módulo de negocio están escondidos.
7485

7586
=== "SortPage.ts"
7687
``` TypeScript
@@ -89,25 +100,38 @@ Así que necesitamos poder enviar y recuperar esa información desde Angular, no
89100
sort: SortPage[];
90101
}
91102
```
92-
=== "AuthorPage.ts"
103+
=== "PaginatedData.ts"
93104
``` TypeScript
94105
import { Pageable } from "src/app/core/model/page/Pageable";
95-
import { Author } from "./Author";
96106

97-
export class AuthorPage {
98-
content: Author[];
107+
export class PaginatedData <TData>{
108+
content: TData[];
99109
pageable: Pageable;
100110
totalElements: number;
101111
}
102112
```
103113

114+
!!! info "¿Qué es `<TData>`?"
115+
`<TData>` es un **genérico de TypeScript** (el equivalente a los *generics* de Java `<T>` o de C# `<T>`).
116+
117+
Permite que `PaginatedData` sea una clase **reutilizable con cualquier tipo de contenido**, sin necesidad de crear una clase distinta para cada entidad:
118+
119+
```typescript
120+
PaginatedData<Author> // content será Author[]
121+
PaginatedData<Category> // content será Category[]
122+
PaginatedData<Loan> // content será Loan[]
123+
```
124+
125+
TypeScript verifica el tipo en tiempo de compilación, por lo que si intentas acceder a una propiedad que no existe en `Author` dentro de un `PaginatedData<Author>`, el compilador te avisará de inmediato. Obtienes reutilización **y** seguridad de tipos al mismo tiempo.
126+
104127
Con estos objetos creados ya podemos implementar el servicio y sus datos mockeados.
105128

106129
=== "mock-authors.ts"
107130
``` TypeScript
108-
import { AuthorPage } from './model/AuthorPage';
131+
import { PaginatedData } from 'src/app/core/model/page/PaginatedData';
132+
import { Author } from './Author';
109133

110-
export const AUTHOR_DATA: AuthorPage = {
134+
export const AUTHOR_DATA: PaginatedData<Author> = {
111135
content: [
112136
{ id: 1, name: 'Klaus Teuber', nationality: 'Alemania' },
113137
{ id: 2, name: 'Matt Leacock', nationality: 'Estados Unidos' },
@@ -131,7 +155,7 @@ Con estos objetos creados ya podemos implementar el servicio y sus datos mockead
131155
import { Observable, of } from 'rxjs';
132156
import { Pageable } from '../core/model/page/Pageable';
133157
import { Author } from './model/Author';
134-
import { AuthorPage } from './model/AuthorPage';
158+
import { PaginatedData } from 'src/app/core/model/page/PaginatedData';
135159
import { AUTHOR_DATA } from './model/mock-authors';
136160

137161
@Injectable({
@@ -140,7 +164,7 @@ Con estos objetos creados ya podemos implementar el servicio y sus datos mockead
140164
export class AuthorService {
141165
constructor() {}
142166

143-
getAuthors(pageable: Pageable): Observable<AuthorPage> {
167+
getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {
144168
return of(AUTHOR_DATA);
145169
}
146170

@@ -363,18 +387,18 @@ El último paso es definir la pantalla de diálogo que realizará el alta y modi
363387
<form>
364388
<mat-form-field>
365389
<mat-label>Identificador</mat-label>
366-
<input type="text" matInput placeholder="Identificador" [(ngModel)]="author.id" name="id" disabled>
390+
<input type="text" matInput placeholder="Identificador" [(ngModel)]="id" name="id" disabled>
367391
</mat-form-field>
368392

369393
<mat-form-field>
370394
<mat-label>Nombre</mat-label>
371-
<input type="text" matInput placeholder="Nombre del autor" [(ngModel)]="author.name" name="name" required>
395+
<input type="text" matInput placeholder="Nombre del autor" [(ngModel)]="name" name="name" required>
372396
<mat-error>El nombre no puede estar vacío</mat-error>
373397
</mat-form-field>
374398

375399
<mat-form-field>
376400
<mat-label>Nacionalidad</mat-label>
377-
<input type="text" matInput placeholder="Nacionalidad del autor" [(ngModel)]="author.nationality" name="nationality" required>
401+
<input type="text" matInput placeholder="Nacionalidad del autor" [(ngModel)]="nationality" name="nationality" required>
378402
<mat-error>La nacionalidad no puede estar vacía</mat-error>
379403
</mat-form-field>
380404
</form>
@@ -409,7 +433,7 @@ El último paso es definir la pantalla de diálogo que realizará el alta y modi
409433
```
410434
=== "author-edit.component.ts"
411435
``` TypeScript
412-
import { Component, Inject, OnInit } from '@angular/core';
436+
import { Component, inject, OnInit, signal } from '@angular/core';
413437
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
414438
import { AuthorService } from '../author.service';
415439
import { Author } from '../model/Author';
@@ -426,26 +450,32 @@ El último paso es definir la pantalla de diálogo que realizará el alta y modi
426450
styleUrl: './author-edit.component.scss',
427451
})
428452
export class AuthorEditComponent implements OnInit {
429-
author: Author;
430-
431-
constructor(
432-
public dialogRef: MatDialogRef<AuthorEditComponent>,
433-
@Inject(MAT_DIALOG_DATA) public data: any,
434-
private authorService: AuthorService
435-
) {}
453+
protected readonly authorService = inject(AuthorService);
454+
protected readonly dialogRef = inject(MatDialogRef<AuthorEditComponent>);
455+
protected readonly data = inject(MAT_DIALOG_DATA);
456+
457+
protected readonly id = signal<number | null>(null);
458+
protected readonly name = signal<string | null>(null);
459+
protected readonly nationality = signal<string | null>(null);
460+
461+
loadFormData(initialData: Author | null) {
462+
this.id.set(initialData.id ?? null);
463+
this.name.set(initialData.name ?? null);
464+
this.nationality.set(initialData.nationality ?? null);
465+
}
436466

437467
ngOnInit(): void {
438-
this.author = this.data.author ? Object.assign({}, this.data.author) : new Author();
468+
this.loadFormData(this.data.author ?? null);
439469
}
440470

441471
onSave() {
442472
this.authorService.saveAuthor(this.author).subscribe(() => {
443-
this.dialogRef.close();
473+
this.dialogRef.close(true);
444474
});
445475
}
446476

447477
onClose() {
448-
this.dialogRef.close();
478+
this.dialogRef.close(false);
449479
}
450480
}
451481
```
@@ -466,24 +496,24 @@ Una vez implementado front y back, lo que nos queda es modificar el servicio del
466496

467497
=== "author.service.ts"
468498
``` TypeScript hl_lines="13 15 18 22-25 28"
469-
import { Injectable } from '@angular/core';
499+
import { Injectable, inject } from '@angular/core';
470500
import { Observable, of } from 'rxjs';
471501
import { Pageable } from '../core/model/page/Pageable';
472502
import { Author } from './model/Author';
473-
import { AuthorPage } from './model/AuthorPage';
503+
import { PaginatedData } from 'src/app/core/model/page/PaginatedData';
474504
import { AUTHOR_DATA } from './model/mock-authors';
475505
import { HttpClient } from '@angular/common/http';
476506

477507
@Injectable({
478508
providedIn: 'root',
479509
})
480510
export class AuthorService {
481-
constructor(private http: HttpClient) {}
511+
protected readonly http = inject(HttpClient);
482512

483513
private baseUrl = 'http://localhost:8080/author';
484514

485-
getAuthors(pageable: Pageable): Observable<AuthorPage> {
486-
return this.http.post<AuthorPage>(this.baseUrl, { pageable: pageable });
515+
getAuthors(pageable: Pageable): Observable<PaginatedData<Author>> {
516+
return this.http.post<PaginatedData<Author>>(this.baseUrl, { pageable: pageable });
487517
}
488518

489519
saveAuthor(author: Author): Observable<Author> {

0 commit comments

Comments
 (0)