Cet atelier permet de monter en compétence sur les dernières versions d'Angular en migrant une petite application.
npm install
npm start
Bonne pratique : Faites des commits réguliers pour facilement revenir en arrière en cas de besoin
Depuis Angular 16, il est possible de dire au compilateur que l'input est obligatoire.
@Input({required: true}) name: string;
Il n'est plus nécessaire d'importer le service Route pour lire au sein de l'url. En configurant l'option suivante :
Pour des modules :
@NgModule({
imports: [RouterModule.forRoot(routes, {
bindToComponentInputs: true
})],
exports: [RouterModule]
})
Pour du standalone :
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes,
...
withComponentInputBinding()
)
],
});
Depuis une url tel que celle-ci :
http://localhost:4200/your-path/42?query=Angular
On peut récupérer directement les informations sous formes d'input au sein des composants:
import { ActivatedRoute } from '@angular/router';
@Component({ ... })
class YourComponent {
@Input() id?: number;
@Input() crisis?: Crisis;
@Input() otherSample?: string;
@Input() query?: string;
}
utility.ts
et utiliser des @Input() avec l'option de routing bindToComponentInputs
Un composant standalone est un composant autogéré. Il n'appartient pas à un module car il devient, lors de la compilation, lui même un module.
La notion de standalone est en réalité qu'un flag dans le decorator @Component, @Directive :
@Component({
standalone: true,
selector: 'photo-gallery',
// an existing module is imported directly into a standalone component
imports: [MatButtonModule],
template: `
...
<button mat-button>Next Page</button>
`,
})
export class PhotoGalleryComponent {
// logic
}
Ce flag va permettre de dire au compilateur Angular de créer un module lors de la compilation.
Étant donné qu'il est lui même un module, il va pouvoir être importé directement dans un module au même titre que les autres.
Exemple :
@NgModule({
declarations: [AlbumComponent],
exports: [AlbumComponent],
imports: [PhotoGalleryComponent],
})
export class AlbumModule {}
L'avantage des standalone component est la facilité de mettre en place du lazy loading.
On va écrire la même syntax, à peu de chose près que pour un module. La seule chose qui va changer c'est la méthode appelée :
export const ROUTES: Route[] = [
{path: 'admin', loadComponent: () => import('./admin/panel.component').then(mod => mod.AdminPanelComponent)},
// ...
];
Il est possible de séparer le fichier de configuration des routes :
// In the main application:
export const ROUTES: Route[] = [
{path: 'admin', loadChildren: () => import('./admin/routes').then(mod => mod.ADMIN_ROUTES)},
// ...
];
// In admin/routes.ts:
export const ADMIN_ROUTES: Route[] = [
{path: 'home', component: AdminHomeComponent},
{path: 'users', component: AdminUsersComponent},
// ...
];
Depuis Angular 16, il est possible également d'utiliser la notion de default, ce qui simplifie la syntaxe :
// In the main application:
export const ROUTES: Route[] = [
{path: 'admin', loadChildren: () => import('./admin/routes')},
// ...
];
// In admin/routes.ts:
export default [
{path: 'home', component: AdminHomeComponent},
{path: 'users', component: AdminUsersComponent},
// ...
] as Route[];
Pour supprimer l'ensemble des modules, il va falloir modifier le bootstraping de l'application en modifiant la méthode du fichier main.js. Les services nécessaires à l'ensemble de l'application seront fournis dans ce fichier.
bootstrapApplication(PhotoAppComponent, {
providers: [
provideRouter([/* app routes */], {/* options comme withComponentInputBinding() par exemple*/}),
provideHttpClient()
// ...
]
});
Par exemple, provideRouter
remplacera la configuration RouterModule.forRoot
. Il en va de même avec plusieurs fonctions utilitaires.
header
à la main.ng g @angular/core:standalone
pour migrer en automatique un maximum de choses.Depuis la version 17 d'Angular, une nouvelle manière d'écrire les conditions et les boucles dans le template des composants a été introduite.
Il est important de noter que les méthodes précédentes continuent de fonctionner.
Voici un exemple dans un composant du nouveau système de control flow :
// user-controls.component.ts
@Component({
standalone: true,
selector: 'user-controls',
template: `
@if (isAdmin) {
<button>Erase database</button>
}
`,
})
export class UserControls {
isAdmin = true;
}
Note : Pour la suite de ce document, seule la partie template sera présentée, sans le composant lui-même, afin de simplifier l'explication.
Le template
<ng-container *ngIf="a > b">
{{a}} is greater than {{b}}
</ng-container>
devient :
@if (a > b) {
{{a}} is greater than {{b}}
}
Le template
<ng-container *ngIf="a > b; else cond1">
{{a}} is greater than {{b}}
</ng-container>
<ng-template #cond1>
<ng-container *ngIf="b > a; else equal">
{{a}} is less than {{b}}
</ng-container>
<ng-template #equal>
{{a}} is equal to {{b}}
</ng-template>
</ng-template>
devient :
@if (a > b) {
{{a}} is greater than {{b}}
} @else if (b > a) {
{{a}} is less than {{b}}
} @else {
{{a}} is equal to {{b}}
}
Il est toujours possible de créer une variable HTML avec la syntaxe :
@if (users$ | async; as users) {
{{ users.length }}
}
Le template :
<ng-container [ngSwitch]="condition">
<ng-container *ngSwitchCase="'caseA'">Case A.</ng-container>
<ng-container *ngSwitchCase="'caseB'">Case B.</ng-container>
<ng-container *ngSwitchDefault>Default case.</ng-container>
</ng-container>
devient :
@switch (condition) {
@case (caseA) {
Case A.
}
@case (caseB) {
Case B.
}
@default {
Default case.
}
}
Le template
<ng-container *ngFor="let item of items">{{ item.name }}</ng-container>
devient :
@for (item of items; track item.id) {
{{ item.name }}
}
Il est toujours possible d'utiliser les variables contextuelles de la directive @for.
@for (item of items; track item.id; let idx = $index, e = $even) {
Item #{{ idx }}: {{ item.name }}
}
Voici les variables contextuelles disponibles :
Variable contextuel | Signification |
$count | Nombre d'éléments dans la collection |
$index | Index de la ligne |
$first | True si c'est la première ligne |
$last | True si c'est la dernière ligne |
$even | True si la ligne est paire |
$odd | True si la ligne est impaire |
Lorsqu'Angular affiche une liste d'éléments avec @for, ces éléments peuvent être modifiés ou déplacés ultérieurement. Angular doit suivre chaque élément lors de toute réorganisation, généralement en traitant une propriété de l'élément comme un identifiant ou une clé unique.
Cela garantit que toutes les mises à jour de la liste sont correctement reflétées dans l'interface utilisateur et correctement suivies dans Angular, en particulier dans le cas d'éléments ou d'animations avec état.
Pour ce faire, nous pouvons fournir une clé unique à Angular avec le mot-clé track.
Il est également possible de fournir une fonction comme préalablement avec le mot clé trackBy dans la directive ngFor :
@for (item of items; track itemId($index, item)) {
{{ item.name }}
}
Le template :
<ng-container *ngIf="item.length; else empty">
<li *ngFor="let item of items"> {{ item.name }} </li>
</ng-container>
<ng-template #empty>
<li> There are no items. </li>
</ng-template>
devient :
@for (item of items; track item.name) {
<li> {{ item.name }} </li>
} @empty {
<li> There are no items. </li>
}
recipe
vers le modèle full control-flow.$any
et voir que maintenant le typage fonctionne !ng g @angular/core:control-flow
pour migrer en automatique un maximum de choses.Depuis Angular 14 il est possible et recommandé de typer ses formulaires. Cette pratique permet d'utiliser au mieux les possibilités du langage Typescript afin d'avoir un retour au plus tôt sur des erreurs éventuelles de type de données.
Voici un exemple de formulaire typé :
const login = new FormGroup({
email: new FormControl(''),
password: new FormControl(''),
});
Ici, les champs email et password sont typés en string. Cela est possible via l'inférence de type.
L'ínférence de type c'est lorsqu'on ne type pas explicitement le formulaire. On laisse le compilateur faire le travail pour nous.
Dans l'exemple précédent, le compilateur lit la valeur initiale. C'est un string alors le compilateur positionne le champ en tant que string.
Le typage permet d'avoir des erreurs de compilation lorsque vous utilisez cette notation par exemple :
const emailDomain = login.value.email.domain; -> Erreur
En réalité, c'est un peu plus complexe que cela. Le type n'est pas simplement string mais string | undefined | null.
Sur un AbstractForm, on peut utiliser la méthode reset qui va positionner la valeur null par défaut. Il est donc possible d'avoir la valeur null pour l'ensemble des champs du formulaire.
On peut changer ce comportement par défaut en ajoutant une option sur le champs de la façon suivante :
const email = new FormControl('angularrox@gmail.com', {nonNullable: true});
email.reset();
console.log(email.value); // angularrox@gmail.com
Dans ce cas présent, la valeur positionnée par la méthode reset sera la valeur initiale.
Un AbstractControl peut être désactivé. Lorsqu'un champ est disable, alors la valeur retournée par le getter value est undefined.
Il est possible d'éviter cette problèmatique en utilisant la méthode getRawValue à la place du getter value. Cette méthode permet de lire les valeurs des champs disable.
Attention: L'usage de getRawValue peut ne pas pas avoir le comportement attendu !
Parfois l'inférence de type n'est pas possible. Dans ce cas, on va pouvoir utiliser le typage explicite.
Pour typer explicitement, on va positionner entre chevron les types que le champ aura le droit de prendre de la façon suivante :
const email = new FormControl<string|null>(null);
email.setValue('angularrox@gmail.com');
Attention: Le champ email peut être disable. Donc il peut avoir la valeur undefined même si vous ne l'avez pas noté explicitement.
Il est également possible de typer le FormGroup complet sans avoir à décrire champ par champ.
Pour faire cela, on va créer un nouveau type qu'on va attribuer, entre chevrons, au FormGroup.
const FormGroupType = {
count: FormControl<number | null>;
}
formGroup = new FormGroup<FormGroupType>({
count: new FormControl(null)
});
Le code ci-dessus sera équivalent à celui-ci :
formGroup = new FormGroup({
count: new FormControl<number | null>(null)
});
Attention: Le champs count peut être disable. Donc il peut avoir la valeur undefined même si vous ne l'avez pas noté explicitement.
formGroup.count = {value: null} ou {value: 12}
type FormValue = {
count: null | number;
}
function test(val: FormValue) {...}
test(formGroup.count) => error
Ne pas oublier le cas du champ disabled
Types of property 'count' are incompatible.
Type 'number | null | undefined' is not assignable to type 'number | null'.
Type 'undefined' is not assignable to type 'number | null'.
Note : Il existe du code en haut du composant fournit afin de gagner du temps.
Signal est une nouveauté Angular 16. Cela correspond à un observable qui va informer les consommateurs qui l'écoute lorsqu'il change de valeur. Il peut contenir n'importe quelle valeur. Un signal peut être writable ou read-only.
Exemple :
const count: WritableSignal<number> = signal(0);
// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());
On peut changer la valeur du signal via la méthode set :
count.set(3);
Ou par la méthode update qui permet de mettre à jour la valeur en fonction de la valeur précédente :
// Increment the count by 1.
count.update(value => value + 1);
Note : un signal read-only à uniquement le type Signal
On s'abonne aux modifications d'un WritableSignal via la méthode computed :
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);
doubleCount sera mis à jour à chaque mise à jour de count.
Note: le résultat de la méthode computed est read only. On ne peut donc pas utiliser la méthode set, update ou mutate sur ce résultat.
La méthode computed peut s'executer suite à l'écoute de plusieurs signals :
const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
if (showCount()) {
return `The count is ${count()}.`;
} else {
return 'Nothing to see here!';
}
});
Dans cet exemple, conditionalCount dépend de count et de showCount.
Une fonction effect est une fonction qui va s'executer lorsqu'un signal est mis à jour :
effect(() => {
console.log(`The current count is: ${count()}`);
});
Note : La méthode effect est toujours lancée au moins une fois.
Attention, cette méthode peut engendrer des erreurs comme celle-ci : ExpressionChangedAfterItHasBeenChecked.
shopping.service.ts
et de tout les composants associés en utilisant les signals.Depuis Angular 15, Angular a ajouté une nouvelle directive de gestion de chargement d'image NgOptimizedImage
. Cette balise à pour objectif :
NgOptimizedImage
est une directive standalone et donc doit être importée pour pouvoir être utilisée.
import { NgOptimizedImage } from '@angular/common'
...
imports: [
NgOptimizedImage,
// ...
],
Il suffit de remplacer l'attribut src
par ngSrc
.
<img ngSrc="cat.jpg">
Note : Si vous utilisez un chargeur tiers intégré, assurez-vous d'omettre le chemin de l'URL de base de src
, car il sera automatiquement ajouté au début par le chargeur.
Lors de l'usage de l'attribut ngSrc
, il est obligatoire d'attribuer à l'image une taille fixe (width et height). Cela permet de conserver l'espace nécessaire à l'affichage de l'image, de sorte qu'après son chargement, aucune modification de la mise en page ne soit visibile à l'écran.
<img ngSrc="cat.jpg" width="400" height="200">
Note : Dans les cas où vous souhaitez qu'une image remplisse un élément conteneur, vous pouvez utiliser l'attribut fill
. Ceci est souvent utile lorsque vous désirez obtenir un comportement "image d'arrière-plan". Cela peut également être utile lorsque vous ne connaissez pas la largeur et la hauteur exactes de votre image, mais que vous disposez d'un conteneur parent avec une taille connue dans lequel vous souhaitez insérer votre image (voir « ajustement d'objet » ci-dessous).
<img ngSrc="cat.jpg" fill>
Si le ratio de l'image est différent de sa taille configuré, alors un message d'avertissement sera notifié dans la console. Pour résoudre ce problème, vous pouvez utiliser les valeurs auto
pour les attributs height et width.
Les blocks defer sont un peu particulier. Les templates et les dépendances de ces templates ne seront chargés uniquement que lorsque les conditions de chargement sont respectées.
Les blocks @defer prennent en charge une série de déclencheurs, de prélecture et plusieurs sous-blocs utilisés pour la gestion des espaces réservés, du chargement et de l'état d'erreur.
@defer {
<large-component />
}
Les vues différées, également appelées blocs @defer, sont des outils puissant qui peuvent être utilisés pour réduire la taille initiale du bundle de votre application ou différer le chargement des composants lourds qui ne seront peut-être jamais chargés ou chargés à une date ultérieure. L'objectif est d'entraîner un chargement initial plus rapide.
Note : Attention, lorsqu'un block @defer se charge, celui-ci peut entrainer un changement succeptible de modifier la disposition de la page pour l'utilisateur.
Pour que les dépendances au sein d'un bloc @defer soient différées, elles doivent remplir deux conditions :
Les blocs @defer ont plusieurs sous-blocs pour vous permettre de gérer différentes étapes du processus de chargement différé.
@placeholder Par défaut, le bloc @defer ne rend aucun contenu avant d'être déclenché. Le @placeholder est un bloc facultatif qui déclare le contenu à afficher pour ce cas d'usage. Ce contenu d'espace réservé est remplacé par le contenu principal une fois le chargement terminé.
Note : Pour une expérience utilisateur optimale, vous devez toujours spécifier un bloc @placeholder.
Le bloc @placeholder accepte un paramètre facultatif pour spécifier la durée minimale pendant laquelle cet espace réservé doit être affiché. Ce paramètre minimum est spécifié en incréments de temps de millisecondes (ms) ou de secondes (s). Ce paramètre existe pour empêcher le scintillement rapide du contenu de l'espace réservé dans le cas où les dépendances différées sont récupérées rapidement. Le minuteur minimum pour le bloc @placeholder commence une fois le rendu initial de ce bloc @placeholder terminé.
Note : Les dépendances du contenu du bloc @placeholder sont chargées immédiatement au chargement.
@defer {
<large-component />
} @placeholder (minimum 500ms) {
<p>Placeholder content</p>
}
Note : Certains déclencheurs peuvent nécessiter la présence d'un @placeholder ou d'une variable de référence de modèle pour fonctionner. Voir la section Déclencheurs pour plus de détails.
Le bloc @loading est un bloc facultatif qui permet de déclarer le contenu qui sera affiché lors du chargement d'éventuelles dépendances différées. Par exemple, vous pouvez afficher une icône de chargement. Semblable à @placeholder, les dépendances du bloc @loading sont chargées immédiatement au chargement.
Le bloc @loading accepte deux paramètres facultatifs pour spécifier la durée minimale d'affichage de cet espace réservé et la durée d'attente après le début du chargement avant d'afficher le modèle de chargement. Les paramètres minimum et after sont spécifiés par incréments de temps de millisecondes (ms) ou de secondes (s). Tout comme @placeholder, ces paramètres existent pour empêcher le scintillement rapide du contenu dans le cas où les dépendances différées sont récupérées rapidement. Les minuteries minimale et ultérieure du bloc @loading commencent immédiatement après le déclenchement du chargement.
@defer {
<large-component />
} @loading (after 100ms; minimum 1s) {
<img alt="loading..." src="loading.gif" />
}
Le bloc @error vous permet de déclarer le contenu qui sera affiché en cas d'échec du chargement différé. Semblable à @placeholder et @loading, les dépendances du bloc @error sont chargées immédiatement au chargement. Le bloc @error est facultatif.
@defer {
<calendar-cmp />
} @error {
<p>Failed to load the calendar</p>
}
Lorsqu'un block @defer est déclenché, il remplace le placeholder avec le contenu lazy loaded. Il existe deux options pour configurer ce déclenchement : on et when.
on spécifie une condition de déclenchement utilisant un déclencheur de la liste des déclencheurs disponibles ci-dessous. Un exemple serait sur l'interaction ou sur la fenêtre d'affichage.
Plusieurs déclencheurs d'événements peuvent être définis simultanément. Par exemple : sur l'interaction ; on timer(5s) signifie que le bloc @defer sera déclenché si l'utilisateur interagit avec l'espace réservé, ou après 5 secondes.
Remarque : Plusieurs déclencheurs activés sont toujours des conditions OU. De même, les conditions mélangées avec quand sont également des conditions OU.
@defer (on viewport; on timer(5s)) {
<calendar-cmp />
} @placeholder {
<img src="placeholder.png" />
}
Par défaut, l'espace réservé agira comme l'élément surveillé pour entrer dans la fenêtre d'affichage tant qu'il s'agit d'un nœud d'élément racine unique.
@defer (on viewport) {
<calendar-cmp />
} @placeholder {
<div>Calendar placeholder</div>
}
Vous pouvez également spécifier une variable de référence de modèle dans le même modèle que le bloc @defer en tant qu'élément surveillé pour entrer dans la fenêtre. Cette variable est transmise en tant que paramètre sur le déclencheur de la fenêtre d'affichage.
<div #greeting>Hello!</div>
@defer (on viewport(greeting)) {
<greetings-cmp />
}
@defer (on timer(500ms)) {
<calendar-cmp />
}
@defer (on interaction) {
<calendar-cmp />
} @placeholder {
<div>Calendar placeholder</div>
}
Vous pouvez également spécifier une variable de référence de modèle comme élément déclenchant l'interaction. Cette variable est transmise en tant que paramètre sur le déclencheur d'interaction.
<button type="button" #greeting>Hello!</button>
@defer (on interaction(greeting)) {
<calendar-cmp />
} @placeholder {
<div>Calendar placeholder</div>
}
Par défaut, l'espace réservé servira d'élément de survol tant qu'il s'agit d'un nœud d'élément racine unique.
@defer (on hover) {
<calendar-cmp />
} @placeholder {
<div>Calendar placeholder</div>
}
Vous pouvez également spécifier une variable de référence de modèle comme élément de survol. Cette variable est transmise en tant que paramètre sur le déclencheur de survol.
<div #greeting>Hello!</div>
@defer (on hover(greeting)) {
<calendar-cmp />
} @placeholder {
<div>Calendar placeholder</div>
}
@defer (on immediate) {
<calendar-cmp />
} @placeholder {
<div>Calendar placeholder</div>
}
recipe-list
de façon à prefetch uniquement au survol du bouton et de charger uniquement lors du clic.<button *ngIf="!displayIdeas" (click)="displayIdeas = true"><mat-icon fontIcon="people" /></button>
<app-ideas *ngIf="!!displayIdeas" />