Fermer

octobre 29, 2025

Nettoyer le code à l’aide de composants intelligents et stupides dans Angular

Nettoyer le code à l’aide de composants intelligents et stupides dans Angular


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 intelligent/dumb.

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 composant intelligent/stupide 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 intelligents et stupides

UN composant intelligent contient principalement une logique métier et une communication avec l’extérieur de l’application, comme un appel API.

UN composant stupide 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 intelligent parent, qui gère les données et la logique.

Composant intelligent - parent

Comme vous le voyez sur le diagramme, un 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 intelligent est également appelé composant conteneur.

composant intelligent pointant vers le bas, composant conteneur pointant vers le haut

UN composant stupide 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.

Un composant stupide est également appelé composant de présentation.

Composant stupide pointant vers le bas, composant de présentation pointant vers le haut

Point de terminaison des données

Pour implémenter le modèle de conception Smart-Dumb Component, 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.

Point de terminaison de l'API

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. La prochaine étape consiste à intégrer ce code dans le composant intelligent.

Création du composant intelligent

Pour créer le composant intelligent, 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 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. Nous utiliserons le service produit pour récupérer des données dans le Composant du produitlui permettant de fonctionner comme un composant intelligent.

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

Démo de composants intelligents et stupides

À ce stade, nous pouvons créer un test unitaire pour vérifier que le composant intelligent récupère avec succès les données de l’API.

Créer des composants stupides

Comme indiqué précédemment, un composant stupide 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 :

  1. Vue grille
  2. Vue en liste

Puisque nous avons déjà récupéré les données dans le composant intelligent, nous devons maintenant fournir deux représentations d’interface utilisateur différentes pour les mêmes données. Pour cela, nous allons créer deux composants stupides.

  • ng g c product-grid
  • ng 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 stupide, nous définirons une propriété d’entrée pour recevoir les données du composant parent intelligent, 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 stupide 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 stupide

Dans le composant intelligent, nous commençons par définir un ng-templatequi sert d’espace réservé où d’autres composants stupides 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 stupides 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 stupides 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 stupide :

  • Effacer le ViewContainerRef.
  • Utilisez le import instruction pour charger dynamiquement le product-grid déposer.
  • Appelez le createComponent méthode sur le ViewContainerRef pour restituer le composant.

L’une des choses les plus importantes, vous devriez remarquer que nous utilisons setInput() pour transmettre des données au composant stupide.

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 stupide 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 résumé, la mise en œuvre complète du composant intelligent 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 des composants intelligents et stupides, le code propre devient plus facile à maintenir. Lorsqu’une nouvelle exigence arrive pour afficher les produits dans un format différent, il vous suffit de créer un nouveau composant stupide pour l’interface utilisateur et d’ajouter une fonction dans le composant intelligent pour le charger. Les composants stupides existants restent intacts et seul un test unitaire pour la nouvelle fonction du composant intelligent est nécessaire.

Capturer l’événement d’un composant stupide

La dernière étape du modèle de composant intelligent-dumb consiste pour le composant intelligent à gérer les événements émis par le composant stupide. Par exemple, lorsque l’utilisateur clique sur le Détails bouton dans le ProductGrid (stupide), il émet le composant sélectionné productid comme charge utile. Le composant intelligent 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 intelligent), 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 :

  1. Traditionnel: Abonnez-vous à l’enfant EventEmitter/observable avec subscribe()
  2. Réactif: Convertissez la sortie en un signal angulaire (par exemple, via outputToObservabletoSignal) et lisez-le de manière réactive

Le parent peut lire l’événement émis par le composant stupide 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 intelligent et stupide dans Angular aide à organiser les composants en séparant leurs responsabilités.

  • Composants intelligents gérer la logique métier, interagir avec les services et gérer l’état des applications.
  • Composants stupides concentrez-vous uniquement sur l’interface utilisateur et comptez sur leurs composants intelligents parents 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