back to top

Come creare una direttiva personalizzata in Angular

In questa lezione illustreremo attraverso degli esempi in che modo creare delle direttive personalizzate. Anche in questo caso, sebbene sia possibile procedere manualmente alla creazione e modifica dei singoli file, ci affideremo ancora una volta ad Angular CLI.

Come abbiamo già affermato nelle precedendi lezioni, in Angular esistono tre tipi di direttive:

  • i Componenti che sono delle direttive particolari dotate di un template;
  • le cosiddette Attribute Directives che modificano l’aspetto o il comportamento dell’elemento a cui sono applicate;
  • le Direttive Strutturali che vanno a modificare la struttura del DOM aggiungendo o rimuovendo degli elementi.

Creare un’Attribute Directive personalizzata

Partiamo quindi con un primo esempio in cui creiamo un’Attribute Directive. Iniziamo generando i file necessari all’interno di un progetto precedentemente creato con il comando ng new. Anche in questa occasione utilizziamo Angular CLI che ci semplifica la vita e lanciamo il seguente comando.

ng g d darkMode --flat --no-spec

Il comando riportato sopra è equivalente a ng generate directive. Nel nostro caso abbiamo anche espresso la volontà di non creare il file per i test d’unità (–no-spec) e di aggiungere il file dark-mode.directive.ts nella cartella app allo stesso livello del file app.component.ts, senza creare una directory a parte (–flat). Il nome della nostra direttiva sarà DarkModeDirective.

import { Directive, HostBinding, HostListener } from '@angular/core'; // 1

@Directive({
  selector: '[simpleDarkMode]' // 2
})
export class DarkModeDirective {

  private activateDarkMode = false;

  @HostBinding('style.background-color')
  private backgroudColor: string;

  @HostBinding('style.color')
  private textColor: string;

  @HostListener('mouseover')
  onMouseOver() {
    this.backgroudColor = '#26252C';
    this.textColor = '#F6F5FA';
  }

  @HostListener('mouseleave')
  onMouseLeave() {
    if (!this.activateDarkMode) {
      this.backgroudColor = '';
      this.textColor = '';
    }
  }

  @HostListener('dblclick')
  onDoubleClick() {
    this.activateDarkMode = !this.activateDarkMode;
    this.onMouseOver();
  }
}

Analizziamo quindi il frammento di codice appena creato. Per prima cosa importiamo i tre decoratori Directive, HostBinding e HostListener (riga 1) da @angular/core. Selezioniamo poi il selettore per applicare la direttiva ad un elemento, nel caso specifico si tratterà dell’attributo simpleDarkMode.

<!-- Applica la direttiva DarkModeDirective al paragrafo -->
<p simpleDarkMode>Sed posuere consectetur est at lobortis.</p>

Nel definire la direttiva facciamo uso dei due decoratori @HostBinding e @HostListener. Il primo consente di impostare delle proprietà sull’elemento che ospita la direttiva (Host element) mentre il secondo permette di registrare delle funzioni che verranno invocate al verificarsi dell’evento specificato sull’elemento host. Grazie al decoratore @HostBinding il valore della proprietà backgroudColor della nostra direttiva sarà associato a quello di style.background-color dell’elemento a cui viene applicata. Ogni volta che viene modificata backgroudColor, verrà cambiato il colore di sfondo dell’elemento. Lo stesso principio vale per la proprietà textColor associata in questo caso a style.color che permette di controllare il colore del testo dell’elemento che ospita la direttiva.

Supponiamo quindi che la direttiva venga applicata a un paragrafo come mostrato nel frammento di codice riportato sopra. Ogni volta che per quel paragrafo si verifica l’evento mouseover, ovvero quando l’utente posiziona il cursore del mouse sull’elemento, cambiamo il colore dello sfondo e del testo del paragrafo modificando le proprietà backgroudColor e textColor della direttiva. Quando invece si verifica l’evento mouseleave, ovvero non appena il cursore del mouse non si trova più sull’elemento, se la proprietà activateDarkMode è pari a false, riportiamo l’aspetto dell’elemento alle condizioni precedenti all’evento mouseover. Facendo doppio click sull’elemento attiviamo "la modalità Dark Mode" fin quando l’utente non cliccherà nuovamente per due volte consecutive sull’elemento.

Dopo aver capito come funziona la direttiva appena descritta, possiamo provare a modificarla per permettere di configurare sia il colore dello sfondo che del testo attraverso delle proprietà di input.

import { 
  Directive, 
  ElementRef, 
  HostListener, 
  Input, 
  Renderer2 
} from '@angular/core';

@Directive({
  selector: '[simpleDarkMode]'
})
export class DarkModeDirective {

  public activateDarkMode = false;

  constructor(private renderer: Renderer2, private el: ElementRef) { }

  @Input()
  private backgroundColor: string;

  @Input()
  private textColor: string;

  @HostListener('mouseover')
  onMouseOver() {
    const backgroundColor = this.backgroundColor || '#26252C';
    const textColor = this.textColor || '#F6F5FA';

    this.renderer.setStyle(
      this.el.nativeElement, 
      'background-color', 
      backgroundColor
    );
    this.renderer.setStyle(
      this.el.nativeElement, 
      'color', 
      textColor
    );
  }

  @HostListener('mouseleave')
  onMouseLeave() {
    if (!this.activateDarkMode) {
      this.renderer.removeStyle(this.el.nativeElement, 'background-color');
      this.renderer.removeStyle(this.el.nativeElement, 'color');
    }
  }

  @HostListener('dblclick')
  onDoubleClick() {
    this.activateDarkMode = !this.activateDarkMode;
    this.onMouseOver();
  }
}

Abbiamo aggiornato la direttiva apportando una serie di modifiche. Per prima cosa importiamo oltre ai decoratori @Directive(), @HostListener() e @Input() le due classi ElementRef e Renderer2 che rappresentano rispettivamente un wrapper che incapsula le funzionalità di un elemento nativo e una classe fornita da Angular per manipolare gli elementi dell’applicazione senza dover agire in maniera diretta sul DOM.

Per il costruttore utilizziamo il sistema di Dependency Injection di Angular grazie al quale basterà indicare, attraverso dei parametri, di quali servizi o componenti esterni necessita il componente corrente. Angular provvederà poi a passarli come argomento in fase di costruzione dell’istanza del componente. Parleremo del meccanismo di Dependency Injection in una delle prossime lezioni, per ora ci basta sapere che Angular è in grado di capire che il componente che DarkModeDirective ha bisogno di due istanze delle due classi ElementRef e Renderer2 e le passa al costruttore in fase di creazione del componente.

In più aggiungendo la keyword private prima dei parametri del costruttore, abbiamo utilizzato la sintassi semplificata di TypeScript che permette di creare e assegnare un valore ad una proprietà di istanza di classe direttamente nel costruttore. In alternativa potremmo riscrivere la nostra classe in modo più esplicito e chiaro come mostrato sotto.

// ...
class DarkModeDirective {

  // ...

  private renderer: Renderer2;
  private el: ElementRef;

  constructor(renderer: Renderer2, el: ElementRef) { 
    this.renderer = renderer;
    this.el = el;
  }

  // ...
}

Continuando a parlare della nostra direttiva, abbiamo aggiunto due proprietà di input di tipo stringa e modificato leggermente i metodi che vengono invocati in risposta ad un evento che si verifica sull’elemento su cui è applicata la direttiva.

Notiamo che abbiamo utilizzato alcuni metodi di Renderer2 come setStyle() e removeStyle() grazie ai quali possiamo modificare con facilità lo stile dell’elemento host. Il metodo setStyle() imposta il colore dello sfondo e quello del testo utilizzando come valore quello delle proprietà di input e ripiegando sui valori di default se quest’ultime non sono state settate.

Possiamo quindi applicare la direttiva appena definita come mostrato nel frammento di codice sottostante in cui abbiamo anche specificato il valore delle due proprietà di input backgroundColor e textColor della direttiva.

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

import { DarkModeDirective } from './dark-mode.directive';

@Component({
  selector: 'simple-root',
  template: `
    <span>
      (doppio click per attivare/disattivare la modalità notturna)
    </span>
    <p simpleDarkMode backgroundColor="#141413" textColor="#FFFFFF">
      Nullam quis risus eget urna mollis ornare vel eu leo. 
      Maecenas sed diam eget risus varius
      blandit sit amet non magna. 
      Nullam quis risus eget urna mollis ornare vel eu leo.
      Donec sed odio dui. 
      Vestibulum id ligula porta felis euismod semper.
    </p>
  `
})
export class AppComponent { }

A questo punto possiamo però migliorare ulteriormente la direttiva appena creata e fare in modo di non dover specificare le proprietà backgroundColor e textColor separatamente, ma utilizzare direttamente l’attributo simpleDarkMode sia per applicare la direttiva che per settare il colore di sfondo e del testo attraverso il meccanismo del binding delle proprietà.

All’interno della direttiva, sostituiamo allora le due proprietà di input con una sola.

// @Input()
// private backgroundColor: string;

// @Input()
// private textColor: string;

@Input('simpleDarkMode')
private colorConfig: { backgroundColor: string, textColor: string };

Utilizziamo una sola proprietà di input e, passando un solo argomento di tipo stringa al decoratore @Input, indichiamo che all’interno della definizione della direttiva faremo riferimento a questa proprietà con il nome colorConfig, ma all’esterno sarà possibile associarle dei valori attraverso il binding delle proprietà e utilizzando il nome simpleDarkMode. Notiamo che dovrà essere passato un oggetto il quale a sua volta deve avere due proprietà di tipo stringa backgroundColor e textColor.

import { 
  Directive, 
  ElementRef, 
  HostListener, 
  Input, 
  Renderer2 
} from '@angular/core';

@Directive({
  selector: '[simpleDarkMode]'
})
export class DarkModeDirective {

  public activateDarkMode = false;

  constructor(private renderer: Renderer2, private el: ElementRef) { }

  @Input('simpleDarkMode')
  private colorConfig: { backgroundColor: string, textColor: string };

  @HostListener('mouseover')
  onMouseOver() {
    const backgroundColor = this.colorConfig.backgroundColor || '#26252C';
    const textColor = this.colorConfig.textColor || '#F6F5FA';

    this.renderer.setStyle(
      this.el.nativeElement, 
      'background-color', 
      backgroundColor
    );
    this.renderer.setStyle(
      this.el.nativeElement, 
      'color', 
      textColor
    );
  }

  @HostListener('mouseleave')
  onMouseLeave() {
    if (!this.activateDarkMode) {
      this.renderer.removeStyle(this.el.nativeElement, 'background-color');
      this.renderer.removeStyle(this.el.nativeElement, 'color');
    }
  }

  @HostListener('dblclick')
  onDoubleClick() {
    this.activateDarkMode = !this.activateDarkMode;
    this.onMouseOver();
  }
}

Possiamo quindi utilizzare la direttiva così modificata nel seguente modo.

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

import { DarkModeDirective } from './dark-mode.directive';

@Component({
  selector: 'simple-root',
  template: `
    <span>
      (doppio click per attivare/disattivare la modalità notturna)
    </span>
    <p [simpleDarkMode]="darkModeConfig" >
      Nullam quis risus eget urna mollis ornare vel eu leo. 
      Maecenas sed diam eget risus varius
      blandit sit amet non magna. 
      Nullam quis risus eget urna mollis ornare vel eu leo.
      Donec sed odio dui. 
      Vestibulum id ligula porta felis euismod semper.
    </p>
  `
})
export class AppComponent {
  darkModeConfig = {backgroundColor: '#123456', textColor: '#FFFFFF' };
}

Ora vogliamo modificare ulteriormente la direttiva fin qui creata e aggiungere una proprietà di output di tipo EventEmitter attraverso il decoratore @Output. In questo modo ogni volta che l’utente clicca due volte consecutive sull’elemento che ospita la direttiva vogliamo emettere un evento personalizzato che notifica l’attivazione o disattivazione della modalità notturna.

import {
  Directive,
  ElementRef,
  HostListener,
  Input,
  Output,
  Renderer2,
  EventEmitter
} from '@angular/core';

@Directive({
  selector: '[simpleDarkMode]'
})
export class DarkModeDirective {

  public activateDarkMode = false;

  constructor(private renderer: Renderer2, private el: ElementRef) { }

  @Input('simpleDarkMode')
  private colorConfig: { backgroundColor: string, textColor: string };

  @Output()
  private darkModeChange: EventEmitter<{darkMode: boolean}> = 
    new EventEmitter<{darkMode: boolean}>();

  @HostListener('mouseover')
  onMouseOver() {
    const backgroundColor = this.colorConfig.backgroundColor || '#26252C';
    const textColor = this.colorConfig.textColor || '#F6F5FA';

    this.renderer.setStyle(
      this.el.nativeElement, 
      'background-color', 
      backgroundColor
    );
    this.renderer.setStyle(
      this.el.nativeElement, 
      'color', 
      textColor
    );
  }

  @HostListener('mouseleave')
  onMouseLeave() {
    if (!this.activateDarkMode) {
      this.renderer.removeStyle(this.el.nativeElement, 'background-color');
      this.renderer.removeStyle(this.el.nativeElement, 'color');
    }
  }

  @HostListener('dblclick')
  onDoubleClick() {
    this.activateDarkMode = !this.activateDarkMode;
    // emette un evento per notificare il cambio di stato
    this.darkModeChange.emit({darkMode: this.activateDarkMode});
    this.onMouseOver();
  }
}

Abbiamo aggiunto una proprietà di output darkModeChange che è di tipo EventEmitter e emette un oggetto del tipo {darkMode: boolean} ogni volta che si verifica l’evento dblclick.

Aggiorniamo allora anche il componente AppComponent in modo da trarre vantaggio da questa nuova modifica.

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

import { DarkModeDirective } from './dark-mode.directive';

@Component({
  selector: 'simple-root',
  template: `
    <span>
      (
        doppio click per
        {{ isDarkModeActive ? 'disattivare' : 'attivare' }}
        la modalità notturna
      )
    </span>
    <p 
      [simpleDarkMode]="darkModeConfig" 
      (darkModeChange)="updateStatus($event)" >
      Nullam quis risus eget urna mollis ornare vel eu leo.
      Maecenas sed diam eget risus varius
      blandit sit amet non magna.
      Nullam quis risus eget urna mollis ornare vel eu leo.
      Donec sed odio dui.
      Vestibulum id ligula porta felis euismod semper.
    </p>
  `
})
export class AppComponent implements AfterViewInit {
  private darkModeConfig = {
    backgroundColor: 'midnightblue', 
    textColor: '#FFFFFF' 
  };
  private isDarkModeActive: boolean;

  @ViewChild(DarkModeDirective) darkModeDirective: DarkModeDirective;

  ngAfterViewInit() {
    this.isDarkModeActive = this.darkModeDirective.activateDarkMode;
  }

  private updateStatus(event: {darkMode: boolean}) {
    this.isDarkModeActive = event.darkMode;
  }
}

Accediamo alla direttiva DarkModeDirective attraverso il decoratore @ViewChild(), recuperiamo il valore iniziale della proprietà darkModeDirective.activateDarkMode all’interno del Lifecycle hook ngAfterViewInit(), perché a questo punto la view del componente è stata già inizializzata, e lo assegniamo alla proprietà isDarkModeActive con la quale controlliamo il testo da mostrare all’interno dell’elemento <span>. Intercettiamo inoltre l’evento (darkModeChange) in risposta al quale aggiorniamo la proprietà isDarkModeActive.

Creare una direttiva strutturale

Abbiamo visto come creare un’Attribute Directive, illustriamo ora, attraverso un altro esempio, come creare una direttiva strutturale.

In questo caso realizziamo una direttiva che chiamiamo RandomDirective la quale riceve in ingresso un valore numerico. Se quest’ultimo è maggiore di 0.5, l’elemento a cui viene applicata la direttiva viene inserito nel DOM, in caso contrario viene rimosso.

Applicheremo dunque la direttiva nel seguente modo.

<p *simpleRandom="randomNumber">Aenean eu leo quam.</p>

Ricordiamo, in base a quanto già affermato in una delle passate lezioni sulle direttive, che per le direttive strutturali è importante precedere il nome del selettore della direttiva dal carattere ‘*’. Dietro le quinte Angular provvederà a racchiudere l’elemento <p> fra una coppia di tag <ng-template></ng-template> e trasformerà l’attributo simpleRandom in modo da applicare la tecnica del binding delle proprietà.

<ng-template [simpleRandom]="randomNumber">
  <p>Aenean eu leo quam.</p>
</ng-template>

Vediamo allora come creare e usare la direttiva strutturale RandomDirective partendo dal componente AppComponent in cui andiamo a utilizzarla nel seguente modo.

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

@Component({
  selector: 'simple-root',
  template: `
    <div class="container">
      <p *simpleRandom="randomNumber">
        Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
      </p>
    </div>
    <button (click)="rand()">Genera numero casuale</button>
    <div>
      Numero generato {{ this.randomNumber}}
    </div>
  `
})
export class AppComponent {
  private randomNumber = 0;
  rand() {
    this.randomNumber = Math.random();
  }
}

Il componente AppComponent è piuttosto semplice, presenta un paragrafo che viene rimosso o inserito nel DOM a seconda del valore della proprietà randomNumber la quale viene aggiornata ogni volta che viene cliccato il pulsante che ‘Genera numero casuale’ invocando il metodo rand().

Vediamo infine il codice della direttiva riportato nel frammento di codice sottostante.

import {
  Directive,
  Input,
  TemplateRef,
  ViewContainerRef
} from '@angular/core';

@Directive({
  selector: '[simpleRandom]'
})
export class RandomDirective {

  private activeView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef) { }

  @Input() set simpleRandom(num: number) {
    if (num < 0.5 && !this.activeView) {
      this.activeView = true;
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else if (num >= 0.5 && this.activeView) {
      this.activeView = false;
      this.viewContainer.clear();
    }
  }

}

All’interno della direttiva avremo accesso a un’istanza di TemplateRef e una di ViewContainerRef grazie al meccanismo di Dependency Injection. La prima costituisce un riferimento al template interno (quello che viene automaticamente creato da Angular nel momento in cui sostituisce la sintassi che fa uso del carattere asterisco e racchiude fra i tag <ng-template> l’elemento a cui è stata applicata la direttiva strutturale.) della direttiva che può essere utilizzato come modello per istanziare una nuova View di tipo embedded ovvero una View che è creata a partire dal template stesso. Per creare una nuova istanza di embedded view sulla base del template interno alla direttiva, si fa uso del metodo createEmbeddedView() di ViewContainerRef il quale rappresenta un contenitore in cui conservare gli elementi della View pronti per essere mostrati sullo schermo. Facendo riferimento al video riportato sopra, il contenitore è costituito dal particolare commento che precede il paragrafo (quando è inserito nel DOM) a cui è originariamente applicata la direttiva. Il metodo createEmbeddedView() di ViewContainerRef riceve in ingresso il riferimento al template e provvede a inserire il contenuto nel DOM come elemento adiacente al contenitore. Facendo sempre riferimento al codice della direttiva, notiamo che abbiamo creato una proprietà di input la quale utilizza un metodo setter in modo tale da permettere alla direttiva di inserire l’elemento a cui è stata applicata nel DOM solo se il valore ricevuto in ingresso è superiore a 0.5. In caso contrario view invocato il metodo clear() di ViewContainerRef che distrugge le view del contenitore rimuovendo di fatto gli elementi a cui è associata la direttiva dal DOM.

Riepilogo

In questa lezione abbiamo mostrato attraverso due semplici esempi come sia semplice creare delle direttive personalizzate in Angular. Nella prossima lezione vedremo come lavorare con i form e descriveremo gli strumenti messi a nostra disposizione per semplificare la creazione delle applicazioni che necessitano di ricevere delle informazioni da parte di un utente.

Pubblicitร