Dynamisez votre HTML
Angular apporte une syntaxe puissante aux templates. Dans le chapitre précédent, nous avons vu l'interpolation de texte {{}}
. Dans ce chapitre, nous aborderons quelques éléments de ce système de syntaxe : le property binding, l'event binding, le class et style binding, ainsi que les blocs de contrôle de flux.
Property binding
Pour lier un élément HTML à la propriété d'un composant, placez-le entre crochets []
. Les crochets, []
, obligent Angular à évaluer le côté droit de l'affectation en tant qu'expression dynamique. Sans les crochets, Angular traite le côté droit comme un littéral et donne pour valeur à la propriété cette valeur statique. []
est la syntaxe pour le one-way data binding avec les données circulant du composant vers le template.
Voici un exemple où les propriétés du DOM href et disabled sont définies à partir de variable de la classe du composant.
<a [href]="url">Search on google</a>
<button [disabled]="isUnchanged">Edit</button>
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
url = 'google.com'
isUnchanged = false
}
Exercice: Essayez de lier les attributs src
et width
de l'image aux variables de la classe du composant
Class et style binding
Class binding
Vous pouvez utiliser le class binding pour ajouter et supprimer des noms de classe CSS à l'attribut class
d'un élément. Pour créer un class binding pour une seule classe, utilisez le préfixe class
suivi d'un point et du nom de la classe CSS, par exemple, [class.emphasis]="onSale"
. Angular ajoute la classe emphasis
lorsque l'expression liée, onSale
, est vraie et supprime la classe emphasis
lorsque l'expression est fausse.
<p [class.my-class-1]="isWarning">This is a warning text</p> <!--Le texte est en rouge-->
.my-class-1 {
color: red;
}
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
isWarning = true
}
Plusieurs classes peuvent également être liées avec la syntaxe [class]
:
<p [class]="classExpression"></p>
.warning {
color: red;
}
.center {
text-align: middle;
}
.big {
font-size: 24px;
}
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
classExpression = { warning: true, center: false, big: true } // syntaxe objet
// Ces autres syntaxes sont aussi légales:
// classExpression = "warning center big" // syntaxe chaine de caractère
// classExpression = ['warning', 'center', 'big'] // syntaxe tableau
}
Style binding
Vous pouvez utiliser le style binding pour définir des styles de manière dynamique. Pour créer un style binding pour une seule propriété de style CSS, utilisez le préfixe style
suivi d'un point et du nom de la propriété de style CSS, par exemple, [style.width]="slimWidth"
avec slimWidth = "100px"
(slimWidth
est une chaîne de caractères). Falcutativement, vous pouvez ajouter une unité comme em
ou %
lorsque pertinent: [style.width.px]="slimWidth"
avec slimWidth = 100
(slimWidth est alors un nombre).
<!-- Le nom de la propriété de style peut être écrité en dash-case ou camelCase -->
<nav [style.background-color]="value"></nav>
<nav [style.backgroundColor]="value"></nav>
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
value = 'white'
}
Pour activer plusieurs styles sur une même balise html, utiliser l'attribut [style]
:
<p [style]="styleExpression"></p>
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
styleExpression = {width: '100px', height: '100px', backgroundColor: 'red'} // Syntaxe objet
// La syntaxe chaine de caractères est également légale:
// styleExpression = "width: 100px; height: 100px; background-color: red;"
}
Exercise: attribuer une classe et une couleur à chaque fantôme en utilisant [class] ou [style]
Directive ngModel
La directive ngModel
vous permet de lier la valeur d'un champ de formulaire à une propriété du composant. Il s'agit d'une liaison bidirectionnelle : la propriété est mise à jour lorsque le contenu du champ change (typiquement par l'utilisateur) et vice versa. La syntaxe pour la liaison de données bidirectionnelle est [()]
(bananes dans la boite).
<label>
What is your name ?
<input [(ngModel)]="name">
</label>
<p>Hello {{ name }} !</p>
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
name = ''
}
Testez-le vous-même :
Hello !
Import
La directive ngModel
ne fait pas partie des imports par défaut du composant. Vous devez l'ajouter vous-même : ajoutez FormsModule
au tableau d'imports du décorateur @Component
du composant.
Exercice : utilisez [(ngModel)] sur les balises input, select, radio et checkbox
Bloc @if
Vous pouvez ajouter ou supprimer un élément en l'incluant dans un bloc @if
. Lorsque la condition du @if
est fausse, Angular supprime son contenu du DOM. Angular supprime ensuite les instances des classes de ces composants s'il en existe, ce qui libère de la mémoire et des ressources. Si vous souhaitez uniquement masquer l'élément sans le retirer du DOM, vous pouvez utiliser [hidden]
qui ajoute/supprime uniquement la propriété CSS display:none
sur l'élément. @if
est utile comme moyen de se prémunir des valeurs nulles ou undefined.
<!-- Le message Hello, ... ne sera affiché que si selectedCustomer est ni null ni undefined-->
@if (selectedCustomer) {
<div>Hello, {{selectedCustomer.name}}</div>
}
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
selectedCustomer = { name: 'Smith', age: 45 }
}
WARNING
Soyez prudent lorsque vous utilisez @if
pour tester la nullité sur des valeurs numériques car 0
est une valeur fausse.
Vous pouvez fournir une instruction else if ou else comme suit :
@if (a > b) {
{{a}} is greater than {{b}}
} @else if (b > a) {
{{a}} is less than {{b}}
} @else {
{{a}} is equal to {{b}}
}
Exercice: utilisez un bloc @if
pour montrer/cacher le loader
Bonus: utilisez un @if
et @else
pour montrer soit les données soit le message comme quoi les données ne sont pas disponibles
Syntaxe avant Angular 17 (directive *ngIf)
<!--Le message Hello, ... ne sera affiché que si selectedCustomer est ni null ni undefined-->
<div *ngIf="selectedCustomer">Hello, {{selectedCustomer.name}}</div>
Avec l'ancienne syntaxe, il est possible de fournir un else mais pas un else if:
<div *ngIf="condition; else elseBlock">Content to render when condition is true.</div>
<ng-template #elseBlock><div>Content to render when condition is false.</div></ng-template>
<ng-template>
crée un fragment de template. Un fragment n'est pas affiché (rendered) par défaut. #elseBlock
est une variable de template qui permet d'obtenir une référence au <ng-template>
et de dire à la directive *ngIf
quoi affiché en cas de else.
Bloc @switch
Le bloc @switch
échange conditionnellement ce qui est affiché en sélectionnant l'un des template enfants en fonction de la valeur de la condition.
@switch (condition) {
@case (caseA) {
Case A.
}
@case (caseB) {
Case B.
}
@default {
Default case.
}
}
La valeur de l'expression conditionnelle est comparé à la valeur du cas en utilisant l'opérateur ===
. @switch
ne nécessite pas d'équivalent de break ou return.
Syntaxe avant Angular 17 (directive ngSwitch)
<div [ngSwitch]="myBeer">
<div *ngSwitchCase="'Ale'">Short fermentation</div>
<div *ngSwitchCase="'Lager'">Long fermentation</div>
<div *ngSwitchCase="'Sour ale'">Crafted from wild yeasts</div>
<div *ngSwitchDefault>No random knowledge for that type of beer, sorry.</div>
</div>
Bloc @for
Vous pouvez utiliser le bloc @for
pour présenter une liste d'éléments. Le contenu du bloc @for
est répété pour chaque élément de la collection.
@for (item of items; track item.id) {
<div>{{item.name}}</div>
}
@empty {
<div>No item found</div>
}
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
items = [
{ id: 1, name: 'hammer' },
{ id: 2, name: 'nail' },
{ id: 3, name: 'lightbulb' },
]
}
item
est l'élément courant de la collection lors de l'itérationitems
est la collection sur laquelle itérertrack
indique comment suivre les éléments de la collection. Il doit être un identifiant unique. C'est ce qui permet à Angular d'exécuter un ensemble minimal d'opérations DOM lorsque des élémentsde la collection sont ajoutés, supprimés ou déplacés. Pour les collections qui restent statiques,track $index
fournit un mécanisme de suivi simple.
Un bloc optionnel @empty
peut être inclus juste après le bloc @for
. Le contenu du bloc @empty
s'affiche lorsqu'il n'y a pas d'éléments dans la collection.
À l'intérieur du bloc @for
, plusieurs variables contextuelles implicites sont toujours disponibles : $count
, $index
, $first
, $last
, $odd
et $even
. Elles peuvent être renommées via un segment let
, ce qui peut être utile en cas d'utilisation de boucles @for
imbriquées où les noms des variables contextuelles pourraient entrer en collision.
@for (item of items) {
<div>{{$item}}/{{$count}}: {{item.name}}</div>
}
<!-- Avec un alian pour $index -->
@for (item of items; let i = $index) {
<div>{{i}}: {{item.name}}</div>
}
<!-- Avec un alias pour $even -->
@for (item of items; let isEven = $even) {
<div>{{item.name}} is {{isEven ? 'even': 'odd'}}</div>
}
Exercice : utilisez deux boucles @for
pour afficher tout le contenu du panier (une boucle pour chaque type d'élément, et dans cette boucle une autre boucle pour afficher autant d'emojis de cet élément que sa quantité)
Bonus : Un intrus est dans la corbeille de fruits, cachez le maïs avec un bloc @if
Syntaxe avant Angular 17 (directive *ngFor)
<div *ngFor="let item of items">{{item.name}}</div>
<!-- Avec une variable locale pour l'index -->
<div *ngFor="let item of items; let i = index">{{i}}: {{item.name}}</div>
<!-- Avec une variable locale pour connaitre la parité de l'élément courant -->
<div *ngFor="let item of items; let isEven = even">
{{item.name}} is {{isEven ? 'even': 'odd'}}
</div>
*ngIf
et *ngFor
ne peuvent pas être placés en même temps sur un élément HTML. Pour répéter un bloc d'HTML lorsqu'une condition particulière est vraie, soit un niveau supplémentaire d'HTML doit être introduit, ce qui n'est pas toujours souhaitable et peut casser le style, soit la balise <ng-container>
fournie par Angular peut être utilisée . <ng-container>
n'est pas présent dans le DOM.
<!-- Sans ng-container -->
<div *ngIf="condition">
<div *ngFor="let item of items">{{item.name}}</div>
</div>
<!-- Avec ng-container (solution à préférer) -->
<ng-container *ngIf="condition">
<div *ngFor="let item of items">{{item.name}}</div>
</ng-container>
Event binding
L'event binding permet d'écouter et de répondre aux actions de l'utilisateur telles que les frappes, les mouvements de souris, les clics et les touchers ou un événement custom émis par un composant enfant. Pour lier le composant à un événement, utilisez la syntaxe d'event binding d'Angular ()
.
<button (click)="delete()">Delete</button>
Exercice : utilisez des événements pour ajouter un singe lorsque vous cliquez sur le bouton et faites-lui ouvrir les yeux au survol de la souris
Bloc @let
Le bloc @let
sert à définir une variable locale qui peut être réutilisée dans l'ensemble du template. Elle sera réévaluée chaque fois que le cycle de détection du changement est exécuté. Les variables déclarées avec @let
peuvent être utilisées après leur déclaration et dans le scope dans lequel elles ont été déclarées ainsi que dans ses scopes imbriqués.
<!-- Usage avec une variable de templace référençant un élément du DOM -->
<input #name>
@let greeting = 'Hello ' + name.value;
<p>{{greeting}}</p>
<!-- Usage avec un pipe async -->
@let user = this.user$ | async;
<p>{{user.firstname}} {{user.lastname}} is deactivated</p>
À propos des directives
Dans ce chapitre, nous avons vu 1 directive intégrée (4 en utilisant la syntaxe prédatant Angular 17). Les directives sont des classes déclarées avec le décorateur @Directive
.
Il existe trois types de directives :
- Les composants qui sont des directives avec un template (
@Component
hérite de@Directive
) - Les directives d'attributs qui modifient l'apparence ou le comportement d'un élément
- Les directives structurelles qui modifient la mise en page DOM en ajoutant et en supprimant des éléments DOM
Vous pouvez en savoir plus sur la création de vos propres directives ici et ici. Les directives customs ont de nombreux avantages, en voici quelques-uns :
- Comportement réutilisable : Les directives customs encapsulent des fonctionnalités qui peuvent être réutilisées dans plusieurs composants, aidant ainsi à éviter la duplication de code et à promouvoir une mise en œuvre cohérente de fonctionnalités telles que la validation, la gestion des événements ou le style.
- Séparation des préoccupations : En isolant des comportements spécifiques ou des manipulations du DOM au sein des directives, vous pouvez garder vos composants plus propres et plus centrés sur leurs tâches principales, ce qui conduit à une meilleure organisation et maintenabilité du code.
- Manipulation dynamique du template : Les directives structurelles personnalisées permettent d'ajouter ou de supprimer conditionnellement des éléments du DOM, offrant des capacités de rendu dynamique puissantes. Cela peut être utilisé pour créer des fonctionnalités telles que des affichages conditionnels (similaires à
@if
), des boucles (comme@for
), ou toute logique personnalisée pour manipuler la mise en page en fonction de l'état de l'application.
Quizz: Which type of directive is ngModel?
TP : Liste des films
- Dans le composant LoginFormComponent, ajoutez deux variables de classe email (
email = ''
) et password (password = ''
) et utilisez la directive[(ngModel)]
sur les inputs email et mot de passe dans l'html pour les lier. Rappelez-vous de l'avertissement dans le paragraphe sur le ngModel : n'oubliez pas d'importer leFormsModule
dans le tableau d'imports du decorateur@Component
pour utiliser la directive ngModel dans le template. - Ajoutez une variable
loggedIn
à la classe initialement définie àfalse
, puis utilisez l'event binding sur l'événement(ngSubmit)
de la balise<form>
pour la passer àtrue
lorsque le formulaire est soumis (créez une méthodelogin()
dans la classe du composant pour ça). - Dans
login-form.component.html
, ajoutez le code HTML suivant sous le formulaire d'authentification :
<ul class="films">
<li class="film card">
<img
class="poster"
alt="film poster"
src="https://m.media-amazon.com/images/M/MV5BMDdmZGU3NDQtY2E5My00ZTliLWIzOTUtMTY4ZGI1YjdiNjk3XkEyXkFqcGdeQXVyNTA4NzY1MzY@._V1_SX300.jpg"
/>
<div>
<p class="title">
Titanic
<span class="rating">★★★★</span>
</p>
<dl>
<dt>Release date</dt>
<dd>07/01/1998</dd>
<dt>Director</dt>
<dd>James Cameron</dd>
<dt>Actors</dt>
<dd>Leonardo DiCaprio, Kate Winslet, Billy Zane, Kathy Bates</dd>
</dl>
<p class="plot">
84 years later, a 100 year-old woman named Rose DeWitt Bukater tells the
story to her granddaughter Lizzy Calvert, Brock Lovett, Lewis Bodine,
Bobby Buell and Anatoly Mikailavich on the Keldysh about her life set in
April 10th 1912, on a ship called Titanic when young Rose boards the
departing ship with the upper-class passengers and her mother, Ruth DeWitt
Bukater, and her fiancé, Caledon Hockley. Meanwhile, a drifter and artist
named Jack Dawson and his best friend Fabrizio De Rossi win third-class
tickets to the ship in a game. And she explains the whole story from
departure until the death of Titanic on its first and last voyage April
15th, 1912 at 2:20 in the morning.
</p>
</div>
</li>
</ul>
- Utilisez un bloc
@if
pour afficher le formulaire d'authentification et masquer la liste des films lorsqueloggedIn === false
, et vice versa. - Ajoutez le modèle suivant dans le dossier src/app/models, nommez le fichier film.ts :
export interface Film {
title: string
released: string
director: string
actors: string
poster: string
plot: string
metascore: string
}
- Ajoutez le champ suivant dans la classe du composant LoginFormComponent (VSCode devrait vous affichez un erreur et vous proposez d'importer le modèle Film dans le composant, vous devriez voir l'alias @models utilisé dans l'import) :
films: Film[] = [
{
title: 'Titanic',
released: '19 Dec 1997',
director: 'James Cameron',
actors: 'Leonardo DiCaprio, Kate Winslet, Billy Zane, Kathy Bates',
poster: 'https://m.media-amazon.com/images/M/MV5BMDdmZGU3NDQtY2E5My00ZTliLWIzOTUtMTY4ZGI1YjdiNjk3XkEyXkFqcGdeQXVyNTA4NzY1MzY@._V1_SX300.jpg',
plot: `84 years later, a 100 year-old woman named Rose DeWitt Bukater tells the story to her granddaughter Lizzy Calvert, Brock Lovett, Lewis Bodine, Bobby Buell and Anatoly Mikailavich on the Keldysh about
her life set in April 10th 1912, on a ship called Titanic when young Rose boards the departing ship with the upper-class passengers and her mother, Ruth DeWitt Bukater, and her fiancé, Caledon Hockley.
Meanwhile, a drifter and artist named Jack Dawson and his best friend Fabrizio De Rossi win third-class tickets to the ship in a game. And she explains the whole story from departure until the death of Titanic
on its first and last voyage April 15th, 1912 at 2:20 in the morning.`,
metascore: '75'
},
{
title: 'Blade Runner',
released: '25 Jun 1982',
director: 'Ridley Scott',
actors: 'Harrison Ford, Rutger Hauer, Sean Young, Edward James Olmos',
poster: 'https://m.media-amazon.com/images/M/MV5BNzQzMzJhZTEtOWM4NS00MTdhLTg0YjgtMjM4MDRkZjUwZDBlXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_SX300.jpg',
plot: 'A blade runner must pursue and terminate four replicants who stole a ship in space, and have returned to Earth to find their creator.',
metascore: '89'
},
{
title: 'The Shining',
released: '13 Jun 1980',
director: 'Stanley Kubrick',
actors: 'Jack Nicholson, Shelley Duvall, Danny Lloyd, Scatman Crothers',
poster: 'https://m.media-amazon.com/images/M/MV5BZWFlYmY2MGEtZjVkYS00YzU4LTg0YjQtYzY1ZGE3NTA5NGQxXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg',
plot: 'A family heads to an isolated hotel for the winter where an evil spiritual presence influences the father into violence, while his psychic son sees horrific forebodings from both past and future.',
metascore: '63'
}
]
- En utilisant un bloc
@for
, répétez l'élément.film.card
pour afficher autant de films qu'il y en a dans le tableaufilms
. A ce stade, le Titanic est affiché trois fois, occupons-nous de ça dans l'étape suivante. - Complétez la carte avec les données de chaque film en utilisant le property binding et l'interpolation de texte.
- Bonus : Utilisez la propriété
metascore
pour afficher un nombre d'étoiles (de 1 à 5 ★) à côté de chaque titre de film (créez une méthodestarRating
retournant une string comportant le bon nombre d'étoiles à cet effet). - Bonus : Utilisez un bloc
@if
pour afficher uniquement les films avec un score métacritique supérieur à 70. Faites attention à quel noeud du DOM vous incluez dans le bloc pour conserver la sémantique du HTML. Dans la suite du TP, nous n'aurons pas besoin de ce changement, ne le conservez pas une fois que vous êtes arrivé à le faire marcher. - N'oubliez pas de commiter.