diff --git a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.service.ts b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.service.ts index a2cd598e1ff3a8f5fde12ac79bbc5a43f129dca2..334e8cc9bee341746942ad55139bd507ce4e4feb 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.service.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.service.ts @@ -1,6 +1,6 @@ import { CarService } from '@core/cars/ports/car.service'; import { Observable, of } from 'rxjs'; -import { Cars } from '@core/cars/models/car.model'; +import { Car, Cars, NewCar } from '@core/cars/models/car.model'; import { Injectable } from '@angular/core'; @Injectable({ @@ -17,4 +17,10 @@ export class InMemoryCarService extends CarService { getAll(): Observable<Cars> { return of(this.cars); } + + add(newCar: NewCar): Observable<Car> { + const car = { id: this.cars.length + 1, ...newCar }; + this.cars = [...this.cars, car]; + return of(car); + } } diff --git a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/car.store.ts b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/car.store.ts index 64fbef2c4578cd21e6580d88fb7789503edbdaec..e3b6f25230eea68ce72b260eb2213ef57d9e99c3 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/car.store.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/car.store.ts @@ -1,4 +1,4 @@ -import { Cars } from '@core/cars/models/car.model'; +import { Car, Cars, NewCar } from '@core/cars/models/car.model'; import { patchState, signalStore, @@ -10,7 +10,7 @@ import { import { CarService } from '@core/cars/ports/car.service'; import { computed, inject } from '@angular/core'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; -import { exhaustMap, pipe, tap } from 'rxjs'; +import { debounceTime, distinctUntilChanged, exhaustMap, pipe, switchMap, tap } from 'rxjs'; interface CarState { cars: Cars; @@ -28,15 +28,29 @@ export const CarStore = signalStore( isEmpty: computed(() => store.cars().length === 0), isLoading: computed(() => store.loading()), })), - withMethods((store, carGateway = inject(CarService)) => ({ - getCars: rxMethod<void>( - pipe( - tap(() => patchState(store, { loading: true })), - exhaustMap(() => carGateway.getAll()), - tap((cars) => patchState(store, { cars: cars, loading: false })), + withMethods((store, carGateway = inject(CarService)) => { + return { + getCars: rxMethod<void>( + pipe( + debounceTime(500), + distinctUntilChanged(), + tap(() => patchState(store, { loading: true })), + switchMap(() => carGateway.getAll()), + tap((cars) => patchState(store, { cars: cars, loading: false })), + ), ), - ), - })), + + addCar: rxMethod<NewCar>( + pipe( + debounceTime(500), + distinctUntilChanged(), + tap(() => patchState(store, { loading: true })), + exhaustMap(car => carGateway.add(car)), + tap(car => patchState(store, { cars: [...store.cars(), car], loading: false })), + ), + ), + }; + }), withHooks((store) => ({ onInit() { store.getCars(); diff --git a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/models/car.model.ts b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/models/car.model.ts index bbf61cb7e615988fa270b80525dd740b132c0e2f..b14300fc052b7cea4cf44061f0958a8dfe395201 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/models/car.model.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/models/car.model.ts @@ -6,4 +6,13 @@ export interface Car { color: string; } +export type NewCar = Omit<Car, 'id'>; + export type Cars = Car[]; + +export const initialCar: NewCar = { + brand: '', + model: '', + year: 0, + color: '', +} diff --git a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.service.ts b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.service.ts index 291b8d2430fb4c7cd641c30368b3b01e58474eb7..33bbed64bb3470c2e0111cc0f975f6f966dd257a 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.service.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.service.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs'; -import { Cars } from '@core/cars/models/car.model'; +import { Car, Cars, NewCar } from '@core/cars/models/car.model'; import { Injectable } from '@angular/core'; @Injectable({ @@ -7,4 +7,5 @@ import { Injectable } from '@angular/core'; }) export abstract class CarService { abstract getAll(): Observable<Cars>; + abstract add(newCar: NewCar): Observable<Car>; } diff --git a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.html b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.html index 1b75cded1a50408f847e5e03f032b1c00df4c9ac..679e4bab7a518e04d466ec55a736baf8e0aa2b14 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.html +++ b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.html @@ -1,5 +1,6 @@ @let empty = this.store.isEmpty(); @let loading = this.store.isLoading(); +@let cars = this.store.cars(); <div class="container mx-auto py-6"> <h1 class="text-4xl font-bold text-center mb-6">My cars</h1> @@ -24,7 +25,7 @@ </td> </tr> } @else { - @for (car of this.store.cars(); track car.id) { + @for (car of cars; track car.id) { <tr> <td>{{ car.id }}</td> <td>{{ car.brand }}</td> @@ -57,4 +58,4 @@ </div> </div> -<app-modal #modal /> \ No newline at end of file +<app-modal #modal [newCar]="newCar" (submit)="onAddSubmit($event)"/> diff --git a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.ts b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.ts index e8cece3f356968d7a9385a28796179d23e048cf3..30d3cc920e7dc5d3491456b953c315bf69191ce0 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.ts @@ -2,6 +2,7 @@ import { Component, inject, viewChild } from '@angular/core'; import { CarStore } from '@core/cars/car.store'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { ModalComponent } from '@features/cars/components/modal/modal.component'; +import { initialCar, NewCar } from '@core/cars/models/car.model'; @Component({ selector: 'app-cars', @@ -12,7 +13,11 @@ import { ModalComponent } from '@features/cars/components/modal/modal.component' }) export default class CarsComponent { readonly store = inject(CarStore); + modal = viewChild<ModalComponent>('modal'); + newCar = initialCar; + + // TODO: Transformer cars en subjectBehavior onEdit(id: number) { alert(`Edit car with id: ${id}`); @@ -25,4 +30,8 @@ export default class CarsComponent { onAdd() { this.modal()!.open(); } + + onAddSubmit(newCar: NewCar) { + this.store.addCar(newCar); + } } diff --git a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/components/modal/modal.component.html b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/components/modal/modal.component.html index 30b39eb15168ccc05cbcfb70072f90499f9e6d86..64c8048912fd3119254745e591edab277d3bd7ba 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/components/modal/modal.component.html +++ b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/components/modal/modal.component.html @@ -2,47 +2,76 @@ <div class="modal modal-open"> <div class="modal-box"> <button (click)="close()" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button> - <h3 class="text-lg font-bold">Adding a new car!</h3> + <h3 class="text-lg font-bold mb-4">Adding a new car!</h3> - <form (ngSubmit)="onSubmit()" class="mt-4 mb-4"> + <form [formGroup]="carFormBuilder" (ngSubmit)="onSubmit()" class="space-y-4"> <!-- Brand --> - <div class="mb-4"> - <label class="floating-label" for="brand"> - <span>Brand</span> - <input id="brand" name="brand" type="text" placeholder="Brand" class="input input-bordered" required> - </label> - </div> + <label class="floating-label" for="brand"> + <span>Brand</span> + <input + id="brand" + formControlName="brand" + name="brand" + type="text" + placeholder="Brand" + class="input input-bordered w-full" + required + > + </label> <!-- Model --> - <div class="mb-4"> <label class="floating-label" for="model"> <span>Model</span> - <input id="model" name="model" type="text" placeholder="Model" class="input input-bordered" required> + <input id="model" + formControlName="model" + name="model" + type="text" + placeholder="Model" + class="input input-bordered w-full" + required + > </label> - </div> <!-- Year --> - <div class="mb-4"> <label class="floating-label" for="year"> <span>Year</span> - <input id="year" name="year" type="number" min="1900" max="2025" placeholder="Year" class="input input-bordered" required> + <input + id="year" + formControlName="year" + name="year" + type="number" + min="1900" + max="2025" + placeholder="Year" + class="input input-bordered w-full" + required + > </label> - </div> <!-- Color --> - <div class="mb-4"> <label class="floating-label" for="brand"> <span>Color</span> - <input id="color" name="color" type="text" placeholder="Color" class="input input-bordered" required> + <input + id="color" + formControlName="color" + name="color" + type="text" + placeholder="Color" + class="input input-bordered w-full" + required + > </label> - </div> <!-- Actions du modal --> <div class="modal-action"> <button type="button" class="btn" (click)="close()">Cancel</button> - <button type="submit" class="btn btn-primary">Save</button> + <button type="submit" + class="btn btn-primary" + [disabled]="carFormBuilder.invalid"> + Save + </button> </div> </form> </div> </div> -} \ No newline at end of file +} diff --git a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/components/modal/modal.component.ts b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/components/modal/modal.component.ts index fcdd0e0b0ffd34a64543ac5178dae997814cb353..98307717d57bed8d3e9477b581d3411cc5936548 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/components/modal/modal.component.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/components/modal/modal.component.ts @@ -1,15 +1,23 @@ -import { Component, input, signal } from '@angular/core'; -import { Car } from '@core/cars/models/car.model'; -import { FormsModule } from '@angular/forms'; +import { Component, inject, input, output, signal } from '@angular/core'; +import { Car, NewCar } from '@core/cars/models/car.model'; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; @Component({ selector: 'app-modal', - imports: [FormsModule], + imports: [FormsModule, ReactiveFormsModule], templateUrl: './modal.component.html', }) export class ModalComponent { isOpen = signal(false); - newCar = input.required<Car>(); + newCar = input.required<NewCar>(); + submit = output<NewCar>(); + + protected carFormBuilder = inject(FormBuilder).nonNullable.group({ + brand: ['', Validators.required], + model: ['', Validators.required], + year: [0, Validators.required], + color: ['', Validators.required], + }); open(): void { this.isOpen.set(true); @@ -20,8 +28,9 @@ export class ModalComponent { } onSubmit() { - alert('Submitted'); + this.carFormBuilder.markAllAsTouched(); + if (this.carFormBuilder.invalid) return; + this.submit.emit(this.carFormBuilder.getRawValue() as NewCar); + this.close(); } - - // TODO: continuer le add car }