Fin section 14 et 15 : programmation réactive et requêtes HTTP

This commit is contained in:
Lucien Cartier-Tilet 2023-02-27 14:23:58 +01:00
parent a789a10203
commit 108c2b17a7
Signed by: phundrak
GPG Key ID: BD7789E705CB8DCA
15 changed files with 194 additions and 19 deletions

View File

@ -25,6 +25,13 @@
"@angular-devkit/build-angular": "^15.1.5", "@angular-devkit/build-angular": "^15.1.5",
"@angular/cli": "~15.1.5", "@angular/cli": "~15.1.5",
"@angular/compiler-cli": "^15.1.0", "@angular/compiler-cli": "^15.1.0",
"angular-in-memory-web-api": "^0.15.0",
"cz-conventional-changelog": "^3.3.0",
"typescript": "~4.9.4" "typescript": "~4.9.4"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
} }
} }

View File

@ -1,15 +1,27 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { PokemonModule } from './pokemon/pokemon.module'; import { PokemonModule } from './pokemon/pokemon.module';
import { InMemoryDataService } from './in-memory-data.service';
@NgModule({ @NgModule({
declarations: [AppComponent, PageNotFoundComponent], declarations: [AppComponent, PageNotFoundComponent],
imports: [BrowserModule, FormsModule, PokemonModule, AppRoutingModule], imports: [
BrowserModule,
FormsModule,
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, {
dataEncapsulation: false,
}),
PokemonModule,
AppRoutingModule,
],
providers: [], providers: [],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })

View File

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { POKEMONS } from './pokemon/mock-pokemon-list';
@Injectable({
providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const pokemons = POKEMONS;
return { pokemons };
}
constructor() {}
}

View File

@ -0,0 +1,2 @@
<h2 class="center">Ajouter un pokémon</h2>
<app-pokemon-form [pokemon]="pokemon"></app-pokemon-form>

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
import { Pokemon } from '../pokemon';
@Component({
selector: 'app-add-pokemon',
templateUrl: './add-pokemon.component.html',
styles: [],
})
export class AddPokemonComponent implements OnInit {
pokemon: Pokemon;
ngOnInit() {
this.pokemon = new Pokemon();
}
}

View File

@ -49,6 +49,7 @@
<div class="card-action"> <div class="card-action">
<a (click)="goToPokemonList()">Retour</a> <a (click)="goToPokemonList()">Retour</a>
<a (click)="goToEditPokemon(pokemon)">Modifier</a> <a (click)="goToEditPokemon(pokemon)">Modifier</a>
<a (click)="deletePokemon(pokemon)">Supprimer</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -19,10 +19,18 @@ export class DetailPokemonComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
const pokemonId: string | null = this.route.snapshot.paramMap.get('id'); const pokemonId: string | null = this.route.snapshot.paramMap.get('id');
if (pokemonId) { if (pokemonId) {
this.pokemon = this.pokemonService.getPokemonById(+pokemonId); this.pokemonService
.getPokemonById(+pokemonId)
.subscribe((pokemon) => (this.pokemon = pokemon));
} }
} }
deletePokemon(pokemon: Pokemon) {
this.pokemonService
.deletePokemonById(pokemon)
.subscribe(() => this.goToPokemonList());
}
goToPokemonList() { goToPokemonList() {
this.router.navigate(['/pokemons']); this.router.navigate(['/pokemons']);
} }

View File

@ -19,7 +19,9 @@ export class EditPokemonComponent implements OnInit {
ngOnInit() { ngOnInit() {
const pokemonId: string | null = this.route.snapshot.paramMap.get('id'); const pokemonId: string | null = this.route.snapshot.paramMap.get('id');
if (pokemonId) { if (pokemonId) {
this.pokemon = this.pokemonService.getPokemonById(+pokemonId); this.pokemonService
.getPokemonById(+pokemonId)
.subscribe((pokemon) => (this.pokemon = pokemon));
} }
} }
} }

View File

@ -29,3 +29,9 @@
</div> </div>
</div> </div>
</div> </div>
<a
class="btn-floating btn-large waves-effect waves-ligth red z-depth-3"
style="position: fixed; bottom: 25px; right: 25px"
routerLink="/pokemon/add"
></a
>

View File

@ -13,7 +13,9 @@ export class ListPokemonComponent implements OnInit {
constructor(private router: Router, private pokemonService: PokemonService) {} constructor(private router: Router, private pokemonService: PokemonService) {}
ngOnInit() { ngOnInit() {
this.pokemonList = this.pokemonService.getPokemonList(); this.pokemonService
.getPokemonList()
.subscribe((pokemonList) => (this.pokemonList = pokemonList));
} }
goToPokemon(pokemon: Pokemon) { goToPokemon(pokemon: Pokemon) {

View File

@ -24,6 +24,27 @@
</div> </div>
</div> </div>
<!-- Pokemon picture -->
<div *ngIf="isAddForm" class="form-group">
<label for="name">Image</label>
<input
type="url"
class="form-control"
id="picture"
required
[(ngModel)]="pokemon.picture"
name="picture"
#picture="ngModel"
/>
<div
[hidden]="picture.valid || picture.pristine"
class="card-panel red accent-1"
>
Limage du pokémon est requise.
</div>
</div>
<!-- Pokemon hp --> <!-- Pokemon hp -->
<div class="form-group"> <div class="form-group">
<label for="hp">Point de vie</label> <label for="hp">Point de vie</label>

View File

@ -11,11 +11,15 @@ import { PokemonService } from '../pokemon.service';
export class PokemonFormComponent implements OnInit { export class PokemonFormComponent implements OnInit {
@Input() pokemon: Pokemon; @Input() pokemon: Pokemon;
types: string[]; types: string[];
isAddForm: boolean;
constructor(private pokemonService: PokemonService, private router: Router) {} constructor(private pokemonService: PokemonService, private router: Router) {}
ngOnInit(): void { ngOnInit(): void {
this.types = this.pokemonService.getPokemonTypeList(); this.pokemonService
.getPokemonTypeList()
.subscribe((types) => (this.types = types));
this.isAddForm = this.router.url.includes('add');
} }
hasType(type: string) { hasType(type: string) {
@ -33,8 +37,17 @@ export class PokemonFormComponent implements OnInit {
} }
onSubmit() { onSubmit() {
console.log('Submit form!'); if (this.isAddForm) {
this.pokemonService
.addPokemon(this.pokemon)
.subscribe((pokemon: Pokemon) => {
this.router.navigate(['/pokemon', pokemon.id]);
});
} else {
this.pokemonService.updatePokemon(this.pokemon).subscribe(() => {
this.router.navigate(['/pokemon', this.pokemon.id]); this.router.navigate(['/pokemon', this.pokemon.id]);
});
}
} }
isTypesValid(type: string): boolean { isTypesValid(type: string): boolean {

View File

@ -10,9 +10,11 @@ import { PokemonService } from './pokemon.service';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { PokemonFormComponent } from './pokemon-form/pokemon-form.component'; import { PokemonFormComponent } from './pokemon-form/pokemon-form.component';
import { EditPokemonComponent } from './edit-pokemon/edit-pokemon.component'; import { EditPokemonComponent } from './edit-pokemon/edit-pokemon.component';
import { AddPokemonComponent } from './add-pokemon/add-pokemon.component';
const pokemonRoutes: Routes = [ const pokemonRoutes: Routes = [
{ path: 'edit/pokemon/:id', component: EditPokemonComponent }, { path: 'edit/pokemon/:id', component: EditPokemonComponent },
{ path: 'pokemon/add', component: AddPokemonComponent },
{ path: 'pokemon/:id', component: DetailPokemonComponent }, { path: 'pokemon/:id', component: DetailPokemonComponent },
{ path: 'pokemons', component: ListPokemonComponent }, { path: 'pokemons', component: ListPokemonComponent },
]; ];
@ -25,6 +27,7 @@ const pokemonRoutes: Routes = [
DetailPokemonComponent, DetailPokemonComponent,
PokemonFormComponent, PokemonFormComponent,
EditPokemonComponent, EditPokemonComponent,
AddPokemonComponent,
], ],
imports: [CommonModule, FormsModule, RouterModule.forChild(pokemonRoutes)], imports: [CommonModule, FormsModule, RouterModule.forChild(pokemonRoutes)],
providers: [PokemonService], providers: [PokemonService],

View File

@ -1,22 +1,74 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { POKEMONS } from './mock-pokemon-list'; import { catchError, mergeMap, Observable, of, tap } from 'rxjs';
import { Pokemon } from './pokemon'; import { Pokemon } from './pokemon';
@Injectable() @Injectable()
export class PokemonService { export class PokemonService {
getPokemonList(): Pokemon[] { constructor(private http: HttpClient) {}
return POKEMONS;
getPokemonList(): Observable<Pokemon[]> {
return this.http.get<Pokemon[]>('api/pokemons').pipe(
tap((pokemonList) => this.log(pokemonList)),
catchError((error) => this.handleError(error, []))
);
} }
getPokemonById(pokemonId: number): Pokemon | undefined { getPokemonById(pokemonId: number): Observable<Pokemon | undefined> {
return POKEMONS.find((pokemon) => pokemon.id === pokemonId); return this.http.get<Pokemon>(`api/pokemons/${pokemonId}`).pipe(
tap((pokemon) => this.log(pokemon)),
catchError((error) => this.handleError(error, undefined))
);
} }
getPokemonTypeList(): string[] { getPokemonTypeList(): Observable<string[]> {
const types: Set<string> = new Set(); return this.http.get<Pokemon[]>('api/pokemons').pipe(
POKEMONS.forEach((pokemon) => { mergeMap((pokemons: Pokemon[]) => {
pokemon.types.forEach((type) => types.add(type)); const set = new Set<string>();
}); pokemons.forEach((pokemon) =>
return Array.from(types.values()); pokemon.types.forEach((type) => set.add(type))
);
return [Array.from(set.values())];
}),
tap((pokemon) => this.log(pokemon)),
catchError((error) => this.handleError(error, []))
);
}
updatePokemon(pokemon: Pokemon): Observable<null> {
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
};
return this.http.put('api/pokemons', pokemon, httpOptions).pipe(
tap((response) => this.log(response)),
catchError((error) => this.handleError(error, null))
);
}
addPokemon(pokemon: Pokemon): Observable<Pokemon> {
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
};
return this.http.post<Pokemon>('api/pokemons', pokemon, httpOptions).pipe(
tap((response) => this.log(response)),
catchError((error) => this.handleError(error, null))
);
}
deletePokemonById(pokemon: Pokemon): Observable<null> {
return this.http.delete(`api/pokemons/${pokemon.id}`).pipe(
tap((response) => this.log(response)),
catchError((error) => this.handleError(error, null))
);
}
private log(response: any) {
console.table(response);
}
private handleError(error: Error, errorValue: any) {
console.error(error);
return of(errorValue);
} }
} }

View File

@ -1,9 +1,25 @@
export class Pokemon { export class Pokemon {
id: number; id: number;
name: string;
hp: number; hp: number;
cp: number; cp: number;
name: string;
picture: string; picture: string;
types: Array<string>; types: Array<string>;
created: Date; created: Date;
constructor(
name: string = 'Enter a name...',
hp: number = 100,
cp: number = 10,
picture: string = 'https://assets.pokemon.com/assets/cms2/img/pokedex/detail/xxx.png',
types: string[] = ['Normal'],
created: Date = new Date()
) {
this.hp = hp;
this.cp = cp;
this.name = name;
this.picture = picture;
this.types = types;
this.created = created;
}
} }