Angular est-il vraiment plus compliqué que React ?

Quand on débute avec Angular, il est facile de se sentir découragé face à la multitude de concepts à assimiler. Cette complexité peut inciter à se tourner vers des frameworks comme React, qui semblent plus simples à première vue. Mais est-ce vraiment le cas ?

Abonnez-vous à notre chaîne

Pour profiter des prochaines vidéos sur Angular, abonnez-vous à la nouvelle chaîne YouTube !

Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

Utilser model() pour du double data-binding

Le double data-binding est un concept puissant qui permet de synchroniser automatiquement des données entre différents composants. Voyons comment l'implémenter efficacement avec les nouvelles fonctionnalités d'Angular.

Avec Angular, nous avons maintenant une façon plus moderne de gérer le double data-binding grâce aux model inputs.

Utiliser les composants avec des signaux

Depuis Angular 17.x, les signaux sont introduits en tant que réactifs comme alternative aux décorateurs @Input(), @Output(), etc.

Donc, pour utiliser input(), output(), model(), contentChild(), contentChildren(), viewChild() et viewChildren(), il est essentiel de comprendre la notion des signaux.

Lisez l'article sur les signaux pour en savoir plus.

Voici un exemple simple avec un composant de gestion d'utilisateur :

ts
import { Component, model } from '@angular/core';
import { User } from './user.interface';

@Component({
  selector: 'app-user-form',
  standalone: true,
  template: `
    <div>
      <input 
        [value]="username()" 
        (input)="username.set($event.target.value)"
      >
      <button (click)="updateUsername()">Mettre à jour</button>
    </div>
  `
})
export class UserFormComponent {
  username = model(''); // Création d'un model input

  updateUsername() {
    this.username.update(value => value.toUpperCase());
  }
}
ts
import { Component, signal } from '@angular/core';
import { UserFormComponent } from './user-form.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [UserFormComponent],
  template: `
    <div>
      <h2>Nom d'utilisateur: {{ username() }}</h2>
      <app-user-form [(username)]="username" />
    </div>
  `
})
export class AppComponent {
  username = signal('');
}

Le model() fournit :

  • Une fonction getter username() pour lire la valeur
  • Une méthode set() pour modifier la valeur
  • Une méthode update() pour transformer la valeur (comme dans updateUsername())

La syntaxe [(username)] (banana-in-a-box) permet de :

  • Passer la valeur du signal au composant enfant
  • Recevoir automatiquement les mises à jour quand la valeur change dans l'enfant

SYNTAXE BANANA-IN-A-BOX

La syntaxe [(volume)] est souvent appelée "banana-in-a-box" en raison de sa ressemblance avec une banane dans une boîte. C'est un raccourci pour combiner la liaison de propriété [volume] et l'événement (volumeChange).

Quand utiliser model() ?

model() est idéal quand :

  1. Vous avez besoin d'un double data-binding simple
  2. Vous voulez éviter d'écrire manuellement @Input() et @Output()
  3. Vous travaillez avec des formulaires ou des composants interactifs

Événements de changement implicites

Lorsque vous déclarez un model input, Angular crée automatiquement un événement de sortie correspondant. Le nom de cet événement est le nom du model input suivi de "Change".

Voici un exemple avec une case à cocher personnalisée :

ts
import { Component, model } from '@angular/core';

@Component({
  selector: 'app-custom-checkbox',
  standalone: true,
  template: `
    <div 
      class="checkbox" 
      [class.checked]="checked()"
      (click)="toggle()"
    >
      <span>{{ checked() ? '✓' : '' }}</span>
    </div>
  `
})
export class CustomCheckboxComponent {
  // Crée automatiquement un événement checkedChange
  checked = model(false);

  toggle() {
    this.checked.update(value => !value);
  }
}
ts
import { Component, signal } from '@angular/core';
import { CustomCheckboxComponent } from './custom-checkbox.component';

@Component({
  selector: 'app-user-settings',
  standalone: true,
  imports: [CustomCheckboxComponent],
  template: `
    <div>
      <h3>Paramètres utilisateur</h3>
      <app-custom-checkbox
        [checked]="receiveNotifications()"
        (checkedChange)="receiveNotifications.set($event)"
      />
      <span>Recevoir les notifications</span>
    </div>
  `
})
export class UserSettingsComponent {
  receiveNotifications = signal(false);
}

AVANTAGE

L'utilisation de model() réduit considérablement le code boilerplate par rapport à l'approche traditionnelle avec @Input() et @Output(). Plus besoin de déclarer explicitement l'EventEmitter !

Transformation des valeurs

Si vous avez besoin de transformer les valeurs avant qu'elles ne soient mises à jour, vous pouvez utiliser la méthode update du model input :

ts
import { Component, model } from '@angular/core';

@Component({
  selector: 'app-user-input',
  standalone: true,
  template: `
    <input 
      [value]="username()" 
      (input)="formatUsername($event)"
    >
  `
})
export class UserInputComponent {
  username = model('');

  formatUsername(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    // Transformation : suppression des espaces et mise en minuscules
    this.username.update(() => value.trim().toLowerCase());
  }
}

Utiliser les décorateurs @Input et @Output

Avant Angular 17

Les décorateurs @Input() et @Output() sont la façon traditionnelle de définir des inputs et outputs dans Angular avant la version 17.

Le double data-binding consiste à mettre à jour une variable peu importe où l'information a été modifiée.

Par exemple, si nous avons le nom de l'utilisateur affiché sur les deux composants A et B. Si on modifie le nom sur A, il le sera aussi sur B et réciproquement.

En fait, nous avons une entrée et une sortie.

ts
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-count',
  standalone: true,
  template: `
    <p>{{n}}</p>
    <button (click)="up()">Up Counter</button>
  `
})
export class CountComponent {
  @Input() n = 0;
  @Output() nChange: EventEmitter<number> = new EventEmitter();

  up() {
    this.n++;
    this.nChange.emit(this.n);
  }
}

Le composant du compteur ne change pas vraiment. Nous avons juste ajouté le décorateur Input pour récupérer la valeur en entrée.

Lorsque vous faites du data-binding, la propriété de sortie est le même nom que la propriété d'entrée suffixée de Change. Par exemple, ici :

  • Propriété d'entrée : n
  • Propriété de sortie : nChange

Pourquoi ? Car nous n'indiquerons pas l'attribut nChange dans le template. Cela se fera automatiquement par Angular. Voyez sur le composant parent :

ts
import { Component } from '@angular/core';
import { CountComponent } from './count.component';

@Component({
  selector: 'app',
  imports: [CountComponent],
  template: `
    <div>
      <p>{{val}}</p>
      <button (click)="up()">Up Parent</button>
      <app-count [(n)]="val"></app-count>
    </div>
  `
})
export class AppComponent {
  val: number = 0;

  up() {
    this.val++;
  }
}

Remarquez que nous retrouvons le bouton qui incrémente la propriété val. Cette dernière est propre au composant AppComponent. Regardez que nous faisons du data-binding en combinant l'entrée et la sortie : [(n)]. Ainsi, si vous cliquez sur l'un des boutons, la valeur se synchronise.

Cela revient à faire :

html
  <app-count (n)="val" [nChange]="up()"></app-count>

Chaque mois, recevez en avant-première notre newsletter avec les dernières actualités, tutoriels, astuces et ressources Angular directement par email !