Tribu Angular

Oxyl

NGULAR

Présentation

Un peu d'histoire ...

  • Juin 2012 : AngularJS (Miško Hevery et Adam Abronsw puis Google)
  • Septembre 2016 : Angular 2
  • Mars 2017 : Angular 4
  • ...
  • Novembre 2024 : Angular 19
  • Fin mai 2025 : Angular 20

Application traditionnelle

  • Échanges client-serveur lourds
     
  • Rechargement de la page
     
  • Pas d'offline

Initial Request

Form Post

Client

Server

Page reload !

Single Page Application

  • Fluidité
     
  • User experience (UX)
     
  • Offline

Initial Request

XHR

{ ... } JSON

Client

Server

Typescript

  • Transpilé en JavaScript.

 

  • Typage fort : Autocomplétion, Erreur à la compilation, Maintenabilité

 

  • Suit les normes ECMAScript.

 

  • Javascript : typage dynamique => Typescript : typage statique.

 

  • Développé par Microsoft et Open source

Survol d'Angular

Vue d'ensemble

Template

< >

Component

{     }

Injector

 

 

 

 

 

Service

{     }

LoginModule
​{}

{{ person.name }}

(click)="handle()"

Metadata

Directive

{ }

Metadata

UserModule
​{}

CardModule
​{}

ListModule
​{}

Component

Life Cycle Hooks

@Component({
    selector: 'parent-component',
    template: '<child-component></child-component>',
    styleUrls: string[],
    ...
})
export class ParentComponent {}
  
@Component({
    selector: 'child-component',
    template: 'Salut les grands malades !'
})
export class ChildComponent {}

Module (deprecated)

@NgModule({
//Declare components, directives, and pipes that belong to this module
    declarations: any[],
//Import modules whose exported classes are needed in this module
    imports: any[],
//Make components, directives, and pipes available to other modules
    exports: any[],
//Register services (injectables) that this module provides
    providers: any[],
    ...
})
export class MonMondule {}

Standalone Component

@Component({
  selector: 'my-standalone_component',
  standalone: true, // This makes it a standalone component 
  imports: [CommonModule], // Can import standalone or NgModules
  template: `
    <h1>Hello from a standalone component!</h1>
<p>This component doesn't need to be declared in any NgModule.</p>
  `,
  styles: ...
})
export class HelloWorldComponent {
}

Injectable

@Injectable({
  providedIn: 'root'
})
export class MonService {}

export class MonComponent {
  constructor(private readonly service: Service) {}
}

Directive

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
    constructor(el: ElementRef) {
       el.nativeElement.style.backgroundColor = 'yellow';
    }
}
<p appHighlight>Je suis surligné</p>
<p>Pas moi</p>

Data binding

{{ value }}

[property]="value"

(event)="handlerFunction($event)"

[(ngModel)]="property"

DOM

Component

Template binding {{ }}

<div>
    Hello {{ name }}
</div>

Dom events

<button (click)="doSomething()">
    Click me !
</button>

Liste des HTML DOM Events sans le "on" : 

click, dblclick, mousedown, keypress, change, resize, blur, focus, ...

https://www.w3schools.com/jsref/dom_obj_event.asp

Signals

import { signal, computed, effect } from '@angular/core';

// Create a reactive signal that holds a value
const count = signal(0); // ✅ signal<T>(initialValue)

// Create a computed signal that derives its value from another signal
// auto-updates when 'count' changes
const doubleCount = computed(() => count() * 2);

// Set up an effect to run logic whenever a signal changes
effect(() => {
  console.log(`Count is ${count()} and double is ${doubleCount()}`);
});

// Update the signal (triggers effect and recomputes doubleCount)
count.set(1);  // logs: Count is 1 and double is 2
count.update(n => n + 1);  // logs: Count is 2 and double is 4

Signals

increment() {
  this.count.update(n => n + 1);
}
reset() {
  this.count.set(0);
}
<h2>Signal Counter</h2>

<p>Current count: {{ count() }}</p> <!-- 👈 Use signal like a function -->

<button (click)="increment()">Increment</button>
<button (click)="reset()">Reset</button>

Component tree

Root Component

 

 

 

Root Template

< >

Root Class

{ }

Child A Component

 

 

Child A Template

< >

Child A Class

{ }

Child B Component

 

 

Child B Template

< >

Child B Class

{ }

Grandchild Component

 

 

Grandchild Template

< >

Grandchild Class

{ }

Arborescence

Premier pas

Créer et lancer un projet

ng new first-app
cd first-app
npm install
npm start

NE PAS CRÉER EN MODE SSR !!!

Objectif : menu de cocktails

Créer un composant

ng generate component components/recipe

Créer un premier modèle

export interface Ingredient {
    name: string;
    id?: number;
}

Créer un module

ng generate module custom-material

Créer un cocktail

public recipe: Recipe = {
    description: "Les jolis voyages",
    instructions: [
        "Verser l’absinthe dans un verre. L’idéal serait un verre à absinthe, mais une petite coupe à dégustation peut faire l’affaire. Déposer la cuillère à absinthe sur le verre et y déposer le cube de sucre.",
        "Faire couler l’eau de source glacée très doucement, quelques gouttes à la fois, sur le cube de sucre. Enfin, remuer l’absinthe avec la cuillère et servir.",
        "À savourer en bonne compagnie, à 17 h, en écoutant de l’accordéon et en récitant Les Fleurs du Mal.",
        ""
    ],
    name: "l'absinthe",
    ingredients: [
        {
          	id:0,
            ingredient: {
          		id:1,
                name: "absinthe"
            },
            quantity: 1,
            unit: "Oz"
        },
        {
          	id:1,
            ingredient: {
          		id:6,
                name: "sucre"
            },
            quantity: 1,
            unit: "cube"
        },
        {
            id:2,
            "ingredient": {
                "id":4,
                "name": "eau de source glacée"
            },
            "quantity": 3,
            "unit": "oz"
        },
        {
            id:3,
            "ingredient": {
                id: 12,
                "name": "cuillère à absinthe perforée"
            },
            "quantity": 1,
            "unit": "cuillère"
        }
    ],
    picture: "https://upload.wikimedia.org/wikipedia/commons/e/e2/Absinthe-glass.jpg"
}

Conseil : Faire un service avec un getter

*ngFor

<ul>
    <li *ngFor="let item of itemList">
        {{ item }}
    </li>
</ul>

Pour afficher une liste

Conseil : séparer la liste et l'item en deux composants

*ngIf

<div *ngIf="expressionBooleenne">
    Hello
</div>

Import une donnée du parent

Paramètre d'entrée du composant :

@Input()
recipe: Recipe;
<mon-composant-enfant [recipe]="maRecette"></mon-composant-enfant>

Design

Un peu d'aide côté design : Angular Material

Installation :

ng add @angular/material
  1. Créer et lancer le projet.
  2. Créer votre premier composant: Recipe.
  3. Créer des modèles: Recipe, Recipe Ingredients, Ingredients.
  4. Créer une constante de type Recipe dans votre composant.
  5. Afficher les données dans le composant.
  6. Créer un composant parent: Recipe list.
  7. Mettre une liste de Recipe dans le componant Recipe List.
  8. Afficher une liste de Recipe.
  9. Ajouter un bouton toggle pour afficher ou retirer les instructions / ingrédients.
  10. Ajouter une librairie de composants stylisés.
  11. Ajouter un compteur de verre bu par recette avec un Signal

Pour les rapides:  Mettre Angular Material dans un module; Créer un composant Header;  Styliser tes composants; Ajouter un compteur de verre global

A vous de jouer

On continue

Créer un service

ng generate service recipe

L'asynchrone en JS:

Les promesses

new Promise((resolve, reject) => { /* execution en asynchrone */ })
        .then(result => { /* Traiter le résultat */ })
        .catch(error => { /* Traiter l'erreur */ });

RXJS

La library JAVASCRIPT pour faciliter l'utilisation de l'asynchrone.

getRecipes(): Observable<Recipe[]> {
    return of(RECIPES);
}

Observable et Subject :

this.recipeService.getRecipes().subscribe({
    next: (result: Recipe[]) => { /* Traiter le résultat */ },
    error: (error) => { /* Traiter l'erreur */ }
});

Pour en arriver où ?

Http Client

En Standalone,
à ajouter dans le component app:
providers : [HttpClient]

private recipeUrl = 'api/recipes';

constructor(private http: HttpClient) { }

getRecipes(): Observable<Recipe[]> {
    return this.http.get<Recipe[]>(this.recipeUrl);
}

En Module,
à ajouter dans le module app:
providers : [HttpClient]

Le routage 1/2

Créer un module dédié : AppRoutingModule

L'importer dans le module principal

@NgModule({
    exports: [
        RouterModule
    ],
    imports: [
        RouterModule.forRoot(routes)
    ]
})
export class AppRoutingModule { }

Le routage 1/2

Modifier le app.config.js pour ajouter le routing

import { routes } from './app.routes';
import { provideRouter } from '@angular/router';
import { provideHttpCLient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient()]
};

Le routage 2

Définir une stratégie de route :

const routes: Routes = [
    {
        path: 'recipes',
        component: RecipeListComponent,
        pathMatch: 'full'
    },
    {
        path: '**',
        redirectTo: 'recipes',
        pathMatch: 'full'
    }
];
<router-outlet></router-outlet>

Point d'entrée des routes

  1. Créer un service : Recipe.
  2. Mettre la liste de recettes dans le service en asynchrone.
  3. Faire sa première requête http sur le back.
  4. Ajouter le module de routage.
  5. Créer une nouvelle page pour voir les détails d'une recette (Get by ID + page de routage spécifique + composant Recipe Detail).

Pour les rapides : Lors de la récupération des recettes, ajouter un tri par nom; Sur la page recipe ID, ajouter un "guard" dans le router pour vérifier que la recette existe

 

A vous de jouer

http://52.47.189.244:8080/swagger-ui/index.html

TIPS :  

  • Créer une nouvelle route recipes/:id permet de variabiliser le param id
  • L'attribut routerLink="/recipes/{{ recipe.id }}" permet de naviguer vers une la nouvelle route
  • this.route.snapshot.paramMap.get('id') permet de récupérer le paramètre de la route à la création du composant (this.activatedRoute doit être injecté depuis le service Angular ActivatedRoute)

Encore un peu plus loin

Reactive Forms

Validation dynamique de formulaires

https://angular.io/guide/reactive-forms

  1. Créer une nouvelle page d'ajout de recette.
  2. Créer un formulaire pour créer une recette avec un nom, une url d'image, et une description.
  3. Ajouter une validation pour rendre les champs obligatoires.
  4. Ajouter une dropdown pour sélectionner un ou plusieurs ingrédients.
  5. Ajouter une option de suppression de recette sur la page principale.

Pour les rapides : Sur le formulaire d'ajout, forcer l'utilisation d'au moins une instruction et d'au moins un ingrédient, qu'une instruction ne peut pas être vide etc...

 

A vous de jouer

http://52.47.189.244:8080/swagger-ui.html#/

https://angular.io/guide/reactive-forms

Pour les rapides

  1. Gérer l'update des recipes (PUT et PATCH).
  2. Gestion des ingédients sur une page à part.
  3. Avoir la liste des recettes où l'ingrédient est utilisé.
  4. Gestion des erreurs avec Interceptor et snackbar.
  5. Filtres et Tris : 
  6. Par nom de cocktails, par ingrédient présent, par boisson bue.
  7. Gestion du compteur de boisson total lors d'une suppression.
  8. Drag and drop des ingrédients pour créer un cocktail

ENCORE PLUS DE FEATURES

Pipes

@Pipe({ 
    name: 'orderBy'
})
export class OrderByPipe implements PipeTransform {
    transform(param : InputType): OutputType {
        doSomething();
        return newValue;
    }
}

Tests unitaires !

Animations : Animer les composants

https://angular.io/guide/animations#setup

Les liens utiles

Formation Angular v3

By Vincent Protois