Réutilisez les données ou l’état entre les interfaces en séparant vos composants angulaires en fonction de la responsabilité, c’est-à-dire le modèle de composant conteneur/présentation.
Les données constituent un élément essentiel de toute application Web, et il existe souvent des scénarios dans lesquels les mêmes données doivent être présentées dans différents formats d’interface utilisateur. Le conteneur/présentation composant Le modèle dans Angular sépare les composants en fonction de leurs responsabilités, ce qui facilite la réutilisation des mêmes données ou du même état d’application dans différentes présentations d’interface utilisateur.
Définition des composants de conteneur et de présentation
UN composant de conteneur contient principalement une logique métier et une communication avec l’extérieur de l’application, comme un appel API. Ces composants sont également appelés composants avec état, logiques, fonctionnels ou intelligents.
UN composant de présentation est principalement responsable de la présentation de l’interface utilisateur et du rendu des données qu’elle reçoit. Il ne gère aucune logique métier et n’effectue aucun appel API. Il communique uniquement avec son composant conteneur parent, qui gère les données et la logique. Ces composants sont également appelés composants sans état, d’interface utilisateur ou stupides.
Comme vous le voyez sur le diagramme, un conteneur/composant intelligent est responsable du traitement de la logique métier et de la gestion des données. Il se connecte aux services, aux API ou à la gestion des états ; gère les interactions des utilisateurs ; et transmet les données à ses composants enfants. Il gère généralement des états complexes et des effets secondaires.
UN composant de présentation (enfant) se concentre uniquement sur l’interface utilisateur et la présentation. Il reçoit des données via des entrées, émet des événements via des modèles et ignore tout service ou logique métier. Il est hautement réutilisable et facile à tester, ce qui le rend idéal pour créer des interfaces utilisateur propres et modulaires.
Point de terminaison des données
Pour implémenter le modèle de conception de conteneur/composant de présentation, nous travaillerons avec des données extraites d’un point de terminaison d’API. Dans cet exemple, l’API du produit renvoie une liste de produits, comme illustré dans l’image ci-dessous.
Données dans l’application
Nous récupérerons les données de l’API dans un service. Donc, pour représenter le type de réponse API, définissons une interface dans l’application Angular. Dans l’application, ajoutez un fichier produit.model.ts:
export interface IProductModel {
id: string;
name: string;
description: string;
price: number;
category: string;
}
Après avoir ajouté le modèle, ajoutez un service pour vous connecter à l’API. Ajouter un service à l’aide de la commande Angular CLI ng g s product.
Dans le service, nous utiliserons l’API Angular httpResource pour récupérer les données de l’API.
@Injectable({
providedIn: 'root'
})
export class ProductService {
private apirl = `http://localhost:3000/product`;
private productsResource = httpResource<IProductModel[]>(() => this.apirl );
getProducts(): HttpResourceRef<IProductModel[]|undefined> {
return this.productsResource;
}
}
L’API httpResource est une nouvelle fonctionnalité d’Angular 20. L’API httpResource étend l’API Resource en utilisant HttpClient sous le capot, offrant un moyen transparent d’effectuer des requêtes HTTP tout en prenant en charge les intercepteurs et les outils de test existants.
- httpResource est construit au-dessus de la primitive de ressource.
- Il utilise HttpClient comme chargeur.
- Il sert d’abstraction pour
@angular/common/http. - Il effectue des requêtes HTTP via la pile HTTP d’Angular.
- Cela fonctionne avec des intercepteurs.
Vous pouvez lire en détail sur l’API httpResource dans mon article précédent ici : https://www.telerik.com/blogs/getting-started-httpresource-api-angular.
Nous avons écrit le code pour récupérer les données de l’API. L’étape suivante consiste à intégrer ce code dans le composant conteneur.
Création du composant conteneur
Pour créer le composant conteneur, exécutez la commande CLI ng g c product.
Comme vous vous en souviendrez peut-être lors de la discussion précédente, un composant de conteneur est responsable du traitement de la logique métier et de la gestion des données. Il se connecte aux services, aux API ou à la gestion des états ; gère les interactions des utilisateurs ; et transmet les données à ses composants enfants. Nous utiliserons le service produit pour récupérer des données dans le Composant du produitlui permettant de fonctionner comme un composant de conteneur.
export class Product {
private productService = inject(ProductService);
products: IProductModel[] = [];
constructor() {
effect(() => {
console.log(this.productService.getProducts().value());
this.products = this.productService.getProducts().value() || [];
console.table(this.products);
})
}
}
Pour accéder au composant du produit, ajoutez un itinéraire dans le app.route.ts fichier comme indiqué ci-dessous.
export const routes: Routes = [
{
path: 'product',
loadComponent: () => import('./product/product').then(m => m.Product)
},
{
path: '',
redirectTo: '/product',
pathMatch: 'full'
},
];
Maintenant, lorsque vous accédez au /productvous devriez voir les produits dans le composant conteneur.
À ce stade, nous pouvons créer un test unitaire pour vérifier que le composant conteneur récupère avec succès les données de l’API.
Création de composants de présentation
Comme indiqué précédemment, un composant de présentation se concentre uniquement sur l’interface utilisateur et la présentation. Il reçoit des données via des entrées, émet des événements via des modèles et ignore tout service ou logique métier.
Supposons que nous devions afficher les produits dans deux formats :
- Vue grille
- Vue en liste
Puisque nous avons déjà récupéré les données dans le composant conteneur, nous devons maintenant fournir deux représentations d’interface utilisateur différentes pour les mêmes données. Pour cela, nous créerons deux composants de présentation.
ng g c product-gridng g c product-list
De plus, nous utiliserons Bootstrap pour créer l’interface utilisateur, vérifiez donc que Bootstrap est installé dans le projet. Pour ce faire, exécutez la commande ci-dessous :
npm install bootstrap
Et mettez à jour le angulaire.json fichier comme ci-dessous :
"styles": [
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
]
Dans le composant de présentation, nous définirons une propriété d’entrée pour recevoir les données du composant parent conteneur, comme indiqué ci-dessous.
@Component({
selector: 'app-product-grid',
imports: [],
templateUrl: './product-grid.html',
styleUrl: './product-grid.scss'
})
export class ProductGrid {
products = input<IProductModel[]>();
}
Et puis les produits seront affichés dans un tableau comme indiqué ci-dessous :
<div class="container-fluid mt-3">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Product Name</th>
<th scope="col">Price</th>
</tr>
</thead>
<tbody>
@for(product of products(); track product.id) {
<tr>
<th scope="row">{{ product.id }}</th>
<td>{{ product.name }}</td>
<td>${{ product.price.toFixed(2) }}</td>
</tr>
}
@empty {
<tr>
<td colspan="4" class="text-center py-3">No products found</td>
</tr>
}
</tbody>
</table>
</div>
</div>
De même, nous pouvons créer un autre composant de présentation appelé ProductListcomme indiqué ci-dessous.
@Component({
selector: 'app-product-list',
imports: [],
templateUrl: './product-list.html',
styleUrl: './product-list.scss'
})
export class ProductList {
products = input<IProductModel[]>();
}
Ensuite, les produits seront affichés dans une liste comme indiqué ci-dessous :
<div class="container mt-3">
<div class="row">
@for(product of products(); track product.id){
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ product.name }}</h5>
<p class="card-text">Price: ${{ product.price }}</p>
</div>
</div>
</div>
}
</div>
</div>
Transmission de données et chargement dynamique du composant de présentation
Dans le composant conteneur, nous commençons par définir un ng-templatequi sert d’espace réservé où d’autres composants de présentation peuvent être insérés dynamiquement. Ensuite, nous ajoutons deux boutons qui permettent à l’utilisateur de basculer entre l’affichage du ProductGrid et le ProductList composants dans cet espace réservé.
<div class="container-fluid p-3">
<div class="d-flex justify-content-start">
<button type="button" class="btn btn-secondary btn-sm me-2" (click)="loadGridView()">
Grid View
</button>
<button type="button" class="btn btn-secondary btn-sm" (click)="loadListView()">
List View
</button>
</div>
</div>
<div>
<ng-template #productemp></ng-template>
</div>
Comme vous le voyez, nous créons deux boutons qui chargent le ProductGrid et ProductList composants de présentation et utilisez un ng-template comme espace réservé où ces composants sont rendus.
Tout d’abord, lisons la variable de référence du modèle comme un ViewChild:
@ViewChild('productemp', { read: ViewContainerRef, static: true })
private productRef?: ViewContainerRef;
Ici, le modèle est lu comme un ViewContainerRef afin que les composants puissent y être chargés dynamiquement. Le static: true l’option fait ceci ViewChild disponible pendant la ngOnInit crochet de cycle de vie.
Ensuite, injectez le service et lisez les données sous forme de signal calculé.
private productService = inject(ProductService);
products = computed(() => this.productService.getProducts().value() || []);
Nous chargerons les composants de présentation de manière dynamique, ce qui signifie que le navigateur ne les téléchargera qu’en cas de besoin. Cela fonctionne comme un chargement paresseux pour les composants. Pour y parvenir, nous créons une fonction pour charger le ProductGrid composant, comme indiqué ci-dessous.
async loadGridView() {
if (this.productRef) {
this.productRef.clear();
const { ProductGrid } = await import('../product-grid/product-grid');
const componentRef = this.productRef.createComponent(ProductGrid);
componentRef.setInput('products', this.products());
}
}
Pour charger paresseux le composant de présentation :
- Effacer le
ViewContainerRef. - Utilisez le
importinstruction pour charger dynamiquement leproduct-griddéposer. - Appelez le
createComponentméthode sur leViewContainerRefpour restituer le composant.
L’une des choses les plus importantes, vous devriez remarquer que nous utilisons setInput() pour transmettre des données au composant de présentation.
Vous pouvez lire en détail sur le chargement paresseux d’un composant ici : https://www.telerik.com/blogs/how-to-lazy-load-component-angular.
De la même manière, nous pouvons créer une fonction pour charger un autre composant de présentation ProductList composant comme indiqué ci-dessous :
async loadListView() {
if (this.productRef) {
this.productRef.clear();
const { ProductList } = await import('../product-list/product-list');
const componentRef = this.productRef.createComponent(ProductList);
componentRef.setInput('products', this.products());
}
}
En mettant le tout ensemble, l’implémentation complète du composant conteneur est présentée ci-dessous.
import { Component, computed, effect, inject, signal, ViewChild, ViewContainerRef } from '@angular/core';
import { ProductService } from '../product';
@Component({
selector: 'app-product',
imports: [],
templateUrl: './product.html',
styleUrl: './product.scss'
})
export class Product {
private productService = inject(ProductService);
products = computed(() => this.productService.getProducts().value() || []);
@ViewChild('productemp', { read: ViewContainerRef, static: true })
private productRef?: ViewContainerRef;
async ngOnInit() {
this.loadGridView();
}
async loadGridView() {
if (this.productRef) {
this.productRef.clear();
const { ProductGrid } = await import('../product-grid/product-grid');
const componentRef = this.productRef.createComponent(ProductGrid);
componentRef.setInput('products', this.products());
}
}
async loadListView() {
if (this.productRef) {
this.productRef.clear();
const { ProductList } = await import('../product-list/product-list');
const componentRef = this.productRef.createComponent(ProductList);
componentRef.setInput('products', this.products());
}
}
}
Avec l’approche conteneur/composant de présentation, un code propre devient plus facile à maintenir. Lorsqu’une nouvelle exigence arrive pour afficher des produits dans un format différent, il vous suffit de créer un nouveau composant de présentation pour l’interface utilisateur et d’ajouter une fonction dans le composant conteneur pour le charger. Les composants de présentation existants restent intacts et seul un test unitaire pour la nouvelle fonction dans le composant conteneur est nécessaire.
Capture d’événement du composant de présentation
La dernière étape du modèle conteneur/composant de présentation consiste pour le composant conteneur à gérer les événements émis par le composant de présentation. Par exemple, lorsque l’utilisateur clique sur le Détails bouton dans le ProductGrid (présentationnel), il émet le contenu sélectionné productid comme charge utile. Le composant conteneur reçoit cela productid et agit dessus pour des tâches telles que la navigation, la récupération de données, etc.
Pour émettre l’événement depuis le ProductGrid composant à son parent (le composant conteneur), déclarez un output() propriété dessus et ajoutez une méthode qui appelle .emit() pour envoyer les données au parent ProductComponent.
productNavigate = output<string>();
navigate(id:string){
this.productNavigate.emit(id);
}
Appelez ensuite le navigate() fonction dans le gestionnaire de clic du bouton, comme indiqué ci-dessous :
<td><button (click)="navigate(product.id)">details</button></td>
Sur le composant parent, vous pouvez gérer l’événement enfant de deux manières :
- Traditionnel: Abonnez-vous à l’enfant
EventEmitter/observable avecsubscribe() - Réactif: Convertissez la sortie en un signal angulaire (par exemple, via
outputToObservable→toSignal) et lisez-le de manière réactive
Le parent peut lire l’événement émis par le composant de présentation en utilisant le classique subscribe() approche, comme indiqué ci-dessous.
async loadGridView() {
if (this.productRef) {
this.productRef.clear();
const { ProductGrid } = await import('../product-grid/product-grid');
const componentRef = this.productRef.createComponent(ProductGrid);
componentRef.setInput('products', this.products());
componentRef.instance.productNavigate.subscribe((id: string) => {
console.log("Selected Product ID:", id);
});
}
}
Le parent peut lire l’événement de manière réactive moderne en utilisant le signal comme indiqué ci-dessous.
productId?: Signal<string | undefined>;
async loadGridView() {
if (this.productRef) {
this.productRef.clear();
const { ProductGrid } = await import('../product-grid/product-grid');
const componentRef = this.productRef.createComponent(ProductGrid);
componentRef.setInput('products', this.products());
let a = outputToObservable(componentRef.instance.productNavigate);
this.productId = toSignal(a, { injector: this.injector });
console.log(this.productId());
effect(() => {
const id = this.productId?.();
if (id) {
console.log("Selected Product ID:", id);
}
}, { injector: this.injector });
}
}
Ci-dessus, nous convertissons le ProductGrid sortie vers un observable en utilisant outputToObservable. Après cela, convertir cet observable en signal en utilisant toSignalpuis en lisant effectivement les changements dans la valeur du signal.
Résumé
Le modèle de composant de conteneur/présentation dans Angular aide à organiser les composants en séparant leurs responsabilités.
- Composants du conteneur gérer la logique métier, interagir avec les services et gérer l’état des applications.
- Composants de présentation concentrez-vous uniquement sur l’interface utilisateur et comptez sur les composants de leur conteneur parent pour la communication des données et des événements.
Cette approche facilite la réutilisation des mêmes données ou états dans différentes présentations d’interface utilisateur et permet aux composants de rester plus faciles à gérer.
Merci d’avoir lu. J’espère que cet article a été utile.
Source link

