Tribu Angular

Oxyl - 2023

NGULAR

v15.x.x

Prérequis

  • NodeJS (LTS) 14.20.x, 16.13.x or 18.10.x
  • NPM v6.9.0+
  • Angular CLI v15.x.x+

     
  • IDE Front : Visual Studio Code, Atom, Sublim Text, Webstorm (1 mois gratuit)
  • (opt) Git
npm install @angular/cli

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 2022 : Angular 15

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

Pourquoi Angular ?

  • Architecture robuste et complète pour des applications de grande envergure.
  • Syntaxe claire et facilement lisible grâce à l'utilisation de TypeScript.
  • Développement rapide d'applications grâce à un grand nombre de fonctionnalités et de bibliothèques prêtes à l'emploi.
  • Simplicité et facilité d'apprentissage grâce à sa syntaxe intuitive.
  • Performance élevée grâce à une structure légère et à une implémentation de la gestion d'état réactive.
  • Facilité de mise en œuvre dans des projets existants sans besoin de tout revoir.
  • Facilité d'utilisation et apprentissage rapide grâce à sa simplicité.
  • Composants réutilisables, permettant de gagner du temps et de l'efficacité.
  • Grande flexibilité en termes de gestion d'état grâce à des outils tels que Redux.

Typescript

  • Langage de programmation typé.
  • Surcouche de JavaScript.
  • Transpilé en JavaScript.
  • Code structuré avec interfaces/classes.
  • Développé par Microsoft en Open Source

Avantages de Typescript

Qu'est-ce que Typescript ?

  • Détection rapide d'erreurs.
  • Documentation plus complète.
  • Autocomplétion
  • Fonctionnalités avancées intégrées.
  • Modularité et évolutivité facilitées.
  • Interopérabilité avec tous les frameworks.

Typescript cheat sheet

export class MonComponent extends MaClassParente implements MonInterface {

    private maVariable: number | boolean | string = 'abc';
    protected array: any[];
    public abc: null | undefined;

    static readonly CONST = "CONSTANTE";

    constructor(private readonly service: Service) {}

    maFonction(attr: number): void {
      const constante = 123;
      let variable = "abc";
      let monArrowFunction = (a, b) => a+b;
    }
}

Survol d'Angular

Vue d'ensemble

Template

< >

Component

{     }

Injector

 

 

 

 

 

Service

{     }

LoginModule
​{}

{{ person.name }}

(click)="handle()"

Metadata

Directive

{ }

Metadata

UserModule
​{}

CardModule
​{}

ListModule
​{}

Module

@NgModule({
    declarations: any[],
    imports: any[],
    exports: any[],
    providers: any[],
    ...
})
export MonMondule {}

Component

Life Cycle Hooks

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

Injectable

@Injectable({
  providedIn: 'root'
})
class MonService {}
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>

*ngIf

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

*ngFor

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

Data binding

{{ value }}

[property]="value"

(event)="handlerFunction($event)"

[(ngModel)]="property"

DOM

Component

Template binding {{ }}

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

Property binding [ ]

<input [src]="imageUrl" type="image">
@Component(...)
class ImageComponent {
  imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/c/cf/Angular_full_color_logo.svg';
}

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

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

{ }

A vos claviers !

Génération du projet

ng new oxyl-cocktail
  > CSS ?
    > SCSS

  > Routing ?
    > Yes

Démarrer le serveur

ng serve

Ou

npm start

Détail de l'arborescence

Angular Material

Pour nous aider côté design

Installation :

ng add @angular/material
    > Theme ? Deep Purple/Amber
    > Set up global typography ? Yes
    > Animations ? Yes

Générer un module

Dans le répertoire src/app :

ng generate module custom-material

Création du composant Header (1/2)

Dans src/app, créer le composant Header :

ng generate component header

Design du composant à l'aide de :

Création du composant Header (2/2)

Affichage d'une recette

On va voir si vous suivez...

Créez un composant nommé RecipeCard

Création du modèle (1/4)

Générez les 3 fichiers ci-dessous dans le répertoire src/app/models :

  • recipe.model.ts
  • recipe-ingredient.model.ts
  • ingredient.model.ts
export interface Ingredient {
    name: string;
    id?: number;
}

Création du modèle (2/4)

import { Ingredient } from './ingredient.model';

export interface RecipeIngredient {
  id?: number;
  ingredient: Ingredient;
  quantity: number;
  unit: string;
}

Création du modèle (3/4)

import { RecipeIngredient } from './recipe-ingredient.model';

export interface Recipe {
  id?: number;
  name: string;
  picture: string;
  description: string;
  ingredients: RecipeIngredient[];
  instructions: string[];
}

Création du modèle (4/4)

export const MOCK_RECIPES: Recipe[] = [
  {
    id: 0,
    name: 'Daiquiri',
    picture:
      'https://www.liquor.com/thmb/AN9OCZOXjnCO8_YpiYWNTLZDGjY=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc():format(webp)/mango-brava-daiquiri-720x720-primary-4a3e81859e1e497bbd528f71ef09494a.jpg',
    description:
      `The classic Daiquiri is a super simple rum cocktail that’s well-balanced and refreshing.
      The combination of sweet, sour and spirit is refreshingly tangy and perfect for any occasion.`,
    ingredients: [
      {
        ingredient: {
          name: 'Dark rum (Appleton Estate Reserve)',
        },
        quantity: 2,
        unit: 'oz'
      },
      {
        ingredient: {
          name: 'Fresh lime juice',
        },
        quantity: 1,
        unit: 'oz'
      },
      {
        ingredient: {
          name: 'Simple sirup',
        },
        quantity: 1,
        unit: 'oz'
      }
    ],
    instructions: [
      'Add all the ingredients to a shaker and fill with ice.',
      'Shake, and strain into a chilled Martini glass.',
      'Garnish with a lime wheel.'
    ]
  },
  {
    id: 1,
    name: 'Piña Colada',
    picture:
      'https://www.liquor.com/thmb/hmc01qQqlwI0H1od1Qw0me4LEjI=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc():format(webp)/__opt__aboutcom__coeus__resources__content_migration__liquor__2019__02__13090826__pina-colada-720x720-recipe-253f1752769447f6998afd2b9469c24e.jpg',
    description:
      `The Piña Colada is a classic tropical cocktail with rum, pineapple and coconut milk.
      This classic recipe will transport you to paradise. Getting caught in the rain is not required.`,
    ingredients: [
      {
        ingredient: {
          name: 'Light or gold rum',
        },
        quantity: 1.5,
        unit: 'oz'
      },
      {
        ingredient: {
          name: 'Coconut milk',
        },
        quantity: 2,
        unit: 'oz'
      },
      {
        ingredient: {
          name: 'Fresh pineapple juice',
        },
        quantity: 2,
        unit: 'oz'
      }
    ],
    instructions: [
      'Add all the ingredients to a shaker and fill with ice.',
      'Shake, and strain into a Hurricane glass filled with fresh ice.',
      'Garnish with a cherry and a pineapple wedge.'
    ]
  },
  {
    id: 2,
    name: 'Mojito',
    picture:
      'https://www.liquor.com/thmb/G6gVUxrTRCesHawcaUYl9ITSNmA=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc():format(webp)/mojito-720x720-primary-6a57f80e200c412e9a77a1687f312ff7.jpg',
    description:
      `To many people, the Mojito represents the perfect rum cocktail. The origins of the drink can be traced back to
      Cuba and the 16th-century Cuban cocktail El Draque, named for Sir Francis Drake. `,
    ingredients: [
      {
        ingredient: {
          name: 'Mint leaves',
        },
        quantity: 6,
        unit: ''
      },
      {
        ingredient: {
          name: 'Simple syrup',
        },
        quantity: 0.75,
        unit: 'oz'
      },
      {
        ingredient: {
          name: 'Fresh lime juice',
        },
        quantity: 0.75,
        unit: 'oz'
      },
      {
        ingredient: {
          name: 'White rum',
        },
        quantity: 1.5,
        unit: 'oz'
      },
      {
        ingredient: {
          name: 'Club soda',
        },
        quantity: 1.5,
        unit: 'oz'
      }
    ],
    instructions: [
      'In a shaker, lightly muddle the mint.',
      'Add the simple syrup, lime juice and rum, and fill with ice.',
      'Shake well and pour (unstrained) into a highball glass.',
      'Top with the club soda and garnish with a mint sprig.'
    ]
  },
  {
    id: 3,
    name: 'Dirty Martini',
    picture:
      'https://www.liquor.com/thmb/rSFRMIErR5V0GN1eQMobHHwX498=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc():format(webp)/dirty-martini-1500x1500-hero-6cbd60561031409ea1dbf1657d05cb2d.jpg',
    description:
      'A dash of olive brine brings a salty, savory note to the all-time classic.',
    ingredients: [
      {
        ingredient: {
          name: 'Gin or vodka',
        },
        quantity: 2.5,
        unit: 'oz'
      },
      {
        ingredient: {
          name: 'Dry vermouth',
        },
        quantity: 0.5,
        unit: 'oz'
      },
      {
        ingredient: {
          name: 'Olive brine',
        },
        quantity: 0.5,
        unit: 'oz'
      }
    ],
    instructions: [
      'Add all the ingredients to a mixing glass filled with ice.',
      'Stir, and strain into a chilled cocktail glass.',
      'Garnish with 2 olives.'
    ]
  }
];>// TODO : add mock recipes here

Utilisation d'un mock

Affichage d'une recette (1/2)

Paramètre d'entrée du composant :

@Input()
recipe: Recipe;

Design du composant à l'aide de :

Affichage d'une recette (2/2)

Affichage d'une liste de recettes

Composant Recipe List :

ng generate component recipe-list

Affichage des recettes (1/5)

Afficher la liste :

<app-recipe-card *ngFor="let recipe of recipes"
                 [recipe]="recipe">
</app-recipe-card>

Affichage des recettes (2/5)

Affichage des recettes (3/5)

Afficher / masquer les détails :

<button (click)="toggleIngredients()">
    See more
</button>

Affichage des recettes (4/5)

Affichage des recettes (5/5)

Point théorique : asynchrone

Si vous êtes en avance :

  • Ajouter un background de couleur
  • Affichage responsive de la liste
  • Modifier le bouton "Details" en "primary" (voir la doc Angular Material)

 

On se synchronise (😎) pour la suite des explications sur l'asynchrone

Point théorique : asynchronisme

Process

A

Process

B

Process

A

Process

B

Process A en attente tant que B n'est pas terminé

Process A se poursuit en attendant que B se termine

Processus Synchrone

Processus Synchrone

RXJS et le pattern Observable

Récupération des données de l'Observable

Dans un composant appelant le getRecipes() du service.

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

Création du service

Générer le service RecipeService

ng generate service recipe

Service RecipeService

Refacto permettant de gérer l'asynchrone :

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

Création du service

Unmocking & Requête vers le back (1/4)

Utilisation du HttpClient :

imports: [
        ...,
        HttpClientModule,
        ...
]

AppModule :

Créer un fichier : app/environments/dev.ts

export const API_URL = 'http://52.47.189.244:8080/api/v1';

dev.ts :

Unmocking & Requête vers le back (2/4)

Utilisation du HttpClient :

private readonly recipeUrl = `${API_URL}/recipes`

constructor(private http: HttpClient) { }

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

RecipeService :

Unmocking & Requête vers le back (3/4)

Passage au HttpClient :

getRecipe(id: number): Observable<Recipe> {
    return this.http.get<Recipe>(`${ this.recipeUrl }/${ id }`);
}

RecipeService, ajout d'une requête "by ID" :

Unmocking & Requête vers le back (4/4)

Routing (1/2)

const routes: Routes = [
    {
        path: 'recipes',
        component: RecipeListComponent,
        pathMatch: 'full'
    },
    {
        path: '**',
        redirectTo: 'recipes',
        pathMatch: 'full'
    }
];

AppRoutingModule

@NgModule({
    exports: [
        RouterModule
    ],
    imports: [
        RouterModule.forRoot(routes)
    ]
})
<router-outlet></router-outlet>

Point d'entrée des routes

Routing (2/2)

Refacto : Modularisation

AppModule

SharedModule

CoreModule

RecipeModule

...Module

Refacto : lazy-loading

const routes: Routes = [
  {
    path: '',
    component: RecipeListComponent
  },
];
const routes: Routes = [
  {
    path: '',
    redirectTo: '/recipes',
    pathMatch: 'full'
  },
  {
    path: 'recipes',
    loadChildren: () => import('./recipe/recipe.module')
                         .then(m => m.RecipeModule)
  }
];

app-routing.module.ts

recipe-routing.module.ts

A vous de jouer

Nouvelle page : Formulaire d'ajout de recette

  • Ajout d'une nouvelle page de formulaire de création de recette
  • Ajout d'une nouvelle fonctionnalité POST dans le service

TIPS :  

  • https://angular.io/guide/reactive-forms#add-a-formgroup

Nouvelle page : Détail d'une recette

  • Ajout d'une nouvelle page + composant : recipe-detail.component
  • Navigation par le router : /recipes/:id
  • Récupération d'une recette "byId"

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.activatedRoute.snapshot.params['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)

Suppression de recette

Dans la page de détail :

  • Bouton de suppression de recette
  • Ajout d'une nouvelle fonctionnalité DELETE dans le service

Pour les rapides

Excilys Cocktails

Pipes : Transformer les données

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

Excilys Cocktails

Tests unitaires !

Karma + Jasmine

Excilys Cocktails

Animations : Animer les composants

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

Les liens utiles

Formation Angular v2

By Julien Lepasquier

Formation Angular v2

  • 86