Composants
Nous avons vu précédemment que :
- un composant est une classe décorée avec le décorateur
@Component
- il est généré via CLI par la commande
ng g c component-name
- par défaut, un composant est généré avec un fichier html et une feuille de style associés
- le décorateur
@Component
a des options commetemplateUrl
,styleUrl
ouselector
.
Encapsulation de vue et style
Vous pouvez modifier l'extension de feuille de style des fichiers générés par CLI dans le fichier angular.json
sous l'option schematics
.
Encapsulation
Parmi les options du décorateur @Component
, il y en a une qui traite de ViewEncapsulation. Angular fournit trois types d'encapsulation de vue :
ViewEncapsulation.Emulated
(par défaut) : émule le scoping natif,, les styles sont limités au composantViewEncapsulation.None
: tout ce qui est mis dans la feuille de style du composant est disponible globalement dans toute l'applicationViewEncapsulation.ShadowDom
: Angular crée un Shadow DOM pour le composant, les styles sont limités au composant
WARNING
Sous l'option par défaut, les styles spécifiés dans le fichier de style du composant ne sont hérités par aucun composant imbriqué dans son template ni par aucun contenu projeté dans le composant.
:host
Sélecteur CSS Des situations peuvent survenir où styler l'élément hôte du composant à partir de sa propre feuille de style est nécessaire. Pour ce faire, Angular fournit un sélecteur de pseudo-classe : :host
.
Imaginons que nous ayons besoin d'une bordure sur AppComponent. Voici comment l'ajouter :
:host {
border: 1px solid black
}
L'exemple suivant cible à nouveau l'élément hôte, mais uniquement lorsqu'il possède également la classe CSS active.
:host(.active) {
border-width: 3px
}
Lifecycle
Une instance de composant a un cycle de vie qui commence lorsqu'Angular instancie la classe du composant et présente la vue du composant avec ses vues enfants. Le cycle de vie se poursuit avec la détection des modifications, car Angular vérifie quand les propriétés liées aux données changent et met à jour à la fois la vue et l'instance de composant si nécessaire. Le cycle de vie se termine lorsqu'Angular détruit l'instance du composant et retire son template du DOM.
Angular fournit des méthodes de hook pour exploiter les événements clés du cycle de vie d'un composant.
ngOnChanges
: appelée après le constructeur et chaque fois que les valeurs input changent. La méthode reçoit un objet SimpleChanges qui contient les valeurs actuelles et précédentes des propriétés annotées d'@Input().ngOnInit
: appelée une seule fois. C'est là que l'initialisation du composant doit avoir lieu, tel que la récupération des données initiales. En effet, les composants doivent être peu coûteux à construire, les opérations coûteuses doivent donc être tenues à l'écart du constructeur. Le constructeur ne doit pas faire plus que donner des valeurs initiales simples aux variables de la classe.ngDoCheck
: appelée immédiatement aprèsngOnChanges
à chaque exécution du cycle de détection du changement, et immédiatement aprèsngOnInit
lors de la première exécution. Donne la possibilité de mettre en œuvre un algorithme de détection du changement personnalisé.ngAfterContentInit
: appelée une seule fois. Invoquée après qu'Angular ait effectué une projection de contenu dans la vue du composant.ngAfterContentChecked
: appelée aprèsngAfterContentInit
et chaquengDoCheck
suivant.ngAfterViewInit
: appelée une seule fois. Appelée lorsque la vue du composant a été complètement initialisée.ngAfterViewChecked
: appelée aprèsngAfterViewInit
et chaquengDoCheck
suivant.
Pour chaque hook du cycle de vie il existe une interface correspondante. Leurs noms sont dérivés du nom du hook de cycle de vie correspondant moins le ng
. Par exemple, pour utiliser ngOnInit()
, implémentez l'interface OnInit
.
Communication entre les composants enfant et parent
Une pratique courante dans Angular est le partage de données entre un composant parent et un ou plusieurs composants enfants. Pour ce faire, vous pouvez utiliser les directives @Input()
et @Output()
. @Input()
permet à un composant parent de mettre à jour les données dans le composant enfant. Inversement, @Output()
permet à l'enfant d'envoyer des données à un composant parent.
@Input()
L'ajout du décorateur @Input()
sur une propriété d'un composant enfant signifie qu'il peut recevoir sa valeur de son composant parent. Le composant parent transmet cette valeur via property binding dans son template. Une telle propriété ne devrait pas être mutée par l'enfant directement. Les mutations doivent se produire dans le parent, elles se propageront automatiquement via le property binding.
Voici comment l'AppComponent
communiquerait à son composant enfant BlogPostComponent
le titre et le contenu de son article.
// app.component.ts
import { Component } from "@angular/core"
@Component({
selector: "my-app",
templateUrl: "./app.component.html"
})
export class AppComponent {
article = {
title: "My first awesome article",
content: "This content is super interesting"
};
}
// app.component.html
<app-blog-post [title]="article.title" [content]="article.content"><app-blog-post>
// blog-post.component.ts
import { Component, Input } from "@angular/core"
@Component({
selector: "app-blog-post",
templateUrl: "./blog-post.component.html"
})
export class BlogPostComponent {
@Input() title: string
@Input() content: string
}
// blog-post.component.html
<article>
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</article>
Pour surveiller les changements sur une propriété @Input()
, vous pouvez utiliser le hook de cycle de vie ngOnChanges
.
Exercice : Transmettez les informations de chaque livre au BookComponent
@Output()
Les composants enfants communiquent avec leurs parents à l'aide d'événements : ils émettent des événements qui se propagent à leur parent. Un bon composant est agnostique de son environnement, il ne connaît pas ses parents et ne sait pas si les événements qu'il émet seront un jour interceptés (ou "écoutés").
L'ajout du décorateur @Output()
sur une propriété de type EventEmitter
d'un composant enfant permet aux données de circuler de l'enfant vers le parent. Le composant parent peut réagir à l'événement via la syntaxe d'event binding.
Voici comment le AddTaskComponent
communiquerait à son parent qu'une nouvelle tâche a été ajoutée :
// app.component.ts
import { Component } from "@angular/core"
@Component({
selector: "my-app",
templateUrl: "./app.component.html"
})
export class AppComponent {
items = ['Do the laundry', 'Wash the dishes', 'Read 20 pages']
addItem(item: string): void {
this.items.push(item)
}
}
// app.component.html
<h1>My To-do list</h1>
<ul>
<li *ngFor="let item of items">{{item}}</li>
</ul>
<app-add-task (newTask)="addItem($event)"></app-add-task>
// add-task.component.ts
import { Component, EventEmitter, Output } from "@angular/core"
@Component({
selector: "app-add-task",
templateUrl: "./add-task.component.html"
})
export class AddTaskComponent {
@Output() newTask = new EventEmitter<string>()
addNewTask(task: string): void {
this.newTask.emit(task)
}
}
// add-task.component.html
<label>New task: <input #newTask/></label>
<button (click)="addNewTask(newTask.value)">Add</button>
Vous pouvez expérimenter avec cet exemple ici.
Exercice : les livres sont désormais empruntables, communiquez lorsque les livres sont empruntés à leur composant parent
Variable locale dans le template
Un composant parent ne peut pas utiliser le data binding (@Output
ou @Input
) pour accéder aux propriétés ou méthodes d'un enfant. Une variable locale dans le template peut être utilisée pour réaliser les deux.
// app.component.html
<app-greet #child></app-greet>
<button (click)="child.greetMe()">Greet Me</button>
// greet.component.html
<div *ngIf="displayText">Hello User!</div>
// greet.component.ts
import { Component } from '@angular/core'
@Component({
selector: 'app-greet',
templateUrl: './greet.component.html'
})
export class GreetComponent {
displayText: boolean = false
greetMe(): void {
this.displayText = true
}
}
@ViewChild
Le décorateur ViewChild
peut accomplir le même objectif qu'une variable de template mais directement à l'intérieur de la classe du composant parent en injectant le composant enfant dans le composant parent. Utilisez ViewChild
sur une variable locale chaque fois que vous devez coordonner les interactions entre plusieurs composants enfants.
Dans cet exemple, le MenuComponent
obtient l'accès au MenuItemComponent
:
// menu.component.html
<app-menu-item [menuText]="'Contact Us'"></app-menu-item>
// menu.component.ts
@Component({
selector: 'app-menu',
templateUrl: './menu.component.html'
})
export class MenuComponent{
@ViewChild(MenuItemComponent) menu: MenuItemComponent
}
// menu-item.component.html
<p>{{menuText}}</p>
// menu-item.component.ts
@Component({
selector: 'app-menu-item',
templateUrl: './menu-item.component.html'
})
export class MenuItemComponent {
@Input() menuText: string;
}
Dans le cas où le composant parent contient plusieurs instances du même composant enfant, elles peuvent chacune être récupérées via une variable de référence du template :
// menu.component.html
<app-menu-item #contactUs [menuText]="'Contact Us'"></app-menu-item>
<app-menu-item #aboutUs [menuText]="'About Us'"></app-menu-item>
// menu.component.ts
@Component({
selector: 'app-menu',
templateUrl: './menu.component.html'
})
export class MenuComponent{
@ViewChild('aboutUs') aboutItem: MenuItemComponent
@ViewChild('contactUs') contactItem: MenuItemComponent
}
// menu-item.component.html
<p>{{menuText}}</p>
// menu-item.component.ts
@Component({
selector: 'app-menu-item',
templateUrl: './menu-item.component.html'
})
export class MenuItemComponent {
@Input() menuText: string
}
Les composants injectés via @ViewChild
deviennent disponibles dans le hook de cycle de vie ngAfterViewInit
. Pour récupérer tous les enfants d'un certain type, utilisez le décorateur @ViewChildren
.
Projection de contenu
Avec @Input
, nous avons pu transmettre des données à un composant enfant, mais qu'en est-il de la transmission d'éléments HTML ou même d'autres composants ?
Étant donné que les composants Angular sont déclarés en tant que balises, nous pouvons placer d'autres éléments ou contenus à l'intérieur de leurs balises. Dans l'exemple suivant, la chaîne My profile
fait office de contenu du composant NavigationLink
:
<!-- in a parent component's template-->
<app-navigation-link [url]="/profile">My profile<app-navigation-link>
<!-- navigation-link.component.html -->
<div>
<a [routerLink]="url"><ng-content></ng-content></a>
</div>
Tout ce qui est écrit entre les balises du composant enfant dans le composant parent est injecté dans le template de l'enfant et remplace les balises <ng-content>
.
Tout contenu HTML, y compris d'autres composants Angular, peut être projeté. Cette fonctionnalité est particulièrement utile dans les composants qui servent de conteneur plutôt que de contenu, tels que les fenêtres de dialogue ou les éléments de mise en page :
<!-- my-popin.component.html -->
<div class="popin">
<div class="popin-header">
<ng-content select="[slot=header]"></ng-content>
</div>
<main class="popin-content">
<ng-content></ng-content>
</main>
<div class="popin-actions">
<ng-content select="[slot=actions]"></ng-content>
</div>
</div>
<!-- in a parent component template -->
<my-popin>
<h1 slot="header">Popin title</h1>
<p>Popin content</p>
<button slot="actions">OK</button>
</my-popin>
En plus du <ng-content>
par défaut, vous pouvez nommer d'autres balises <ng-content>
pour distribuer le contenu à plusieurs emplacements dans l'enfant. Vous y parvenez en utilisant l'attribut select
sur la balise <ng-content>
et en ajoutant la valeur choisie comme attribut sur l'élément à projeter.
TP : Décomposer l'application
- Refactorisez le
LoginFormComponent
pour extraire le code et le template liés aux détails d'un film. Pour cela, créez avec le CLI unFilmComponent
(ng g c components/film
). Il y aura autant d'instances deFilmComponent
qu'il y a de films (déplacez la balise<li></li>
et son contenu vers le nouveau composant). Utilisez@Input()
pour transmettre les données duLoginFormComponent
à chaqueFilmComponent
. - Créez un autre composant avec le CLI :
FilmSearchComponent
. Il contiendra un formulaire de recherche et la liste deFilmComponent
ci-dessous :
<form (ngSubmit)="searchFilms()">
<label for="search">Search :</label>
<input id="search" type="text" name="title"/>
</form>
<ul class="films">
<!-- list of <app-film> -->
</ul>
Ne remplacez pas déjà le commentaire par la liste des FilmComponent
. C'est le but du point 3.
- Insérez ce
FilmSearchComponent
en dessous duLoginFormComponent
dans le template de l'AppComponent
et déplacez le code nécessaire (html et ts) duLoginFormComponent
vers ce nouveau composant, supprimez le code qui n'est plus utilisé.
Résultat attendu de l'étape 3
- Affichez le composant
FilmSearchComponent
uniquement si l'utilisateur est connecté. Vous devrez communiquer la variableloggedIn
duLoginFormComponent
à l'AppComponent
via un@Output()
(transformez le champ loggedIn). Vous aurez besoin d'une méthodeonLogin()
dans l'AppComponent
. - Dans le
FilmSearchComponent
, affectez initialement la variablefilms
à un tableau[]
vide. Lors de la soumission du formulaire de recherche, exécutez une méthodesearchFilms()
qui mettra les 3 exemples de films dans cette liste. - Commitez
Résultat attendu
Pour aller plus loin
En savoir plus sur la projection de contenu contextuelle en utilisant ngTemplateOutlet
Angular 14 a introduit les standalone components en version beta dans le framework et Angular 15 a rendu leur API stable. Vous pouvez en apprendre plus sur ce type de composants ici