back to top

Composizione di componenti in Angular

Nelle precedenti lezioni abbiamo detto che un’applicazione può essere suddivisa in più componenti che possono essere organizzati in modo da formare una struttura gerarchica avente un componente base che chiamiamo root component. Un componente può avere a sua volta uno o più componenti discendenti. Il primo e più usato modo per annidare dei componenti all’interno di un componente genitore, consiste nell’inserire gli elementi HTML ad essi associati all’interno del template del componente genitore. Tali elementi vengono denominati view children.

relazione fra componenti

Comunicazione fra componenti

Vediamo quindi come annidare dei componenti all’interno di altri e nel farlo iniziamo a mostrare alcune tecniche che permettono lo scambio di informazioni fra componenti. Partiamo dal caso più semplice, ovvero il passaggio di dati dal componente padre al figlio.

Come passare dei dati dal componente genitore ai figli in Angular

Per illustrare questa prima forma di comunicazione, realizziamo un esempio. Dopo aver inizializzato la nostra applicazione col comando:

ng new my-angular-app --no-interactive --prefix simple

creiamo un nuovo componente ProgressBar. Possiamo farlo manualmente o attraverso Angular CLI che provvede a modificare opportunamente anche il file app.module.ts aggiungendo il nuovo componente all’array declarations del modulo.

ng generate component progress-bar -s -t --no-spec

Per semplicità abbiamo creato solo il file progress-bar.component.ts (i flag -s, equivalente a –inline-style, e -t, equivalente a –inline-template, indicano che non vogliamo utilizzare dei file esterni per regole CSS e template) in cui andiamo a definire il nuovo componente.

tree src/app  -a --dirsfirst
src/app
├── progress-bar
│   └── progress-bar.component.ts
├── app.component.css
├── app.component.html
├── app.component.spec.ts
├── app.component.ts
└── app.module.ts

1 directory, 6 files

Come già detto, dopo aver creato il nuovo componente con Angular CLI, viene automaticamente modificato il file app.module.ts in cui viene importato il nuovo componente e aggiunto all’array declarations dell’oggetto che viene passato come argomento al decoratore @NgModule().

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
// Aggiunto automaticamente da Angular CLI
import { ProgressBarComponent } from './progress-bar/progress-bar.component';

@NgModule({
  declarations: [
    AppComponent,
    ProgressBarComponent // Aggiunto automaticamente da Angular CLI
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Passiamo a definire il nuovo componente ProgressBarComponent all’interno del file progress-bar.component.ts. In questa prima versione mostriamo delle informazioni statiche. Si tratta di una semplice barra di progresso che andremo ad inserire nel template del componente AppComponent.

import { Component } from '@angular/core';

@Component({
  selector: 'simple-progress-bar',
  template: `
    <div class="container">
      <div class="progress" style="width: 20%">20%</div>
    </div>
  `,
  styles: [
    `
      .container {
        background-color: #ededed;
        width: 400px;
        border-radius: 24px;
        position: relative;
        margin: 20px auto;
      }
      .progress {
        background-color: #10ADED;
        padding: 10px;
        text-align: center;
        font-weight: bold;
        border-radius: inherit;
        transition: width .5s ease-out;
      }
    `
  ]
})
export class ProgressBarComponent {}

Modifichiamo quindi i due file relativi al componente AppComponent di nostro interesse.

<div style="text-align:center">
  <h1>
    Progress: {{ progress }}%
  </h1>
  <simple-progress-bar></simple-progress-bar>
</div>

Nel file app.component.html troviamo il template di AppComponent. Al suo interno abbiamo inserito l’elemento relativo al componente definito in precedenza ProgressBarComponent. Ma ad AppComponent abbiamo anche assegnato una proprietà progress come possiamo vedere dal seguente frammento di codice.

import { Component } from '@angular/core';

@Component({
  selector: 'simple-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  progress = 20;
}

Con la configurazione corrente, se visualizziamo un’anteprima della nostra applicazione, ci sarà mostrato un risultato simile a quello riportato nell’immagine sottostante.

esempio applicazione composizione componenti

(Per completezza riportiamo anche il codice presente nel file style.css anche se non ha grande rilevanza per l’argomento che stiamo trattando in questa lezione.)

// file style.css
* {
  box-sizing: border-box;
  font-family: 'Courier New', Courier, monospace;
  color: rgb(1, 0, 73);
}

Nell’applicazione corrente, i due componenti AppComponent e ProgressBarComponent non scambiano nessuna informazione fra di loro, sarebbe conveniente e utile se potessimo usare il valore della proprietà progress di AppComponent per controllare la lunghezza della barra di progresso. Vediamo allora come modificare il codice della nostra applicazione in modo tale che AppComponent possa passare dei dati al componente figlio ProgressBarComponent. Il risultato che vogliamo ottenere consiste nell’utilizzare la proprietà progress di AppComponent per controllare il valore della proprietà CSS width dell’elemento <div> (anche se non è il modo più efficiente), identificato dalla classe CSS progress, presente nel template del componente ProgressBarComponent.

Per prima cosa, al solo scopo dimostrativo, modifichiamo il codice del componente AppComponent in modo da simulare una variazione nel corso del tempo della proprietà progress.

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

@Component({
  selector: 'simple-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  progress = 0;

  ngOnInit(): void {
    this.updateProgress(6);
  }

  updateProgress(value: number): void {
    if (this.progress >= 100 || value === 0) {
      return;
    }
    const delta = 100 - this.progress;

    if (value > delta) {
      value = delta;
    }

    setTimeout(() => { 
      this.progress += value; 
      this.updateProgress(value); 
    }, 200);
  }
}

Trascuriamo per il momento il metodo ngOnInit() di cui parleremo in una delle prossime lezioni quando discuteremo del ciclo di vita di un componente. Per ora ci interessa solo sapere che viene invocato in fase di inizializzazione del componente. Attraverso il metodo updateProgress() modifichiamo il valore della proprietà progress. Ora dobbiamo solo capire come passare tale proprietà al componente figlio. Analizziamo quindi il file app.component.html

<div style="text-align:center">
  <h1>
    Progress: <span>{{ progress }}%</span>
  </h1>
  <simple-progress-bar [progress]="progress"></simple-progress-bar>
</div>

Per passare dei dati al componente figlio, utilizziamo il meccanismo del binding delle proprietà. Racchiudiamo fra parentesi quadre il nome della proprietà del componente figlio a cui vogliamo associare il valore della proprietà del componente genitore. In questo caso il nome dellle due proprietà coincide, ma non è richiesto che ciò accada.

Dobbiamo quindi modificare il codice del componente ProgressBarComponent come segue.

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

@Component({
  selector: 'simple-progress-bar',
  template: `
    <div class="container">
      <div class="progress" [style.width.%]="progress">{{ progress }}%</div>
    </div>
  `,
  styles: [
    `
      .container {
        background-color: #ededed;
        width: 400px;
        border-radius: 24px;
        position: relative;
        margin: 20px auto;
      }
      .progress {
        background-color: #10ADED;
        padding: 10px;
        text-align: center;
        font-weight: bold;
        border-radius: inherit;
        transition: width .5s ease-out;
      }
    `
  ]
})
export class ProgressBarComponent {
  @Input() progress: number;
}

Nel frammento di codice riportato sopra, l’unica novità rispetto a quanto abbiamo già visto finora è rappresentata dalla presenza del decoratore @Input(), importato da @angular/core, che precede la proprietà progress in fase di dichiarazione (il nome della proprietà deve coincidere con quello presente fra parentesi quadre sull’elemento <simple-progress-bar>). In questo modo ProgressBarComponent indica che può ricevere dei dati dall’esterno attraverso la proprietà progress. Solo le proprietà precedute dal decoratore @Input() possono essere usate dal componente genitore per passare delle informazioni via binding della proprietà. Se infatti cancellassimo il decoratore o cercassimo di passare delle informazioni a ProgressBarComponent attraverso un’altra proprietà, che non è preceduta dal decoratore @Input(), visualizzeremmo nella console del browser un messaggio di errore come il seguente.

messaggio errore decoratore @input mancante

Se ora apriamo la nostra applicazione nel browser con ng serve –open, visualizziamo una pagina come quella mostrata nel video sottostante.

Notificare il componente genitore attraverso event binding

È possibile passare delle informazioni dal componente figlio al genitore attraverso il meccanismo del binding degli eventi. La procedura da eseguire è piuttosto facile.

Nella classe che definisce il componente figlio dovremo:

  1. Creare una proprietà a cui applichiamo il decoratore @Output().
  2. Assegnare a tale proprietà un oggetto di tipo EventEmitter(). Si tratta di una classe generica, per cui dovremo specificare fra parentesi angolari il tipo del valore che passeremo come argomento al metodo emit(value?: T) il quale emetterà un evento contenente il valore opzionale passatogli.
  3. Invocare il metodo emit() nel momento in cui vogliamo emettere l’evento personalizzato e passare i dati al componente padre.

Per il componente genitore invece sarà necessario:

  1. specificare l’azione da eseguire al verificarsi di un evento utilizzando la sintassi del binding degli eventi all’interno del template.
  2. definire eventuali metodi che verranno invocati al verificarsi dell’evento emesso dal componente figlio.

Modifichiamo quindi l’esempio precedente per illustrare questo nuovo concetto e partiamo dal componente ProgressBarComponent di cui riportiamo la nuova versione di seguito.

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

@Component({
  selector: 'simple-progress-bar',
  template: `
    <div class="container">
      <div class="progress" [style.width.%]="progress">{{ progress }}%</div>
    </div>
    <button (click)="onClick($event)">+{{ increment }}</button>
  `,
  styles: [
    `
      .container {
        background-color: #ededed;
        width: 400px;
        border-radius: 24px;
        position: relative;
        margin: 20px auto;
      }
      .progress {
        background-color: #10ADED;
        padding: 10px;
        text-align: center;
        font-weight: bold;
        border-radius: inherit;
        transition: width .5s ease-out;
      }
      button {
        color: white;
        background-color: darkseagreen;
        border: none;
        border-radius: 2em;
        padding: 1em 2em;
        font-size: .8em;
        transform: scale(1);
        outline: none;
        cursor: pointer;
      }

      button:focus {
        background-color: rgb(108, 161, 108);
        transform: scale(1.05);
        box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.26);
      }

      button:active {
        background-color: rgb(100, 148, 100);
        transform: scale(.8);
        box-shadow: none;
      }
    `
  ]
})
export class ProgressBarComponent {
  @Input() progress: number;
  @Output() progressChange = new EventEmitter<number>();
  private increment = 15;

  onClick(event: MouseEvent) {
    const delta = 100 - this.progress;
    if (delta > this.increment) {
      this.progressChange.emit(this.increment);
    } else {
      this.progressChange.emit(delta);
    }
  }
}

Rispetto alla versione analizzata nell’esempio precedente, in questo caso abbiamo aggiunto un pulsante al template. Ogni volta che viene cliccato, viene emesso l’evento ‘click’ al verificarsi del quale viene invocato il metodo onClick() in cui emettiamo un nuovo evento attraverso l’istanza di Event Emitter progressChange. Si tratta di una nuova proprietà del componente ProgressBarComponent a cui abbiamo applicato il decoratore @Output() per indicare che si tratta di una proprietà che trasmetterà dei dati all’esterno del componente. (Abbiamo importato sia la classe EventEmitter che il decoratore @Output() da @angular/core) La proprietà progressChange è un’istanza di EventEmitter e possiede quindi il metodo emit() a cui passiamo il valore numerico che vogliamo includere nell’evento che verrà emesso.

Nel template del componente AppComponent intercettiamo il nuovo evento emesso (progressChange) al verificarsi del quale invochiamo il metodo updateProgress() che abbiamo modificato rispetto all’esempio precedente. A quest’ultimo passiamo come argomento il valore contenuto in $event, ovvero il valore numerico emesso da progressChange.emit(). $event è lo strumento messo a disposizione da Angular per ottenere il valore incluso all’atto dell’emissione di un evento. Infatti per tutti gli eventi emessi da un’istanza di EventEmitter, $event conterrà il valore passato al metodo emit(valore). Nel nostro caso si tratterà semplicemente di un numero. Al contrario, al metodo onClick() di ProgressBarComponent Angular aveva passato un oggetto $event istanza di MouseEvent.

<div style="text-align:center">
  <h1>
    Progress: <span>{{ progress }}%</span>
  </h1>
  <simple-progress-bar
    [progress]="progress"
    (progressChange)="updateProgress($event)">
  </simple-progress-bar>
</div>

Vediamo infine il codice contenuto nel file app.component.ts:

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

@Component({
  selector: 'simple-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  progress = 0;

  ngOnInit(): void {}

  updateProgress(value: number): void {
    this.progress += value;
  }
}

Il metodo updateProgress() viene invocato ogni volta che il componente figlio emette l’evento progressChange, ovvero ogni volta che viene cliccato il pulsante presente nel suo template. Al verificarsi di tale evento, AppComponent aggiorna la propria proprietà progress sommando al valore corrente il nuovo incremento. Dal momento che progress assume un nuovo valore, quest’ultimo viene automaticamente passato al componente figlio che aggiorna la barra di progresso in modo da rispecchiare le ultime modifiche.

Componenti e two way data binding

Consideriamo di nuovo per un momento il template del componente AppComponent e in particolare l’elemento <simple-progress-bar>.

<simple-progress-bar
  [progress]="progress"
  (progressChange)="updateProgress($event)">
</simple-progress-bar>

La sintassi usata nell’esempio ricorda molto quella estesa impiegata per la direttiva ngModel quando abbiamo parlato della tecnica del two-way data binding. Anche per i componenti da noi definiti Angular offre la possibilità di usare la sintassi del two-way data binding che ricordiamo combina l’uso delle parentesi quadre del binding delle proprietà e quello delle parentesi tonde del binding degli eventi.

Vediamo allora come modificare il nostro esempio in più fasi in modo da arrivare ad applicare la sintassi del two-way binding all’elemento <simple-progress-bar> come mostrato sotto.

<simple-progress-bar [(progress)]="progress">
</simple-progress-bar>

La sintassi riportata nel frammento di codice soprastante è solo un modo semplificato che permette di scrivere dei template in maniera più elegante e semplificata. Dietro le quinte Angular trasforma l’elemento <simple-progress-bar> come riportato sotto.

<simple-progress-bar 
  [progress]="progress" 
  (progressChange)="progress=$event">
</simple-progress-bar>

Al verificarsi dell’evento progressChange, emesso dal componente ProgressBarComponent, Angular assegna il valore numerico presente in $event alla proprietà progress del componente AppComponent. Possiamo quindi semplificare quest’ultimo e modificare il componente ProgressBarComponent opportunamente in modo da emettere il valore della sua proprietà progress dopo averla aggiornata.

Il nuovo template di AppComponent è dunque il seguente.

<div style="text-align:center">
  <h1>
    Progress: <span>{{ progress }}%</span>
  </h1>
  <simple-progress-bar [(progress)]="progress">
  </simple-progress-bar>
</div>

Mentre nel file app.component.ts rimuoviamo i metodi superflui.

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

@Component({
  selector: 'simple-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  progress = 0;

  ngOnInit(): void {
    setTimeout(() => {
      this.progress = 20;
    }, 500);
  }
}

Abbiamo usato il metodo setTimeout() per simulare l’aggiornamento della proprietà progress di AppComponent dopo 500ms solo a scopo dimostrativo.

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

@Component({
  selector: 'simple-progress-bar',
  template: `
    <div class="container">
      <div class="progress" [style.width.%]="progress">{{ progress }}%</div>
    </div>
    <button 
      [disabled]="this.progress >= 100" 
      (click)="onClick($event)">+{{ increment }}</button>
  `,
  styles: [
    `
      .container {
        background-color: #ededed;
        width: 400px;
        border-radius: 24px;
        position: relative;
        margin: 20px auto;
      }
      .progress {
        background-color: #10ADED;
        padding: 10px;
        text-align: center;
        font-weight: bold;
        border-radius: inherit;
        transition: width .5s ease-out;
      }
      button {
        color: white;
        background-color: darkseagreen;
        border: none;
        border-radius: 2em;
        padding: 1em 2em;
        font-size: .8em;
        transform: scale(1);
        outline: none;
        cursor: pointer;
      }

      button:focus {
        background-color: rgb(108, 161, 108);
        transform: scale(1.05);
        box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.26);
      }

      button:active {
        background-color: rgb(100, 148, 100);
        transform: scale(.8);
        box-shadow: none;
      }
      button:disabled {
        background-color: rgb(211, 211, 211);
        cursor: not-allowed;
        box-shadow: none;
      }
    `
  ]
})
export class ProgressBarComponent {
  @Input() progress: number;
  @Output() progressChange = new EventEmitter<number>();
  private increment = 15;
  private disabled = false;

  onClick(event) {
    const delta = 100 - this.progress;
    if (delta > this.increment) {
      this.progress += this.increment;
    } else {
      this.progress += delta;
    }
    this.progressChange.emit(this.progress);
  }
}

Abbiamo leggermente modificato il componente ProgressBarComponent in modo che ogni volta che viene premuto il pulsante, viene opportunamente aggiornata la proprietà progress e viene emesso un nuovo evento contenente il valore aggiornato. Il componente genitore AppComponent, al verificarsi di tale evento, aggiornerà di conseguenza la propria proprietà progress.

Scambio di informazioni fra componenti adiacenti

comunicazione fra componenti adiacenti

Per concludere questa lezione vediamo ora un possibile modo per mettere in comunicazione due componenti adiacenti. Per farlo utilizziamo un semplice esempio in cui creiamo due componenti CounterComponent e un componente genitore AppComponent. Ciascun componente CounterComponent presenta un contatore e un pulsante che, se cliccato, incrementa il contatore del componente adiacente. Per permettere la comunicazione fra i due componenti adiacenti sfruttiamo il componente genitore comune. Quando uno dei due vuole passare il nuovo incremento all’altro, emetterà un evento al verificarsi del quale il componente genitore trasferirà le informazioni passate con l’evento all’altro componente.

tree src/app  -F
src/app
├── app.component.ts
├── app.module.ts
└── counter/
    └── counter.component.ts

1 directory, 3 files

Nel nostro esempio ciascun componente CounterComponent avrà una proprietà counter che viene incrementata quando il componente adiacente emette un evento in seguito al click del pulsante in esso presente.

import { Component } from '@angular/core';

@Component({
  selector: 'simple-root',
  template: `
    <simple-counter 
      title="Componente 1" 
      [counter]="counter1" 
      (update)="counter2 = counter2 + $event"></simple-counter>
    <simple-counter 
      title="Componente 2" 
      [counter]="counter2" 
      (update)="counter1 = counter1 + $event"></simple-counter>
  `
})
export class AppComponent {
  counter1 = 0;
  counter2 = 1;
}

Il componente AppComponent presenta due proprietà che sono associate, attraverso property binding, alle proprietà counter dei due componenti CounterComponent. Quando riceve l’evento update da uno dei due componenti figli, aggiorna la proprietà counter dell’altro incrementando il suo valore di uno. Per ogni elemento <simple-counter> impostiamo una proprietà title. Notate che non abbiamo racchiuso tale proprietà fra parentesi quadre. Questa tecnica prende il nome di One-time string initialization e permette di inizializzare una proprietà di tipo stringa del componente CounterComponent indicando che tale proprietà mantiene un valore fisso che non intendiamo mai più cambiare in futuro. Possiamo usare questa tecnica ogni volta che la proprietà del componente figlio che vogliamo settare è di tipo stringa, il suo valore è costante e non vogliamo aggiornarlo successivamente.

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

@Component({
  selector: 'simple-counter',
  template: `
    <h2>{{ title }}</h2>
    <div>
      {{ counter }}
      <button (click)="onClick($event)">Emetti evento</button>
    </div>
  `,
  styles: []
})
export class CounterComponent {
  @Input() title: string;
  @Input() counter: number;
  @Output() update = new EventEmitter<number>();
  onClick(event: MouseEvent) {
    this.update.emit(1);
  }
}

Il componente CounterComponent presenta al suo interno tre proprietà due delle quali vengono impostate dall’esterno. La terza invece consente di emettere un evento al click del pulsante presente nel suo template.

Riepilogo

In questa lezione abbiamo visto come annidare dei componenti da noi definiti. Abbiamo dunque illustrato alcuni metodi per permettere la comunicazione fra i diversi componenti. Nella prossima lezione vedremo come creare dei componenti configurabili grazie a <ng-content>.

Pubblicitร