diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie-detail/movie-detail.component.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie-detail/movie-detail.component.ts
new file mode 100644
index 000000000..1940a9429
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie-detail/movie-detail.component.ts
@@ -0,0 +1,25 @@
+import { CurrencyPipe, NgFor, NgIf } from '@angular/common';
+import { Component, Input, OnInit } from '@angular/core';
+import { Movie } from '../movie';
+
+@Component({
+ selector: 'app-movie-detail',
+ standalone: true,
+ imports: [NgIf, NgFor, CurrencyPipe],
+ templateUrl: './movie-detail.component.html',
+ styles: ``
+})
+export class MovieDetailComponent {
+ // Just enough here for the template to compile
+ @Input() movieId: number = 0;
+ errorMessage = '';
+
+ // Movie to display
+ movie: Movie | null = null;
+
+ // Set the page title
+ pageTitle = this.movie ? `Product Detail for: ${this.movie.movieName}` : 'Movie Detail';
+
+ addToCart(movie: Movie) {
+ }
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie-list/movie-list.component.html b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie-list/movie-list.component.html
new file mode 100644
index 000000000..573189e6e
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie-list/movie-list.component.html
@@ -0,0 +1,30 @@
+
+
+
+
+ {{ pageTitle }}
+
+
+
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
+
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie-list/movie-list.component.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie-list/movie-list.component.ts
new file mode 100644
index 000000000..99d4a991c
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie-list/movie-list.component.ts
@@ -0,0 +1,27 @@
+import { NgClass, NgFor, NgIf } from '@angular/common';
+import { Component } from '@angular/core';
+import { MovieDetailComponent } from '../movie-detail/movie-detail.component';
+import { Movie } from '../movie';
+
+@Component({
+ selector: 'app-movie-list',
+ standalone: true,
+ imports: [NgIf, NgClass, NgFor, MovieDetailComponent],
+ templateUrl: './movie-list.component.html',
+ styles: ``,
+})
+export class MovieListComponent {
+ // Just enough here for the template to compile
+ pageTitle = 'Movies';
+ errorMessage = '';
+
+ // Movies
+ movies: Movie[] = [];
+
+ // Selected movie id to highlight the entry
+ selectedMovieId: number = 0;
+
+ onSelected(movieId: number): void {
+ this.selectedMovieId = movieId;
+ }
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie.service.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie.service.ts
new file mode 100644
index 000000000..fa914df41
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie.service.ts
@@ -0,0 +1,8 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class MovieService {
+ private moviesUrl = 'api/movies';
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie.ts
new file mode 100644
index 000000000..d2dca1299
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/movies/movie.ts
@@ -0,0 +1,13 @@
+import { Review } from '../reviews/review';
+
+/* Defines the product entity */
+export interface Movie {
+ id: number;
+ movieName: string;
+ movieCode: string;
+ description: string;
+ price: number;
+ likes?: number;
+ hasReviews?: boolean;
+ reviews?: Review[];
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/reviews/review-data.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/reviews/review-data.ts
new file mode 100644
index 000000000..90d2d2652
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/reviews/review-data.ts
@@ -0,0 +1,77 @@
+import { Review } from './review';
+
+export class ReviewData {
+ static reviews: Review[] = [
+ {
+ id: 1,
+ movieId: 10,
+ userName: 'jackharkness',
+ title: 'Really good',
+ text: "I've beat every level faster with this controller",
+ },
+ {
+ id: 2,
+ movieId: 5,
+ userName: 'thor364',
+ title: 'Not great...',
+ text: 'I summon this film, and it does not heed my call',
+ },
+
+ {
+ id: 3,
+ movieId: 5,
+ userName: 'allthumbs',
+ title: 'Dangerous!',
+ text: 'I almost injured myself with this....',
+ },
+ {
+ id: 4,
+ movieId: 2,
+ userName: 'mom42',
+ title: 'Great for the kiddos',
+ text: 'My kids love it!!',
+ },
+ {
+ id: 5,
+ movieId: 5,
+ userName: 'theoden',
+ title: 'Now for wrath. Now for ruin',
+ text: 'This film (and a dinner bell) worked even better than a horn for drawing attention',
+ },
+ {
+ id: 6,
+ movieId: 5,
+ userName: 'glamdring',
+ title: 'This was no foe-hammer',
+ text: 'Movie was much shorter than expected',
+ },
+ {
+ id: 7,
+ movieId: 10,
+ userName: 'grima',
+ title: 'Nothing but a herald of woe',
+ text: 'I played no better with this controller than my old one',
+ },
+ {
+ id: 8,
+ movieId: 1,
+ userName: 'hama',
+ title: 'Has no evil purpose',
+ text: 'This film is worthy of honor',
+ },
+ {
+ id: 9,
+ movieId: 1,
+ userName: 'hama',
+ title: 'More than a movie',
+ text: 'The movie makes me cry...',
+ },
+ {
+ id: 10,
+ movieId: 1,
+ userName: 'eowyn',
+ title: 'A necessity!',
+ text: 'Those without expectations can still die upon them',
+ },
+ ];
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/reviews/review.service.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/reviews/review.service.ts
new file mode 100644
index 000000000..521e6ab22
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/reviews/review.service.ts
@@ -0,0 +1,12 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ReviewService {
+ private reviewUrl = 'api/reviews';
+
+ getReviewUrl(movieId: number): string {
+ return `${this.reviewUrl}?movieId=^${movieId}$`;
+ }
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/reviews/review.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/reviews/review.ts
new file mode 100644
index 000000000..c56e6c26a
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/reviews/review.ts
@@ -0,0 +1,8 @@
+/* Defines a product review */
+export interface Review {
+ id: number;
+ movieId: number;
+ userName: string;
+ title: string;
+ text: string;
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/utilities/http-error.service.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/utilities/http-error.service.ts
new file mode 100644
index 000000000..83b97b3c7
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/utilities/http-error.service.ts
@@ -0,0 +1,26 @@
+import { HttpErrorResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class HttpErrorService {
+ formatError(err: HttpErrorResponse): string {
+ return this.httpErrorFormatter(err);
+ }
+
+ private httpErrorFormatter(err: HttpErrorResponse): string {
+ // In a real world app, we may send the error to some remote logging infrastructure
+ // instead of just logging it to the console
+ // console.error(err);
+ let errorMessage = '';
+ if (err.error instanceof ErrorEvent) {
+ // A client-side or network error occurred. Handle it accordingly.
+ errorMessage = `An error occurred: ${err.error.message}`;
+ } else {
+ // The backend returned an unsuccessful response code.
+ errorMessage = `Server returned code: ${err.status}, error message is: ${err.statusText}`;
+ }
+ return errorMessage;
+ }
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/utilities/page-not-found.component.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/utilities/page-not-found.component.ts
new file mode 100644
index 000000000..147d1429b
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/app/utilities/page-not-found.component.ts
@@ -0,0 +1,12 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-page-not-found',
+ standalone: true,
+ imports: [],
+ template: `
This is not the page you were looking for!
`,
+ styles: ``
+})
+export class PageNotFoundComponent {
+
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/index.html b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/index.html
new file mode 100644
index 000000000..290d61b73
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Movies
+
+
+
+
+
+
+
+
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/main.ts b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/main.ts
new file mode 100644
index 000000000..5df75f9c8
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/main.ts
@@ -0,0 +1,6 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import { App } from './app/app';
+
+bootstrapApplication(App, appConfig)
+ .catch((err) => console.error(err));
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/styles.css b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/styles.css
new file mode 100644
index 000000000..78be18673
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/src/styles.css
@@ -0,0 +1,9 @@
+/* You can add global styles to this file, and also import other style files */
+div.card-header {
+ font-size: large;
+}
+
+div.card {
+ margin-top: 20px;
+ margin-left: 10px;
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/tsconfig.app.json b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/tsconfig.app.json
new file mode 100644
index 000000000..264f459bf
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/tsconfig.app.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "include": [
+ "src/**/*.ts"
+ ],
+ "exclude": [
+ "src/**/*.spec.ts"
+ ]
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/tsconfig.json b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/tsconfig.json
new file mode 100644
index 000000000..e4955f26b
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/tsconfig.json
@@ -0,0 +1,34 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "experimentalDecorators": true,
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "preserve"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "typeCheckHostBindings": true,
+ "strictTemplates": true
+ },
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/tsconfig.spec.json b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/tsconfig.spec.json
new file mode 100644
index 000000000..940b30a5f
--- /dev/null
+++ b/04-frameworks/02-angular/11-rxjs/00-start-code-20/movies/tsconfig.spec.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "src/**/*.d.ts",
+ "src/**/*.spec.ts"
+ ]
+}
diff --git a/04-frameworks/02-angular/version-20/02-list-users/readme_es.md b/04-frameworks/02-angular/version-20/02-list-users/readme_es.md
new file mode 100644
index 000000000..a41f7d790
--- /dev/null
+++ b/04-frameworks/02-angular/version-20/02-list-users/readme_es.md
@@ -0,0 +1,247 @@
+# 01 Listado de usuarios
+
+## Resumen
+
+Este ejemplo toma como punto de partida una aplicación recién creada.
+
+Vamos a pedirle a la API de Github la lista de miembros que pertenece a una
+organización y vamos a mostrarla por pantalla.
+
+Que vamos a aprender en este ejemplo:
+
+- Cómo crear un componente de visualización
+- Cómo hacer una llámada asícnrona para pedir datos a una api remota.
+- Cómo iterar y mostrar una lista de resultados con la directiva estructural `*ngFor`
+
+También aprenderemos posteriormente la librería de Angular para realizar peticiones a APIs remotas.
+
+Pasos:
+
+## Paso a Paso
+
+- Primero copiamos el ejemplo anterior, y hacemos un _npm install_
+
+```bash
+npm install
+```
+
+> NOTA: Podemos usar `npm ci` de esta forma instalaremos las dependencias existentes en `package-lock.json` en vez de regenerarlas.
+
+- Arrancamos la aplicación:
+
+```bash
+ng serve
+```
+
+- Si queremos ver que tipo de datos vamos a manejar, podemos abrir el navegador web y ver que devuelve la API Rest de Github
+
+```bash
+https://api.github.com/orgs/lemoncode/members
+```
+
+- Creamos nuestro modelo
+
+Creamos un directorio _model_ dentro de _src/app_ y en dicho directorio creamos un fichero _model.ts_ con el interfaz de nuestro modelo:
+
+_./src/app/model/index.ts_
+
+```typescript
+export interface MemberEntity {
+ avatar_url: string;
+ id: string;
+ login: string;
+}
+```
+
+- Utilizamos el CLI de Angular para crear el componente
+
+```bash
+ng g c user/user-list
+```
+
+- Incluimos el componente en la página de nuestra aplicación
+
+Abrimos el fichero .ts del componente recién creado (_src/app/user/user-list/user-list.component.ts_) y nos fijamos en cuál es su selector. (_app-user-list_).
+
+Abrimos el fichero app.component.html y ponemos el tag donde queramos que aparezca nuestro componente.
+
+_src/app/app.component.ts_
+
+```diff
+@Component({
+ selector: 'app-root',
+ standalone: true,
+- imports: [CommonModule, RouterOutlet, MenuComponent, SearchComponent],
+ imports: [
+ CommonModule,
+ RouterOutlet,
+ MenuComponent,
+ SearchComponent,
++ UserListComponent,
+ ],
+ templateUrl: './app.component.html',
+ styleUrl: './app.component.css',
+})
+```
+
+_src/app/app.component.html_
+
+```diff
+
Hola Mundo
+
+
+
+-
+
++
+```
+
+Y comprobamos en el navegador que el componente aparece en la pantalla.
+
+- HTML de partida
+
+Creamos el HTML necesario para mostrar una lista de usuarios. El siguiente código podría valer como ejemplo:
+
+_src/app/user/user-list/user-list.component.html_
+
+```html
+
+
+
+
Avatar
+
Id
+
Name
+
+
+
+
+
+
+
+
+ xxxxxxxx
+
+
+ xxxxxxxx
+
+
+
+
+```
+
+- Obtener la lista de miembros
+
+El HTML necesita los datos de los miembros.
+
+Recordemos que en Angular la plantilla (html) de un componente tiene acceso a las propiedades públicas y métodos públicos de SU clase componente (ts).
+
+Por lo tanto la misión del ts será conseguir la lista de miembros y guardarla en una propiedad pública de su clase.
+
+_src/app/user/user-list/user-list.component.ts_
+
+```ts
+import { Component, OnInit } from '@angular/core';
+import { MemberEntity } from '../../model';
+
+@Component({
+ selector: 'app-user-list',
+ standalone: true,
+ imports: [],
+ templateUrl: './user-list.component.html',
+ styleUrl: './user-list.component.css',
+})
+export class UserListComponent implements OnInit {
+ members: MemberEntity[] = [];
+
+ ngOnInit(): void {
+ fetch('https://api.github.com/orgs/lemoncode/members')
+ .then((response) => response.json())
+ .then((result) => (this.members = result));
+ }
+}
+
+```
+
+- Iterar la lista en el html para mostrar los datos.
+
+Con la directiva estructural *ngFor recorremos el listado de miembros
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
+
+
+
Avatar
+
Id
+
Name
+
+
+
+-
+-
+-
+-
+-
+- xxxxxxxx
+-
+-
+- xxxxxxxx
+-
+-
++ @for (member of members; track member) {
++
++
++
++
++
++ {{member.id}}
++
++
++ {{member.login}}
++
++
+ }
+
+
+```
+
+Añadimos estilos para visualizar mejor los elementos de la tabl:
+
+_src/app/user/user-list/user-list.component.css_
+
+```css
+/* Estilos generales para la tabla */
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 20px; /* Ajusta el margen según sea necesario */
+}
+
+/* Estilos para las celdas del encabezado */
+th {
+ background-color: #f2f2f2; /* Color de fondo del encabezado */
+ padding: 10px;
+ text-align: left;
+}
+
+/* Estilos para las celdas del cuerpo de la tabla */
+td {
+ padding: 10px;
+ border-bottom: 1px solid #ddd; /* Línea divisoria entre filas */
+}
+
+/* Estilos para las imágenes dentro de las celdas */
+img {
+ max-width: 50px; /* Ajusta el tamaño máximo de las imágenes */
+ border-radius: 50%; /* Borde redondeado para las imágenes */
+}
+
+/* Estilos para las filas impares, para mejorar la legibilidad */
+tbody tr:nth-child(odd) {
+ background-color: #f9f9f9; /* Fondo alternativo para filas impares */
+}
+
+```
+
+Y ya está: _members_ empieza siendo una lista vacía [] y en el html se reflejará por lo tanto una tabla sin filas, pero en cuanto la petición a la API devuelva datos y los guardemos en la propiedad members del ts, Angular actualizará el HTML y veremos los datos en la pantalla.
diff --git a/04-frameworks/02-angular/version-20/03-directives/readme_es.md b/04-frameworks/02-angular/version-20/03-directives/readme_es.md
new file mode 100644
index 000000000..9a12d5b04
--- /dev/null
+++ b/04-frameworks/02-angular/version-20/03-directives/readme_es.md
@@ -0,0 +1,542 @@
+# 02 Directiva de atributo
+
+## Resumen
+
+Este ejemplo toma como punto de partida el ejemplo 01.
+
+Vamos a crear una directiva que **resalte elementos HTML** al pasar el ratón por encima del elemento. La directiva nos permitirá elegir el color de resaltado.
+
+Qué vamos a aprender en este ejemplo:
+
+- Cómo crear una directiva personalizada
+- Cómo parametrizar una directiva
+
+Pasos:
+
+## Paso a Paso
+
+- Primero copiamos el ejemplo anterior, y hacemos un _npm install_
+
+```bash
+npm install
+```
+
+> NOTA: Podemos usar `npm ci` de esta forma instalaremos las dependencias existentes en `package-lock.json` en vez de regenerarlas.
+
+- Arrancamos la aplicación:
+
+```bash
+ng serve
+```
+
+- Utilizamos el CLI de Angular para crear la directiva
+
+```bash
+ng g d directives/highlight
+```
+
+Este es el archivo geneado por el CLI
+
+_src/app/directives/highlight.ts_
+
+```ts
+import { Directive } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true
+})
+export class HighlightDirective {
+
+ constructor() { }
+
+}
+```
+
+Como se puede apreciar, una directiva es una clase decorada con la función decoradora `@Directive`.
+
+El objeto de metadatos de este decorador es simple, un objeto con una propiedad _selector_ en el que, con sintaxis de selectores css, configuramos qué elementos van a verse afectados por esta directiva.
+
+Con el decorador del ejemplo anterior le decimos a Angular que debe aplicar esta directiva a todos los elementos del DOM que tengan el ATRIBUTO _appHighlight_.
+
+- Ponemos la directiva en los elementos html en los que queramos que se aplique. Voy a poner la directiva en un elemento del listado de usuarios y en otro elemento del menú.
+
+_src/app/user/user-list/user-list.component.ts_
+
+```diff
++import { Highlight } from '../../directives/highlight';
+
+@Component({
+ selector: 'app-user-list',
+ standalone: true,
+ imports: [
++ Highlight
+ ],
+ templateUrl: './user-list.component.html',
+ styleUrl: './user-list.component.css',
+})
+```
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
+```
+
+Angular aplicará la directiva tanto al `th` del listado de usuarios como al `p` del menú.
+
+- Programamos la directiva
+
+La potencia que tienen las directivas de atributo es que Angular les puede pasar como argumento del constructor una _referencia_ al elemento sobre el que se está aplicando.
+
+_src/app/directives/highlight.ts_
+
+```diff
+- import { Directive } from '@angular/core';
++ import { Directive, ElementRef } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true
+})
+export class HighlightDirective {
+
+- constructor() { }
++ constructor(el: ElementRef) { }
+
+}
+```
+
+De esta forma, tendremos en la variable _el_ una referencia al elemento th en el caso del listado de usuarios y una referencia al elemento p en el caso del menú.
+
+`ElementRef` tiene una única propiedad, _nativeElement_, que nos da acceso al elemento en sí. A través de _nativeElement_ podemos acceder y modificar cualquier atributo de elemento en cuestión.
+
+Vamos a modificar el valor de `style.backgroundColor`:
+
+_src/app/directives/highlight.ts_
+
+```diff
+import { Directive, ElementRef } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true,
+})
+export class Highlight {
+
+- constructor(el: ElementRef) { }
++ constructor(el: ElementRef) {
++ (el.nativeElement as HTMLElement).style.backgroundColor = 'lightblue';
++ }
+
+}
+```
+
+Si miramos el navegador veremos que Angular a cambiado el valor de `style.backgroundColor` a todos los elementos que tengan el atributo _appHighlight_.
+
+- Modificar la directiva para que solamente se resalte cuando pongamos el ratón encima del elemento
+
+Vamos a reescribir la directiva para que los elementos no estén resaltados de inicio, sino que se resalten cuando el ratón se ponga encima de ellos.
+
+Para ello, desde la directiva vamos a escuchar el evento _mouseenter_ de los elementos afectados por la directiva.
+
+Esto lo hacemos con el decorador **@HostListener**. Este decorador configura un método para ser el _listener_ de un evento ocurrido en el elemento del DOM que haga de anfitrión (_host_) de la directiva.
+
+```diff
+- import { Directive, ElementRef } from '@angular/core';
++ import { Directive, ElementRef, HostListener } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true,
+})
+export class HighlightDirective {
+
+- constructor(el: ElementRef) {
++ constructor(public el: ElementRef) {
+- (el.nativeElement as HTMLElement).style.backgroundColor = 'lightblue';
+ }
+
++ @HostListener('mouseenter')
++ onMouseEnterEvent() {
++ (this.el.nativeElement as HTMLElement).style.backgroundColor = 'lightblue';
++ }
+
+}
+```
+
+Y para que desde `onMouseEnterEvent()` podamos acceder a la referencia (_el_) que llega a través del constructor, almacenamos la variable _el_ en una propiedad de la clase.
+
+```diff
+import { Directive, ElementRef, HostListener } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true,
+})
+export class HighlightDirective {
+
++ private el: ElementRef;
+ constructor(el: ElementRef) {
++ this.el = el;
+ }
+
+ @HostListener('mouseenter')
+ onMouseEnterEvent() {
+- el.nativeElement.style.backgroundColor = 'lightblue';
++ (this.el.nativeElement as HTMLElement).style.backgroundColor = 'lightblue';
+ }
+
+}
+```
+
+Aprovechándonos de los **constructores breves** que nos aporta el lenguaje typescript, podemos ahorrarnos un par de líneas en el código si indicamos la visibilidad (public/private) en el propio argumento del constructor
+
+```diff
+import { Directive, ElementRef, HostListener } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true,
+})
+export class Highlight {
+
+- private el: ElementRef;
+- constructor(el: ElementRef) {
+- this.el = el;
+- }
+
++ constructor(private el: ElementRef) { }
+
+ @HostListener('mouseenter')
+ onMouseEnterEvent() {
+ (this.el.nativeElement as HTMLElement).style.backgroundColor = 'lightblue';
+ }
+
+}
+```
+
+Podemos comprobar que los elementos se resaltan cuando el ratón se ponga encima del elemento.
+
+- Modificar la directiva para que solamente se resalte cuando pongamos el ratón encima del elemento
+
+De forma análoga podemos programar que el elemento deje de estar resaltado cuando el ratón salga del área ocupada por el elemento.
+
+```diff
+import { Directive, ElementRef, HostListener } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true,
+})
+export class Highlight {
+
+ constructor(private el: ElementRef) { }
+
+ @HostListener('mouseenter')
+ onMouseEnterEvent() {
+ (this.el.nativeElement as HTMLElement).style.backgroundColor = 'lightblue';
+ }
++
++ @HostListener('mouseleave')
++ onMouseLeaveEvent() {
++ this.el.nativeElement.style.backgroundColor = '';
++ }
++
+}
+```
+
+- Refactorización
+
+Quitamos algo de código repetido y nos llevamos ese _magic value_ que es 'lightblue' a un lugar que sea más accesible y rápido de identificar.
+
+
+```diff
+import { Directive, ElementRef, HostListener } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true,
+})
+export class Highlight {
+
++ color = 'lightblue';
+ constructor(private el: ElementRef) { }
+
++ private highlight(color: string) {
++ (this.el.nativeElement as HTMLElement).style.backgroundColor = color;
++ }
+
+ @HostListener('mouseenter')
+ onMouseEnterEvent() {
+- (this.el.nativeElement as HTMLElement).style.backgroundColor = 'lightblue';
++ this.highlight(this.color);
+ }
+
+ @HostListener('mouseleave')
+ onMouseLeaveEvent() {
+- (this.el.nativeElement as HTMLElement).style.backgroundColor = '';
++ this.highlight('');
+ }
+
+}
+```
+
+- Aplicamos el decorador @Input para parametrizar el color de resaltado
+
+Con otro decorador llamado @Input podemos configurar una propiedad de nuestra clase HighlightDirective para que su valor pueda ser establecido desde fuera.
+
+```diff
+- import { Directive, ElementRef, HostListener } from '@angular/core';
++ import { Directive, ElementRef, HostListener, Input } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]'
+})
+export class Highlight {
+
+- color = 'lightblue';
++ @Input()
++ public color!: string;
+
+ constructor(private el: ElementRef) { }
+
+ private highlight(color: string) {
+ (this.el.nativeElement as HTMLElement).style.backgroundColor = color;
+ }
+
+ @HostListener('mouseenter')
+ onMouseEnterEvent() {
+ this.highlight(this.color);
+ }
+
+ @HostListener('mouseleave')
+ onMouseLeaveEvent() {
+ this.highlight('');
+ }
+
+}
+```
+
+Este decorador `@Input` hace que los elementos afectados por `appHighlight` dispongan de un nuevo atributo, llamado _color_ (mismo nombre que la propiedad decorada con `@Input`). El valor del atributo color en el html se establecerá como valor de la propiedad en la directiva.
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
+```
+
+Turno de probarlo.
+
+- Ponemos un color por defecto
+
+Con un poco de javascript podemos elegir un color por defecto en caso de que no se haya usado el atributo color.
+
+```diff
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true,
+})
+export class HighlightDirective {
+
+ @Input()
+ public color!: string;
+
++ private defaultColor = 'lightblue';
+
+ constructor(private el: ElementRef) { }
+
+ private highlight(color: string) {
+ (this.el.nativeElement as HTMLElement).style.backgroundColor = color;
+ }
+
+ @HostListener('mouseenter')
+ onMouseEnterEvent() {
+- this.highlight(this.color);
++ this.highlight(this.color || this.defaultColor);
+ }
+
+ @HostListener('mouseleave')
+ onMouseLeaveEvent() {
+ this.highlight('');
+ }
+
+}
+```
+
+Podemos tener tantas propiedades decoradas con `@Input()` como queramos.
+
+Cabe recordar que podemos utilizar binding en el html para asociar propiedades/atributos con propiedades del typescript del componente:
+
+_src/app/layout/menu/menu.component.html_
+
+```diff
+-
menu works!
++
menu works!
+```
+
+_src/app/layout/menu/menu.component.ts_
+
+```ts
+import { Component } from '@angular/core';
+import { Highlight } from '../../directives/highlight.directive';
+
+@Component({
+ selector: 'app-menu',
+ standalone: true,
+ imports: [Highlight],
+ templateUrl: './menu.component.html',
+ styleUrl: './menu.component.css',
+})
+export class MenuComponent {
+ /*diff*/
+ public variableColorEnElTS = 'pink';
+ /*diff*/
+}
+
+```
+
+Así hacemos que sea el typescript del componente el que decida cuál es el color con el que la directiva debe resaltar el elemento.
+
+- Aprovechar el propio atributo de la directiva como `@Input` para el color.
+
+El decorador `@Input` permite realizar la asociación aunque los nombres del atributo html y de la propiedad de la directiva no coincidan. Por ejemplo:
+
+_src/app/layout/menu/menu.component.html_
+
+```diff
+-
menu works!
+-
menu works!
++
menu works!
+```
+
+```diff
+export class Highlight {
+
++ @Input('colorResaltado')
+ public color!: string;
+```
+
+Esto se suele utilizar para aprovechar el propio atributo `appHighlight` para pasar uno de los parámetros que tengamos con @Input.
+
+_src/app/layout/menu/menu.component.html_
+
+```diff
+-
menu works!
++
menu works!
+```
+
+```diff
+import { Directive, ElementRef, HostListener, Input } from '@angular/core';
+
+@Directive({
+ selector: '[appHighlight]',
+ standalone: true,
+})
+export class Highlight {
+
+- @Input()
++ @Input('appHighlight')
+ public color;
+
+ private defaultColor = 'lightblue';
+
+ constructor(private el: ElementRef) { }
+
+ private highlight(color: string) {
+ this.el.nativeElement.style.backgroundColor = color;
+ }
+
+ @HostListener('mouseenter')
+ onMouseEnterEvent() {
+ this.highlight(this.color || this.defaultColor);
+ }
+
+ @HostListener('mouseleave')
+ onMouseLeaveEvent() {
+ this.highlight('');
+ }
+
+}
+```
diff --git a/04-frameworks/02-angular/version-20/04-template-forms/readme_es.md b/04-frameworks/02-angular/version-20/04-template-forms/readme_es.md
new file mode 100644
index 000000000..18a9baef2
--- /dev/null
+++ b/04-frameworks/02-angular/version-20/04-template-forms/readme_es.md
@@ -0,0 +1,410 @@
+# 03 Template Forms
+
+## Resumen
+
+Este ejemplo toma como punto de partida el ejemplo 02.
+
+Vamos a crear un formulario para añadir miembros a la lista.
+
+Qué vamos a aprender en este ejemplo:
+
+- Formularios template-driven
+- La directiva `ngModel`
+- Directivas de atributo para validaciones
+- Variables de plantilla
+- Resaltar con CSS los campos no válidos
+- Mostrar mensajes de error
+
+Pasos:
+
+## Paso a Paso
+
+- Primero copiamos el ejemplo anterior, y hacemos un _npm install_
+
+```bash
+npm install
+```
+
+> NOTA: Podemos usar `npm ci` de esta forma instalaremos las dependencias existentes en `package-lock.json` en vez de regenerarlas.
+
+- Arrancamos la aplicación:
+
+```bash
+ng serve
+```
+
+- Añadimos html de formulario de alta
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
++
Alta
++
++
++
+
++
Listado
++
+
+
+
+
Avatar
+
Id
+
Name
+
+
+
+ @for (member of members; track member) {
+
+
+
+
+
+ {{member.id}}
+
+
+ {{member.login}}
+
+
+ }
+
+
++
+```
+
+- Creamos una variable en el componente de tipo _MemberEntity_, cuyas propiedades bindearemos con los inputs del formulario, y la inicializamos para no tener errores por intentos de acceso a propiedades de objetos _null_.
+
+_src/app/user/user-list/user-list.component.ts_
+
+```diff
+export class UserListComponent implements OnInit {
+ members: MemberEntity[] = [];
++ newMember!: MemberEntity;
+
+ ngOnInit(): void {
+ fetch('https://api.github.com/orgs/lemoncode/members')
+ .then((response) => response.json())
+ .then((result) => (this.members = result));
++
++ this.newMember = {
++ id: '',
++ login: '',
++ avatar_url: '',
++ };
+ }
+}
+```
+
+- Usamos la directiva `ngModel` para hacer doble binding con los inputs de id y de nombre, para ello importamos el módulo `FormsModule`
+
+_src/app/user/user-list/user-list.component.ts_
+
+```diff
+@Component({
+ selector: 'app-user-list',
+ standalone: true,
+- imports: [Highlight],
++ imports: [Highlight, FormsModule],
+ templateUrl: './user-list.component.html',
+ styleUrl: './user-list.component.css',
+})
+```
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
Alta
+
+
+
+```
+
+- Hacemos binding del evento click del botón con un método en el componente que añada el nuevo miembro a la lista
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
+```
+
+- Aprovechamos las clases `ng-xxxxx` que pone `ngModel` a los input para dar estilos CSS según el estado de validez de los campos
+
+_src/app/user/user-list/user-list.component.css_
+
+```css
+.ng-valid[required], .ng-valid.required {
+ border-left: 5px solid #42A948;
+}
+
+.ng-invalid:not(form) {
+ border-left: 5px solid #a94442;
+}
+```
+
+- Utilizamos template reference variables (extendidas de la clase `ngModel`) para incluir mensajes de error
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
Alta
+
+
+
+```
+
+- Utilizamos otra variable de plantilla (esta vez extendida de la directiva `ngForm`) para deshabilitar el botón si algún campo no es válido:
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
Alta
+
+-
+
+```
+
+- El contenido de la propiedad value de un input de tipo file no se puede bindear con la propiedad `avatar_url` porque en nuestro caso no contienen el mismo tipo de información. Gestionamos el input file "manualmente" con el evento _change_ y un poco de javascript en el componente.
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
Alta
+
+
+
+```
+
+
+_src/app/user/user-list/user-list.component.ts_
+
+```diff
+export class UserListComponent implements OnInit {
+ members: MemberEntity[] = [];
+ newMember!: MemberEntity;
+
+ add(): void {
+ this.members.push(this.newMember);
+ this.newMember = {
+ id: '',
+ login: '',
+ avatar_url: '',
+ };
+ }
++
++ handleFileInput($event: any) {
++ const files = $event.target.files as FileList;
++ const reader = new FileReader();
++ reader.readAsDataURL(files[0]);
++ reader.onload = () => {
++ this.newMember.avatar_url = reader.result as string;
++ };
++ }
++
+ ngOnInit(): void {
+ fetch('https://api.github.com/orgs/lemoncode/members')
+ .then((response) => response.json())
+ .then((result) => (this.members = result));
+
+ this.newMember = {
+ id: '',
+ login: '',
+ avatar_url: '',
+ };
+ }
+}
+```
diff --git a/04-frameworks/02-angular/version-20/05-pipes/readme_es.md b/04-frameworks/02-angular/version-20/05-pipes/readme_es.md
new file mode 100644
index 000000000..006f38684
--- /dev/null
+++ b/04-frameworks/02-angular/version-20/05-pipes/readme_es.md
@@ -0,0 +1,196 @@
+# 04 Pipes
+
+## Resumen
+
+Este ejemplo toma como punto de partida el ejemplo 03.
+
+Vamos a crear una pipe para realizar búsquedas sobre la lista de miembros
+
+Qué vamos a aprender en este ejemplo:
+
+- Creación de pipes personalizadas
+- Utilización de pipes
+- Utilización de variables de plantilla
+
+Pasos:
+
+## Paso a Paso
+
+- Primero copiamos el ejemplo anterior, y hacemos un _npm install_
+
+```bash
+npm install
+```
+
+> NOTA: Podemos usar `npm ci` de esta forma instalaremos las dependencias existentes en `package-lock.json` en vez de regenerarlas.
+
+- Arrancamos la aplicación:
+
+```bash
+ng serve
+```
+
+- Creamos una caja para buscar por nombre
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
Listado
+
++
++
+
+
+
+
Avatar
+
Id
+
Name
+
+
+
+
+
+
+
+
+ {{ member.id }}
+
+
+ {{ member.login }}
+
+
+
+
+
+```
+
+- Creamos una pipe utilizando el CLI
+
+```bash
+ng generate pipe pipes/search-by-login
+```
+
+- Programamos la pipe
+
+La función `transform` de la pipe recibirá la lista de miembros sobre la que realizar la búsqueda y el valor por el que buscar. Devolverá una lista con los miembros cuyo campo contenga el valor buscado.
+
+_src/app/pipes/search-by-login.pipe.ts_
+
+```diff
+import { Pipe, PipeTransform } from '@angular/core';
++import { MemberEntity } from '../models';
+
+@Pipe({
+ name: 'searchByLogin',
+ standalone: true,
+})
+export class SearchByLoginPipe implements PipeTransform {
+
+- transform(value: unknown, ...args: unknown[]): unknown {
+- return null;
+- }
++ transform(members: MemberEntity[], value: string): MemberEntity[] {
++ return members.filter(
++ member => member.login.toLowerCase().includes(value.toLowerCase())
++ );
++ }
+
+}
+```
+
+- Aplicamos la pipe sobre la lista de miembros.
+
+El valor buscado lo obtenemos con una variable de plantilla declarada en el input de búsqueda.
+
+_src/app/user/user-list/user-list.component.ts_
+
+```diff
+import { Component, OnInit } from '@angular/core';
+import { MemberEntity } from '../../model';
+import { NgFor, NgIf } from '@angular/common';
+import { Highlight } from '../../directives/highlight';
+import { FormsModule } from '@angular/forms';
++import { SearchByLoginPipe } from '../../pipes/search-by-login.pipe';
+
+@Component({
+ selector: 'app-user-list',
+ standalone: true,
+- imports: [Highlight, FormsModule],
++ imports: [Highlight, FormsModule, SearchByLoginPipe],
+ templateUrl: './user-list.component.html',
+ styleUrl: './user-list.component.css',
+})
+```
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
Listado
+
+
+-
++
+
+
+
+
Avatar
+
Id
+
Name
+
+
+
+-
++
+
+
+
+
+ {{ member.id }}
+
+
+ {{ member.login }}
+
+
+
+
+
+```
+
+Pero no ocurre nada, no está filtrando. Angular no encuentra motivo para revisar el html (que es cuando ejecutaría la pipe).
+
+- Nos enlazamos al evento que nos interese de la caja de búsqueda, para que cuando ocurra el evento Angular revise el html y se ejecute la pipe.
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
Listado
+
+
+-
++
+
+
+
+
Avatar
+
Id
+
Name
+
+
+
+
+
+
+
+
+ {{ member.id }}
+
+
+ {{ member.login }}
+
+
+
+
+
+```
+
+Y ahora sí, con cada pulsación de tecla, se realiza la búsqueda.
diff --git a/04-frameworks/02-angular/version-20/06-services/readme_es.md b/04-frameworks/02-angular/version-20/06-services/readme_es.md
new file mode 100644
index 000000000..76f2204d6
--- /dev/null
+++ b/04-frameworks/02-angular/version-20/06-services/readme_es.md
@@ -0,0 +1,107 @@
+# 05 Servicios
+
+## Resumen
+
+Este ejemplo toma como punto de partida el ejemplo 04.
+
+Vamos a crear un servicio que se encargue de conseguir la lista de miembros:
+
+Qué vamos a aprender en este ejemplo:
+
+- Creación de servicios
+- Inyección de servicios
+
+Pasos:
+
+## Paso a Paso
+
+- Primero copiamos el ejemplo anterior, y hacemos un _npm install_
+
+```bash
+npm install
+```
+
+> NOTA: Podemos usar `npm ci` de esta forma instalaremos las dependencias existentes en `package-lock.json` en vez de regenerarlas.
+
+- Arrancamos la aplicación:
+
+```bash
+ng serve
+```
+
+- Creamos un servicio con el CLI
+
+```bash
+ng generate service services/members
+```
+
+- Programamos el servicio
+
+Creamos un método que nos devuelva una promesa de la lista de miembros.
+
+_src/app/services/members.service.ts_
+
+```diff
+import { Injectable } from '@angular/core';
++import { MemberEntity } from '../model/MemberEntity';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class MembersService {
+
+ constructor() { }
+
++ getAll(): Promise {
++ return fetch(`https://api.github.com/orgs/lemoncode/members`)
++ .then((response) => response.json());
++ }
+}
+```
+
+- Inyectamos el servicio en el componente
+
+_src/app/user/user-list/user-list.component.ts_
+
+```diff
+import { Component, OnInit } from '@angular/core';
+import { MemberEntity } from '../../model';
+import { NgFor, NgIf } from '@angular/common';
+import { HighlightDirective } from '../../directives/highlight.directive';
+import { FormsModule } from '@angular/forms';
+import { SearchByLoginPipe } from '../../pipes/search-by-login.pipe';
++import { MembersService } from '../../services/members.service';
+
+@Component({
+ selector: 'app-user-list',
+ standalone: true,
+ imports: [NgFor, HighlightDirective, FormsModule, NgIf, SearchByLoginPipe],
+ templateUrl: './user-list.component.html',
+ styleUrl: './user-list.component.css',
+})
+export class UserListComponent implements OnInit {
+ members: MemberEntity[] = [];
+ newMember!: MemberEntity;
+
++ constructor(private membersService: MembersService) {}
+
+```
+
+- Pedimos los usuarios al servicio en vez de cogerlos directamente en el componente:
+
+_src/app/user/user-list/user-list.component.ts_
+
+```diff
+ ngOnInit(): void {
+- fetch('https://api.github.com/orgs/lemoncode/members')
+- .then((response) => response.json())
+- .then((result) => (this.members = result));
++ this.membersService.getAll().then((members) => (this.members = members));
++
+ this.newMember = {
+ id: '',
+ login: '',
+ avatar_url: '',
+ };
+ }
+```
diff --git a/04-frameworks/02-angular/version-20/07-reactive-form/readme_es.md b/04-frameworks/02-angular/version-20/07-reactive-form/readme_es.md
new file mode 100644
index 000000000..749bce554
--- /dev/null
+++ b/04-frameworks/02-angular/version-20/07-reactive-form/readme_es.md
@@ -0,0 +1,503 @@
+# 06 Formularios reactivos (model-driven)
+
+## Resumen
+
+Este ejemplo toma como punto de partida el ejemplo 06.
+
+Vamos a crear un formulario model-driven para la edición de miembros:
+
+Qué vamos a aprender en este ejemplo:
+
+- Creación de formularios model-driven
+- Validadores model-driven
+- Directivas para formularios model-driven
+- La propiedad valueChanges
+
+Pasos:
+
+## Paso a Paso
+
+- Primero copiamos el ejemplo anterior, y hacemos un _npm install_
+
+```bash
+npm install
+```
+
+> NOTA: Podemos usar `npm ci` de esta forma instalaremos las dependencias existentes en `package-lock.json` en vez de regenerarlas.
+
+- Arrancamos la aplicación:
+
+```bash
+ng serve
+```
+
+- Importamos el módulo `ReactiveFormsModule`
+
+_src/app/user/user-list/user-list.component.ts_
+
+```diff
+import { Component, OnInit } from '@angular/core';
+import { MemberEntity } from '../../model';
+import { Highlight } from '../../directives/highlight.directive';
+-import { FormsModule } from '@angular/forms';
++import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { SearchByLoginPipe } from '../../pipes/search-by-login.pipe';
+import { MembersService } from '../../services/members.service';
+
+@Component({
+ selector: 'app-user-list',
+ standalone: true,
+ imports: [
+ Highlight,
+ FormsModule,
+ SearchByLoginPipe,
++ ReactiveFormsModule,
+ ],
+ templateUrl: './user-list.component.html',
+ styleUrl: './user-list.component.css',
+})
+```
+
+- Inyectamos el servicio `FormBuilder` en el ts
+
+_src/app/user/user-list/user-list.component.ts_
+
+```diff
+import { Component, OnInit } from '@angular/core';
+import { MemberEntity } from '../../model';
+import { Highlight } from '../../directives/highlight.directive';
+-import { FormsModule, ReactiveFormsModule } from '@angular/forms';
++import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { SearchByLoginPipe } from '../../pipes/search-by-login.pipe';
+import { MembersService } from '../../services/members.service';
+
+@Component({
+ selector: 'app-user-list',
+ standalone: true,
+ imports: [
+ Highlight,
+ FormsModule,
+ SearchByLoginPipe,
+ ReactiveFormsModule,
+ ],
+ templateUrl: './user-list.component.html',
+ styleUrl: './user-list.component.css',
+})
+export class UserListComponent implements OnInit {
+ members: MemberEntity[] = [];
+ newMember!: MemberEntity;
+
+- constructor(private membersService: MembersService) {}
++ constructor(private membersService: MembersService, private fb: FormBuilder) {}
+ // ....
+```
+
+- Construimos un formulario en el ts
+
+_src/app/user/user-list/user-list.component.ts_
+
+```ts
+import { Component, OnInit } from "@angular/core";
+import { MemberEntity } from "../../model";
+import { NgFor, NgIf } from "@angular/common";
+import { Highlight } from "../../directives/highlight.directive";
+import {
+ FormBuilder,
+ /*diff*/
+ FormControl,
+ FormGroup,
+ /*diff*/
+ FormsModule,
+ ReactiveFormsModule,
+ /*diff*/
+ Validators,
+ /*diff*/
+} from "@angular/forms";
+import { SearchByLoginPipe } from "../../pipes/search-by-login.pipe";
+import { MembersService } from "../../services/members.service";
+
+@Component({
+ selector: "app-user-list",
+ standalone: true,
+ imports: [
+ Highlight,
+ FormsModule,
+ SearchByLoginPipe,
+ ReactiveFormsModule,
+ ],
+ templateUrl: "./user-list.component.html",
+ styleUrl: "./user-list.component.css",
+})
+export class UserListComponent implements OnInit {
+ members: MemberEntity[] = [];
+ newMember!: MemberEntity;
+ /*diff*/
+ editForm!: FormGroup;
+ idControl!: FormControl;
+ loginControl!: FormControl;
+ avatarControl!: FormControl;
+ /*diff*/
+
+ constructor(
+ private membersService: MembersService,
+ private fb: FormBuilder
+ ) {}
+
+ add(): void {
+ this.members.push(this.newMember);
+ this.newMember = {
+ id: "",
+ login: "",
+ avatar_url: "",
+ };
+ }
+
+ handleFileInput($event: any) {
+ const files = $event.target.files as FileList;
+ const reader = new FileReader();
+ reader.readAsDataURL(files[0]);
+ reader.onload = () => {
+ this.newMember.avatar_url = reader.result as string;
+ };
+ }
+
+ ngOnInit(): void {
+ this.membersService.getAll().then((members) => (this.members = members));
+
+ this.newMember = {
+ id: "",
+ login: "",
+ avatar_url: "",
+ };
+ /*diff*/
+ this.createEditForm();
+ /*diff*/
+ }
+ /*diff*/
+ createEditForm(): void {
+ this.editForm = this.fb.group({
+ id: ["", Validators.required],
+ login: ["", [Validators.required, Validators.minLength(6)]],
+ avatar_url: "",
+ });
+
+ this.idControl = this.editForm.get("id") as FormControl;
+ this.loginControl = this.editForm.get("login") as FormControl;
+ this.avatarControl = this.editForm.get("avatar_url") as FormControl;
+ }
+ /*diff*/
+}
+```
+
+Los formularios model-driven son muy flexibles en su forma de construirlos.
+
+Otra forma de hacerlo podría ser la siguiente:
+
+```ts
+createEditForm() {
+ this.idControl = new FormControl('', Validators.required);
+ this.loginControl = new FormControl('', [Validators.required, Validators.minLength(6)]);
+ this.avatarControl = new FormControl();
+
+ this.editForm = this.fb.group({
+ id: this.idControl,
+ login: this.loginControl,
+ avatar_url: this.avatarControl
+ });
+}
+```
+
+Y aquí otra alternativa más
+
+```ts
+createEditForm() {
+ this.editForm = new FormGroup({});
+ this.idControl = new FormControl('', Validators.required);
+ this.loginControl = new FormControl('', [Validators.required, Validators.minLength(6)]);
+ this.avatarControl = new FormControl();
+
+ this.editForm.addControl('id', this.idControl);
+ this.editForm.addControl('login', this.loginControl);
+ this.editForm.addControl('avatar_url', this.avatarControl);
+}
+```
+
+Todas son equivalentes.
+
+- Rellenamos el formulario con los datos del `MemberEntity` en el que haga click el usuario
+
+_src/app/user/user-list/user-list.component.html_
+
+```diff
+
Listado
+
+
+
+
+
+
+
Avatar
+
Id
+
Name
+
+
+
+ @for (member of members | searchByLogin:search.value; track member) {
+-