Skip to content

Commit

Permalink
Update component chapter to reference input() and output()
Browse files Browse the repository at this point in the history
  • Loading branch information
Ocunidee committed Jan 24, 2025
1 parent ccdf591 commit 9ea1df0
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 14 deletions.
125 changes: 118 additions & 7 deletions docs/src/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ We've previously seen that:
- a component is a class decorated with the `@Component` decorator
- it is generated via the CLI by the `ng g c component-name` command
- by default, a component is generated with an associated html file and stylesheet file
- the `@Component` decorator has [options](https://angular.io/api/core/Component#description) like `templateUrl`, `styleUrl` or `selector`.
- the `@Component` decorator has [options](https://angular.dev/api/core/Component#description) like `templateUrl`, `styleUrl` or `selector`.

## View encapsulation and styling

Expand All @@ -24,10 +24,10 @@ Under the default option, styles specified in the component's style file are not
### `:host` selector
Situations may arise where styling the host element of the component from the component's stylesheet is needed. To do so, Angular provides a pseudo-class selector: `:host`.

Let's imagine we require a border on the AppComponent. This is how to add it:
Let's imagine we require a border on the LoginFormComponent. This is how to add it without having to add a `<div>` around the `<form>` and the `<ul>`:

<CodeGroup>
<CodeGroupItem title="app.component.scss">
<CodeGroupItem title="login-form.component.scss">

```css
:host {
Expand All @@ -40,7 +40,7 @@ Let's imagine we require a border on the AppComponent. This is how to add it:
The next example targets the host element again, but only when it also has the active CSS class.

<CodeGroup>
<CodeGroupItem title="app.component.scss">
<CodeGroupItem title="login-form.component.scss">

```css
:host(.active) {
Expand Down Expand Up @@ -91,7 +91,7 @@ Here is how the `AppComponent` would communicate to its child component `BlogPos
// app.component.ts
import { Component } from "@angular/core"
@Component({
selector: "my-app",
selector: "app-root",
templateUrl: "./app.component.html"
})
export class AppComponent {
Expand Down Expand Up @@ -131,6 +131,57 @@ export class BlogPostComponent {

To watch for changes on an `@Input()` property, you can use the `ngOnChanges` lifecycle hook.

::: details input() function to work with Signals (Angular 17+)
The *zone.js* library serves as a critical foundation for change detection in Angular, enabling the framework to automatically track asynchronous operations like HTTP calls, timers or user interactions. By creating execution contexts, *zone.js* allows Angular to know when to update the user interface without manual intervention from developers. As you become proficient in Angular, understanding how Zone.js manages change detection is of great help to optimise the code and tackle complex use cases. Since Angular 17 and particularly the introduction of reactive programming features in Angular, the reliance on *zone.js* is being reconsidered as it is considered an overhead that is not always optimised. As of Angular 19, it is possible to experimentally opt-out of using *zone.js* to manage change detection. Developers are encouraged to explore new reactive paradigms, such as signals and the refined change detection strategies as alternatives to enhance performance and streamline state management. As you advance your Angular skills, staying abreast of these changes will empower you to build more optimised and maintainable applications.

As of Angular 17, it is possible to shift from using the traditional `@Input()` decorator to the more functional `input()` method to communicate data from a parent to a child component. This change is part of Angular's effort to improve performance and simplify change detection while moving away from the overhead of *zone.js*. While `@Input()` automatically triggers change detection through *zone.js*, the `input()` function allows developers to create reactive inputs that are closely integrated with Angular’s new signal-based architecture.

For example:

```ts
import { Component, signal, input } from '@angular/core'

@Component({
selector: 'app-parent',
template: `<app-child [childValue]="parentValue()"></app-child>`
})
export class ParentComponent {
parentValue = signal('Hello from Parent!')
}

@Component({
selector: 'app-child',
template: `<p>Received Value: {{ childValue }}</p>`
})
export class ChildComponent {
childValue = input('Default Value')
}
```

In this example, the ParentComponent uses a signal to pass a reactive value to the ChildComponent, which defines `childValue` using the `input()` function.

When creating reactive inputs using the `input()` function in Angular, you have the flexibility to define whether an input can have a default value or if it is required. When you specify a default value for an input using the `input()` function, it means that the component can operate with that initial value if no external data is passed in from a parent component. This can be particularly useful for creating components that have sensible defaults. If the parent does not provide a value, the component will still render without issues. On the other hand, required inputs indicate that the parent component **must** provide a value for that input.

```ts
import {Component, input} from '@angular/core'

@Component({/*...*/})
export class CustomSlider {
// Declare a required input named 'value'.
value = input.required<number>()
// Create a computed expression that reads the value input
label = computed(() => `The slider's value is ${this.value()}`)
}
```
And here is how you must pass data to the required `value` input:

```html
<custom-slider [value]="50"></custom-slider>
```

As you can see, as long as the application is using *zone.js*, the parent doesn't have to pass a Signal, it can pass regular values or primitives.
:::

**Exercise: Pass down each book's info to the BookComponent**
<iframe height='500' width='100%' src="https://stackblitz.com/edit/angular-input-training?ctl=1&embed=1&file=src/app/book/book.component.ts&hideNavigation=1"></iframe>

Expand Down Expand Up @@ -163,7 +214,9 @@ export class AppComponent {
// app.component.html
<h1>My To-do list</h1>
<ul>
<li *ngFor="let item of items">{{item}}</li>
@for(item of items; track item) {
<li>{{item}}</li>
}
</ul>
<app-add-task (newTask)="addItem($event)"></app-add-task>
```
Expand Down Expand Up @@ -195,6 +248,64 @@ export class AddTaskComponent {

You can play with this example [here](https://stackblitz.com/edit/angular-output-training-example?file=src/app/app.component.ts).

::: details output() function (Angular 17+)
The same way `input()` is replacing `@Input()`, `output()` is replacing `@Output()`. Here is the above exemple using the new `output()` function.

<CodeGroup>
<CodeGroupItem title="Parent component">

```ts
// 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>
@for(item of items; track item) {
<li>{{item}}</li>
}
</ul>
<app-add-task (newTask)="addItem($event)"></app-add-task>
```
</CodeGroupItem>

<CodeGroupItem title="Child component">

```ts
// add-task.component.ts
import { Component, output } from "@angular/core"
@Component({
selector: "app-add-task",
templateUrl: "./add-task.component.html"
})
export class AddTaskComponent {
newTask = output<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>
```
</CodeGroupItem>
</CodeGroup>

:::

**Exercise: Books are now borrowable, communicate when books are borrowed to their parent component**
<iframe height='500' width='100%' src="https://stackblitz.com/edit/angular-output-training?ctl=1&embed=1&file=src/app/book/book.component.html&hideNavigation=1"></iframe>

Expand Down Expand Up @@ -420,6 +531,6 @@ Do not replace the comment with the list of `FilmComponent` yet. This will be do
:::

## To go further
Learn about context aware content projection using [ngTemplateOutlet](https://indepth.dev/posts/1405/ngtemplateoutlet)
Learn about context aware content projection using [ngTemplateOutlet](https://angular.love/ngtemplateoutlet-the-secret-to-customisation)

Angular 14 has introduced *standalone components* and there were taken out of beta in Angular 15. You can learn more about them [here](https://blog.ninja-squad.com/2022/05/12/a-guide-to-standalone-components-in-angular/)
125 changes: 118 additions & 7 deletions docs/src/fr/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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](https://angular.io/api/core/Component#description) comme `templateUrl`, `styleUrl` ou `selector`.
- le décorateur `@Component` a des [options](https://angular.dev/api/core/Component#description) comme `templateUrl`, `styleUrl` ou `selector`.

## Encapsulation de vue et style

Expand All @@ -23,10 +23,10 @@ Sous l'option par défaut, les styles spécifiés dans le fichier de style du co
### Sélecteur CSS `:host`
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 :
Imaginons que nous ayons besoin d'une bordure sur le LoginFormComponent. Voici comment l'ajouter sans rajouter de `<div>` pour englober le `<form>` et le `<ul>` :

<CodeGroup>
<CodeGroupItem title="app.component.scss">
<CodeGroupItem title="login-form.component.scss">

```css
:host {
Expand All @@ -39,7 +39,7 @@ Imaginons que nous ayons besoin d'une bordure sur AppComponent. Voici comment l'
L'exemple suivant cible à nouveau l'élément hôte, mais uniquement lorsqu'il possède également la classe CSS active.

<CodeGroup>
<CodeGroupItem title="app.component.scss">
<CodeGroupItem title="login-form.component.scss">

```css
:host(.active) {
Expand Down Expand Up @@ -90,7 +90,7 @@ Voici comment l'`AppComponent` communiquerait à son composant enfant `BlogPostC
// app.component.ts
import { Component } from "@angular/core"
@Component({
selector: "my-app",
selector: "app-root",
templateUrl: "./app.component.html"
})
export class AppComponent {
Expand Down Expand Up @@ -130,6 +130,57 @@ export class BlogPostComponent {

Pour surveiller les changements sur une propriété `@Input()`, vous pouvez utiliser le hook de cycle de vie `ngOnChanges`.


::: details Fonction `input()` pour travailler avec les signaux (Angular 17+)
La librairie *zone.js* constitue une base essentielle pour la détection des changements dans Angular, permettant au framework de suivre automatiquement les opérations asynchrones telles que les appels HTTP, les timers ou les interactions utilisateur. En créant des contextes d'exécution, *zone.js* permet à Angular de savoir quand mettre à jour l'interface utilisateur sans intervention manuelle de la part des développeurs. Lors de votre montée en compétence en Angular, comprendre comment *Zone.js* gère la détection des changements est très utile pour optimiser le code et traiter des cas complexes. Depuis Angular 17 et particulièrement avec l'introduction des fonctionnalités de programmation réactive dans Angular, la dépendance à *zone.js* est en train d'être réévaluée, car elle est considérée comme une surcharge qui n'est pas toujours optimisée. Depuis Angular 19, il est possible de choisir expérimentalement de ne pas utiliser *zone.js* pour gérer la détection des changements. Les développeurs sont encouragés à explorer de nouveaux paradigmes réactifs, tels que les signaux comme alternative pour améliorer les performances et rationaliser la gestion de l'état. En développant vos compétences en Angular, rester informé de ces changements vous permettra de créer des applications plus optimisées et maintenables.

Depuis Angular 17, il est possible de passer de l'utilisation du décorateur traditionnel `@Input()` à la méthode plus fonctionnelle `input()` pour communiquer entre un composant parent et un composant enfant. Ce changement fait partie des efforts d'Angular pour améliorer les performances et simplifier la détection des changements tout en se distanciant de la surcharge de *zone.js*. Alors que `@Input()` déclenche automatiquement la détection des changements via *zone.js*, la fonction `input()` permet aux développeurs de créer des inputs réactifs qui sont intégrées à la nouvelle architecture basée sur les signaux d'Angular.

Par exemple :

```ts
import { Component, signal, input } from '@angular/core'

@Component({
selector: 'app-parent',
template: `<app-child [childValue]="parentValue()"></app-child>`
})
export class ParentComponent {
parentValue = signal('Hello from Parent!')
}

@Component({
selector: 'app-child',
template: `<p>Valeur reçue : {{ childValue }}</p>`
})
export class ChildComponent {
childValue = input('Valeur par défaut')
}
```

Dans cet exemple, le `ParentComponent` utilise un signal pour passer une valeur réactive au `ChildComponent`, qui définit `childValue` à l'aide de la fonction `input()`. Cette approche réduit la dépendance à *zone.js* tout en améliorant les performances et la réactivité.

Lors de la création d'inputs réactifs à l'aide de la fonction `input()` dans Angular, vous avez la flexibilité de définir si un input a une valeur par défaut ou si elle est requise. Lorsque vous spécifiez une valeur par défaut pour un input en utilisant la fonction `input()`, cela signifie que le composant peut fonctionner avec cette valeur initiale si aucune donnée externe n'est passée à partir d'un composant parent. Cela peut être particulièrement utile pour créer des composants ayant des valeurs par défaut raisonnables. Si le parent ne fournit pas de valeur, le composant s'affichera toujours sans problème. D'autre part, les inputs requis indiquent que le composant parent **doit** fournir une valeur pour cet input.

```ts
import { Component, input } from '@angular/core'

@Component({/*...*/})
export class CustomSlider {
// Déclarer une entrée requise nommée 'value'.
value = input.required<number>()
// Créer une expression calculée qui lit la valeur d'entrée
label = computed(() => `La valeur du curseur est ${this.value()}`)
}
```
Et voici comment vous devez passer des données à l'input `value` requis :

```html
<custom-slider [value]="50"></custom-slider>
```
Comme on peut le voir ici, le composant parent n'a pas besoin de passer un Signal à `value` tant que l'application utilise *zone.js*.
:::

**Exercice : Transmettez les informations de chaque livre au BookComponent**
<iframe height='500' width='100%' src="https://stackblitz.com/edit/angular-input-training?ctl=1&embed=1&file=src/app/book/book.component.ts&hideNavigation=1"></iframe>

Expand Down Expand Up @@ -162,7 +213,9 @@ export class AppComponent {
// app.component.html
<h1>My To-do list</h1>
<ul>
<li *ngFor="let item of items">{{item}}</li>
@for(item of items; track item) {
<li>{{item}}</li>
}
</ul>
<app-add-task (newTask)="addItem($event)"></app-add-task>
```
Expand Down Expand Up @@ -194,6 +247,64 @@ export class AddTaskComponent {

Vous pouvez expérimenter avec cet exemple [ici](https://stackblitz.com/edit/angular-output-training-example?file=src/app/app.component.ts).

::: details Fonction output() (Angular 17+)
De la même manière que `input()` remplace `@Input()`, `output()` remplace `@Output()`. Voici l'exemple d'au-dessus en faisant usage de la fonction `output()`.

<CodeGroup>
<CodeGroupItem title="Parent component">

```ts
// 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>
@for(item of items; track item) {
<li>{{item}}</li>
}
</ul>
<app-add-task (newTask)="addItem($event)"></app-add-task>
```
</CodeGroupItem>

<CodeGroupItem title="Child component">

```ts
// add-task.component.ts
import { Component, output } from "@angular/core"
@Component({
selector: "app-add-task",
templateUrl: "./add-task.component.html"
})
export class AddTaskComponent {
newTask = output<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>
```
</CodeGroupItem>
</CodeGroup>

:::

**Exercice : les livres sont désormais empruntables, communiquez lorsque les livres sont empruntés à leur composant parent**
<iframe height='500' width='100%' src="https://stackblitz.com/edit/angular-output-training?ctl=1&embed=1&file=src/app/book/book.component.html&hideNavigation=1"></iframe>

Expand Down Expand Up @@ -419,6 +530,6 @@ Ne remplacez pas déjà le commentaire par la liste des `FilmComponent`. C'est l
:::

## Pour aller plus loin
En savoir plus sur la projection de contenu contextuelle en utilisant [ngTemplateOutlet](https://indepth.dev/posts/1405/ngtemplateoutlet)
En savoir plus sur la projection de contenu contextuelle en utilisant [ngTemplateOutlet](https://angular.love/ngtemplateoutlet-the-secret-to-customisation)

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](https://blog.ninja-squad.com/2022/05/12/a-guide-to-standalone-components-in-angular/)

0 comments on commit 9ea1df0

Please sign in to comment.