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, ...

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