Création du composant d’ajout des ingrédients

frontend/shoppinglist/src/app/ingredients/ingredient-new/ingredient-new.component.ts

import { Component, inject, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { IngredientCategory } from '../ingredient.utils';

@Component({
  selector: 'app-ingredient-new',
  imports: [ReactiveFormsModule],
  templateUrl: './ingredient-new.component.html',
  styleUrl: './ingredient-new.component.css'
})
export class IngredientNewComponent implements OnInit, OnDestroy {
  private route = inject(ActivatedRoute);
  private routeSubscription: Subscription | null = null;
  private fb = inject(FormBuilder);

  formGroup = this.fb.group({
    name: ['',[Validators.required]],
    picto: ['',[Validators.required]],
    category: [IngredientCategory.VEGETABLE, [Validators.required]]
  })
  
  ingredientCategory = Object.values(IngredientCategory);
  ingerdientId = -1;

  ngOnInit(): void {
    this.routeSubscription = this.route.params.subscribe( params => {
      if(params['id']) {this.ingerdientId = parseInt(params['id'])}
    })
  }

  ngOnDestroy(): void {
    this.routeSubscription?.unsubscribe();
  }

  submit(event: Event) {
    event.preventDefault();
    console.log(this.formGroup.value);
  }

  isFieldValid(fieldName: string) {
    const formControl = this.formGroup.get(fieldName);
    return formControl?.invalid && (formControl.dirty || formControl.touched);
  }
}

Ce composant fournit un formulaire complet et validé pour créer ou modifier un ingrédient, récupère l’ID d’ingrédient via les paramètres d’URL (permettant la modification), et applique les bonnes pratiques Angular pour la gestion des subscriptions et la validation réactive.

1. Importations et décorateur

  • Import des modules Angular nécessaires (FormBuilderFormGroupValidators pour les formulaires réactifs, ActivatedRoute pour l’accès aux paramètres d’URL).
  • Déclaration du composant avec son sélecteur, template, style et import du module ReactiveFormsModule.

2. Injection des dépendances

private route = inject(ActivatedRoute);
private routeSubscription: Subscription | null = null;
private fb = inject(FormBuilder);
  • Injection des services ActivatedRoute (pour lire les paramètres de la route) et FormBuilder (pour créer le formulaire réactif).
  • Stockage d’une subscription pour la nettoyer ensuite.

3. Définition du formulaire réactif

formGroup = this.fb.group({
name: ['', [Validators.required]],
picto: ['', [Validators.required]],
category: [IngredientCategory.VEGETABLE, [Validators.required]]
})
  • Création d’un groupe de contrôles de formulaire avec des validateurs (champs obligatoires).
  • La catégorie d’ingrédient est par défaut VEGETABLE.

4. Variables complémentaires

ingredientCategory = Object.values(IngredientCategory);
ingerdientId = -1;
  • Stockage de toutes les valeurs possibles de la catégorie (pour un sélecteur).
  • ingredientId pour détecter si on modifie un ingrédient existant (initialisé à -1).

5. Cycle de vie Angular

  • ngOnInit:
    • S’abonne aux paramètres d’URL via route.params.subscribe.
    • Si un ID est présent, le convertit en entier et le stocke (pour potentiellement charger/modifier un ingrédient).
  • ngOnDestroy:
    • Nettoie l’abonnement pour éviter les fuites mémoire avec unsubscribe().

6. Méthodes du composant

  • submit(event: Event) :
    • Empêche la soumission standard du formulaire.
    • Affiche dans la console les données du formulaire (à compléter pour intégrer l’ajout ou la modification).
  • isFieldValid(fieldName: string) :
    • Vérifie si un champ est invalide et a été touché ou modifié, pour afficher des messages d’erreur ou styles.

Création d’un fichier de configuration

Afin d’avoir des données cohérentes et structurées, on crée un fichier de configuration dans lequel on va stocker plusieurs informations liées aux ingrédients

frontend/shoppinglist/src/app/ingredients/ingredient.utils.ts

Ce code TypeScript définit des catégories d’ingrédients avec des propriétés associées, utilisées probablement pour afficher des informations visuelles dans l’application Angular.

1. Enumération IngredientCategory

frontend/shoppinglist/src/app/ingredients/ingredient.utils.ts

export enum IngredientCategory {
VEGETABLE = 'vegetable',
STARCH = 'starch',
MEAT = 'meat'
}
  • Enumération pour représenter les catégories possibles des ingrédients.
  • Chaque catégorie a une valeur textuelle claire : 'vegetable''starch''meat'.
  • Utilisation typique : typer les ingrédients et garantir des valeurs constantes et cohérentes.

2. Interface IIngredientCategoryProperties

frontend/shoppinglist/src/app/ingredients/ingredient.utils.ts

export interface IIngredientCategoryProperties {
imageUrl: string;
color: string;
}
  • Interface qui décrit un objet contenant deux propriétés liées à l’affichage :
    • imageUrl : chemin vers une image représentant la catégorie.
    • color : couleur associée à la catégorie (en format CSS RGB ici).

3. Objet IngredientTypeProperties

frontend/shoppinglist/src/app/ingredients/ingredient.utils.ts

export const IngredientTypeProperties: {[key: string]: IIngredientCategoryProperties} = {
[IngredientCategory.VEGETABLE]: {
imageUrl: 'assets/img/vegetable.png',
color: 'rgb(135, 255, 124)'
},
[IngredientCategory.STARCH]: {
imageUrl: 'assets/img/starch.png',
color: 'rgb(255, 255, 104)'
},
[IngredientCategory.MEAT]: {
imageUrl: 'assets/img/meat.png',
color: 'rgb(255, 104, 104)'
},
}
  • Objet associant chaque catégorie (VEGETABLESTARCHMEAT) à ses propriétés visuelles.
  • Permet de récupérer facilement l’image ou la couleur d’un ingrédient selon sa catégorie.
  • Utile pour une UI dynamique, par exemple afficher une icône colorée à côté des ingrédients.

Création du template associé pour afficher le formulaire

frontend/shoppinglist/src/app/ingredients/ingredient-new/ingredient-new.component.html

<form (submit)="submit($event)" [formGroup]="formGroup">
    <div class="form-field">
        <label for="name">Name</label>
        <input id="name" name="name" type="text" formControlName="name" />
        @if (isFieldValid('name')) {
            @if (formGroup.get('name')?.hasError('required')) {
                <div class="error">This field is required.</div>
            }
        }
    </div>
    <div class="form-field">
        <label for="picto">Picto</label>
        <input id="picto" name="picto" type="text" formControlName="picto" />
        @if (isFieldValid('picto')) {
            @if (formGroup.get('picto')?.hasError('required')) {
                <div class="error">This field is required.</div>
            }
        }
    </div>
    <div class="form-field">
        <label for="category">Category</label>
        <select id="category" name="category" formControlName="category">
            @for (cat of ingredientCategory; track cat) {
                <option [value]="cat">{{cat}}</option>
            }
    </select>
    </div>
    <button type="submit" [disabled]="formGroup.invalid">Save</button>
</form>

“Ces notes techniques sont à but informatif, non formatif, et reflètent mon expérience à la date de rédaction.”