Componenti angolari 101: una panoramica

Pubblicato: 2022-03-11

I componenti sono disponibili in Angular sin dall'inizio; tuttavia, molte persone si trovano ancora a utilizzare i componenti in modo errato. Nel mio lavoro, ho visto persone che non li usavano affatto, creare componenti invece di direttive di attributi e altro ancora. Questi sono problemi che gli sviluppatori Angular junior e senior tendono a fare, me compreso. E quindi, questo post è per persone come me quando stavo imparando Angular, poiché né la documentazione ufficiale né la documentazione non ufficiale sui componenti spiegano con casi d'uso come e quando utilizzare i componenti.

In questo articolo, identificherò i modi corretti e non corretti per utilizzare i componenti Angular, con esempi. Questo post dovrebbe darti un'idea chiara su:

  • La definizione delle componenti angolari;
  • Quando dovresti creare componenti angolari separati; e
  • Quando non dovresti creare componenti angolari separati.

Immagine di copertina per componenti angolari

Prima di poter passare all'uso corretto dei componenti angolari, voglio toccare brevemente l'argomento dei componenti in generale. In generale, ogni singola applicazione Angular ha almeno un componente per impostazione predefinita: il root component . Da lì, sta a noi come progettare la nostra applicazione. Di solito, creeresti un componente per pagine separate, quindi ogni pagina conterrebbe un elenco di componenti separati. Come regola generale, un componente deve soddisfare i seguenti criteri:

  • Deve avere una classe definita, che contenga dati e logica; e
  • Deve essere associato a un modello HTML che visualizza le informazioni per l'utente finale.

Immaginiamo uno scenario in cui abbiamo un'applicazione che ha due pagine: Prossime attività e Attività completate . Nella pagina delle attività imminenti , possiamo visualizzare le attività imminenti, contrassegnarle come "completate" e infine aggiungere nuove attività. Allo stesso modo, nella pagina Attività completate , possiamo visualizzare le attività completate e contrassegnarle come "annullate". Infine, abbiamo i link di navigazione, che ci consentono di navigare tra le pagine. Detto questo, possiamo dividere la seguente pagina in tre sezioni: componente radice, pagine, componenti riutilizzabili.

Diagramma delle attività imminenti e delle attività completate

Esempio di wireframe di applicazione con sezioni di componenti separate colorate

Sulla base dello screenshot sopra, possiamo vedere chiaramente che la struttura dell'applicazione sarebbe simile a questa:

 ── myTasksApplication ├── components │ ├── header-menu.component.ts │ ├── header-menu.component.html │ ├── task-list.component.ts │ └── task-list.component.html ├── pages │ ├── upcoming-tasks.component.ts │ ├── upcoming-tasks.component.html │ ├── completed-tasks.component.ts │ └── completed-tasks.component.html ├── app.component.ts └── app.component.html

Quindi colleghiamo i file dei componenti con gli elementi effettivi sul wireframe sopra:

  • header-menu.component e task-list.component sono componenti riutilizzabili, che vengono visualizzati con un bordo verde nello screenshot wireframe;
  • upcoming-tasks.component e completed-tasks.component sono pagine, che vengono visualizzate con un bordo giallo nello screenshot wireframe sopra; e
  • Infine, app.component è il componente principale, che viene visualizzato con un bordo rosso nello screenshot del wireframe.

Quindi, detto questo, possiamo specificare una logica e un design separati per ogni singolo componente. Sulla base dei wireframe sopra, abbiamo due pagine che riutilizzano un componente task-list.component . Sorgerebbe la domanda: come specifichiamo quale tipo di dati dovremmo mostrare su una pagina specifica? Fortunatamente, non dobbiamo preoccuparci di questo, perché quando crei un componente, puoi anche specificare variabili di Input e Output .

Variabili di input

Le variabili di input vengono utilizzate all'interno dei componenti per trasferire alcuni dati dal componente padre. Nell'esempio sopra, potremmo avere due parametri di input per task-list.component : tasks e listType . Di conseguenza, le tasks sarebbero un elenco di stringhe che visualizzerebbero ciascuna stringa su una riga separata, mentre listType sarebbe imminente o completata , il che indicherebbe se la casella di controllo è selezionata. Di seguito, puoi trovare un piccolo frammento di codice di come potrebbe apparire il componente effettivo.

 // task-list.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. constructor() { } }

Variabili di uscita

Analogamente alle variabili di input, anche le variabili di output possono essere utilizzate per passare alcune informazioni tra i componenti, ma questa volta al componente padre. Ad esempio, per task-list.component , potremmo avere la variabile di output itemChecked . Informerebbe il componente principale se un articolo è stato selezionato o deselezionato. Le variabili di output devono essere emettitori di eventi. Di seguito, puoi trovare un piccolo frammento di codice di come potrebbe apparire il componente con una variabile di output.

 // task-list.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. @Output() itemChecked: EventEmitter<boolean> = new EventEmitter(); @Output() tasksChange: EventEmitter<string[]> = new EventEmitter(); constructor() { } /** * Is called when an item from the list is checked. * @param selected---Value which indicates if the item is selected or deselected. */ onItemCheck(selected: boolean) { this.itemChecked.emit(selected); } /** * Is called when task list is changed. * @param changedTasks---Changed task list value, which should be sent to the parent component. */ onTasksChanged(changedTasks: string[]) { this.taskChange.emit(changedTasks); } }
Possibile contenuto task-list.component.ts dopo l'aggiunta di variabili di output

Utilizzo dei componenti figlio e associazione di variabili

Diamo un'occhiata a come utilizzare questo componente all'interno del componente padre e come eseguire diversi tipi di associazioni di variabili. In Angular, ci sono due modi per associare le variabili di input: associazione unidirezionale, che significa che la proprietà deve essere racchiusa tra parentesi quadre [] e associazione bidirezionale, che significa che la proprietà deve essere racchiusa tra parentesi quadre e tonde [()] . Dai un'occhiata all'esempio seguente e scopri i diversi modi in cui i dati possono essere passati tra i componenti.

 <h1>Upcoming Tasks</h1> <app-task-list [(tasks)]="upcomingTasks" [listType]="'upcoming'" (itemChecked)="onItemChecked($event)"></app-task-list>
Possibili contenuti upcoming-tasks.component.html

Esaminiamo ogni parametro:

  • Il parametro delle tasks viene passato utilizzando l'associazione bidirezionale. Ciò significa che, nel caso in cui il parametro delle attività venga modificato all'interno del componente figlio, il componente padre rifletterà tali modifiche sulla variabile upcomingTasks . Per consentire il data binding bidirezionale, è necessario creare un parametro di output, che segue il modello "[inputParameterName]Change", in questo caso tasksChange .
  • Il parametro listType viene passato utilizzando l'associazione unidirezionale. Ciò significa che può essere modificato all'interno del componente figlio, ma non si rifletterà nel componente padre. Tieni presente che avrei potuto assegnare il valore 'upcoming' a un parametro all'interno del componente e passarlo invece, non farebbe differenza.
  • Infine, il parametro itemChecked è una funzione listener e verrà chiamato ogni volta che onItemCheck viene eseguito su task-list.component . Se un elemento è selezionato, $event conterrà il valore true , ma, se è deselezionato, conterrà il valore false .

Come puoi vedere, in generale, Angular offre un ottimo modo per passare e condividere informazioni tra più componenti, quindi non dovresti aver paura di usarli. Assicurati solo di usarli con saggezza e di non abusarne.

Quando creare un componente angolare separato

Come accennato in precedenza, non dovresti aver paura di usare i componenti Angular, ma dovresti assolutamente usarli con saggezza.

Diagramma delle componenti angolari in azione

Fai un uso saggio dei componenti angolari

Quindi, quando dovresti creare componenti angolari?

  • Dovresti sempre creare un componente separato se il componente può essere riutilizzato in più posti, come con il nostro task-list.component . Li chiamiamo componenti riutilizzabili .
  • Dovresti considerare la creazione di un componente separato se il componente renderà il componente principale più leggibile e consentirà loro di aggiungere ulteriore copertura del test. Possiamo chiamarli componenti dell'organizzazione del codice .
  • Dovresti sempre creare un componente separato se hai una parte di una pagina che non ha bisogno di essere aggiornata spesso e vuoi aumentare le prestazioni. Questo è correlato alla strategia di rilevamento delle modifiche. Possiamo chiamarli componenti di ottimizzazione .

Queste tre regole mi aiutano a identificare se devo creare un nuovo componente e mi danno automaticamente una visione chiara del ruolo del componente. Idealmente, quando crei un componente, dovresti già sapere quale sarà il suo ruolo all'interno dell'applicazione.

Poiché abbiamo già esaminato l'utilizzo dei componenti riutilizzabili , diamo un'occhiata all'utilizzo dei componenti dell'organizzazione del codice . Immaginiamo di avere un modulo di registrazione e, in fondo al modulo, abbiamo una casella con Termini e Condizioni . Di solito, il legalese tende ad essere molto grande, occupando molto spazio, in questo caso in un modello HTML. Quindi, diamo un'occhiata a questo esempio e poi vediamo come potremmo potenzialmente cambiarlo.

All'inizio, abbiamo un componente, registration.component , che contiene tutto, incluso il modulo di registrazione, nonché i termini e le condizioni stessi.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div> <button (click)="onRegistrate()">Registrate</button>
Stato iniziale prima di separare </code>registration.component</code> in più componenti

Il modello ora sembra piccolo, ma immagina se dovessimo sostituire "Testo con termini e condizioni molto lunghi" con un pezzo di testo che contiene più di 1000 parole, renderebbe la modifica del file difficile e scomoda. Abbiamo una soluzione rapida per questo: potremmo inventare un nuovo componente terms-and-conditions.component , che conterrebbe tutto ciò che riguarda termini e condizioni. Quindi diamo un'occhiata al file HTML per i terms-and-conditions.component .

 <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div>
Modello HTML appena creato </code>terms-and-conditions.component</code>

E ora possiamo regolare il registration.component e utilizzare il terms-and-conditions.component al suo interno.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()">Registrate</button>
Modello </code>registration.component.ts</code> aggiornato con il componente di organizzazione del codice

Congratulazioni! Abbiamo appena ridotto la dimensione di registration.component di centinaia di righe e reso più facile la lettura del codice. Nell'esempio precedente, abbiamo apportato modifiche al modello del componente, ma lo stesso principio potrebbe essere applicato alla logica del componente.

Infine, per quanto riguarda i componenti di ottimizzazione , ti consiglio vivamente di leggere questo post, poiché ti fornirà tutte le informazioni necessarie per comprendere il rilevamento delle modifiche e a quali casi specifici puoi applicarlo. Non lo utilizzerai spesso, ma potrebbero esserci alcuni casi e se puoi saltare i controlli regolari su più componenti quando non è necessario, è un vantaggio per tutte le prestazioni.

Detto questo, non dovremmo sempre creare componenti separati, quindi diamo un'occhiata a quando dovresti evitare di creare un componente separato.

Quando evitare la creazione di un componente angolare separato

Ci sono tre punti principali in base ai quali suggerisco di creare un componente angolare separato, ma ci sono casi in cui eviterei di creare anche un componente separato.

Illustrazione di troppi componenti

Troppi componenti ti rallenteranno.

Ancora una volta, diamo un'occhiata ai punti elenco che ti permetteranno di capire facilmente quando non dovresti creare un componente separato.

  • Non dovresti creare componenti per manipolazioni DOM. Per quelli, dovresti usare le direttive degli attributi.
  • Non dovresti creare componenti se renderà il tuo codice più caotico. Questo è l'opposto dei componenti dell'organizzazione del codice .

Ora, diamo un'occhiata più da vicino e controlliamo entrambi i casi negli esempi. Immaginiamo di voler avere un pulsante che registri il messaggio quando viene cliccato. Ciò potrebbe essere errato e potrebbe essere creato un componente separato per questa funzionalità specifica, che conterrebbe un pulsante e azioni specifici. Per prima cosa controlliamo l'approccio errato:

 // log-button.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-log-button', templateUrl: 'log-button.component.html' }) export class LogButtonComponent { @Input() name: string; // Name of the button. @Output() buttonClicked: EventEmitter<boolean> = new EventEmitter(); constructor() { } /** * Is called when button is clicked. * @param clicked - Value which indicates if the button was clicked. */ onButtonClick(clicked: boolean) { console.log('I just clicked a button on this website'); this.buttonClicked.emit(clicked); } }
Logica di componente errato log-button.component.ts

E di conseguenza, questo componente è accompagnato dalla seguente vista html.

 <button (click)="onButtonClick(true)">{{ name }}</button>
Modello di componente errato log-button.component.html

Come puoi vedere, l'esempio sopra funzionerebbe e potresti utilizzare il componente sopra in tutte le tue viste. Farebbe quello che vuoi, tecnicamente. Ma la soluzione corretta qui sarebbe usare le direttive. Ciò ti consentirebbe non solo di ridurre la quantità di codice che devi scrivere, ma anche di aggiungere la possibilità di applicare questa funzionalità a qualsiasi elemento desideri, non solo ai pulsanti.

 import { Directive } from '@angular/core'; @Directive({ selector: "[logButton]", hostListeners: { 'click': 'onButtonClick()', }, }) class LogButton { constructor() {} /** * Fired when element is clicked. */ onButtonClick() { console.log('I just clicked a button on this website'); } }
logButton , che può essere assegnata a qualsiasi elemento

Ora, dopo aver creato la nostra direttiva, possiamo semplicemente usarla nella nostra applicazione e assegnarla a qualsiasi elemento desideriamo. Ad esempio, riutilizziamo il nostro registration.component .

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()" logButton>Registrate</button>
logButton direttiva utilizzata sul pulsante dei moduli di registrazione

Ora, possiamo dare un'occhiata al secondo caso, in cui non dobbiamo creare componenti separati, ed è l'opposto dei componenti di ottimizzazione del codice . Se il componente appena creato rende il codice più complicato e più grande, non è necessario crearlo. Prendiamo come esempio il nostro registration.component . Uno di questi casi sarebbe la creazione di un componente separato per l'etichetta e il campo di input con una tonnellata di parametri di input. Diamo un'occhiata a questa cattiva pratica.

 // form-input-with-label.component.ts import { Component, Input} from '@angular/core'; @Component({ selector: 'app-form-input-with-label', templateUrl: 'form-input-with-label.component.html' }) export class FormInputWithLabelComponent { @Input() name: string; // Name of the field @Input() id: string; // Id of the field @Input() label: string; // Label of the field @Input() type: 'text' | 'password'; // Type of the field @Input() model: any; // Model of the field constructor() { } }
Logica del form-input-with-label.component

E questa potrebbe essere la vista per questo componente.

 <label for="{{ id }}">{{ label }}</label><br /> <input type="{{ type }}" name="{{ name }}" [(ngModel)]="model" /><br />
Visualizzazione del form-input-with-label.component

Certo, la quantità di codice sarebbe ridotta in registration.component , ma rende la logica generale del codice più facile da capire e leggibile? Penso che possiamo vedere chiaramente che rende il codice inutilmente più complesso di quanto non fosse prima.

Passi successivi: componenti angolari 102?

Per riassumere: non abbiate paura di usare i componenti; assicurati solo di avere una visione chiara di ciò che vuoi ottenere. Gli scenari che ho elencato sopra sono i più comuni e li considero i più importanti e comuni; tuttavia, il tuo scenario potrebbe essere unico e spetta a te prendere una decisione informata. Spero che tu abbia imparato abbastanza per prendere una buona decisione.

Se desideri saperne di più sulle strategie di rilevamento delle modifiche di Angular e sulla strategia OnPush, ti consiglio di leggere Rilevamento delle modifiche angolari e la strategia OnPush. È strettamente correlato ai componenti e, come ho già detto nel post, può migliorare notevolmente le prestazioni dell'applicazione.

Poiché i componenti sono solo una parte delle direttive fornite da Angular, sarebbe bello conoscere anche le direttive degli attributi e le direttive strutturali . La comprensione di tutte le direttive molto probabilmente renderà più facile per il programmatore scrivere un codice migliore.