Angular, fra le tante cose, include un complesso sistema di animazioni. Ma lo sapevate che potete usare AnimationBuilder per scatenare animazioni a piacimento, in modo programmatico? No? Vediamolo assieme!
Indice dei contenuti
Creiamo una lista
Come prima cosa, creiamo una lista da mostrare in un ngFor. Facciamo gli sviluppatori seri e tipizziamo questa lista con un’interfaccia! 😀
import { Component, OnInit } from '@angular/core';
interface User {
id: number;
name: string;
}
@Component({
selector: 'my-app',
template: `
<ul>
<li>{{ user.name }}</li>
</ul>
`
})
export class AppComponent implements OnInit {
users: User[] = [];
ngOnInit() {
this.users = [
{ id: Math.random(), name: 'Michele' }
]
}
}
Come vedete, inizializziamo la nostra lista di utenti con un array vuoto, e all’OnInit la valorizziamo. Non solo: stiamo trattando la lista come un oggetto immutabile. In poche parole, non la modifichiamo con un push, quando il valore cambia la sovrascriviamo completamente. Vedremo fra poco perché ho fatto questa scelta!
Come dite? A cosa serve quell’id? Questo ve lo dico subito.
Animiamo la lista
Per prima cosa, ci serve un metodo per aggiungere utenti alla lista. Vogliamo animarla, no?
addUser() {
this.users = [
...this.users.map(user => ({ ...user })),
{ id: Math.random(), name: 'New user' }
];
}
Anche qui, approccio immutabile. Non solo: sto usando lo spread operator per rimpiazzare completamente i vecchi utenti con dei nuovi utenti! Identici, ma diversi, infatti il loro puntatore in memoria cambierà. Curiosi eh?
Ora pensiamo alle animazioni: vogliamo che ogni nuovo utente appaia con un’animazione. Scriviamone una semplice.
@Component({
selector: 'my-app',
template: `
<button (click)="addUser()">Add user</button>
<ul>
<li *ngFor="let user of users" @fadeIn>{{ user.name }}</li>
</ul>
`,
animations: [
trigger('fadeIn', [
transition(':enter', [
style({ transform: 'scale(0.5)', opacity: 0 }),
animate(
'.3s cubic-bezier(.8, -0.6, 0.2, 1.5)',
style({ transform: 'scale(1)', opacity: 1 })
)
])
])
]
})
export class AppComponent implements OnInit {
users: User[] = [];
ngOnInit() {
this.users = [
{ id: Math.random(), name: 'Michele' }
]
}
addUser() {
this.users = [
...this.users.map(user => ({ ...user })),
{ id: Math.random(), name: 'New user' }
];
}
}
Facciamo partire il codice e… non funziona! O meglio, funziona a metà: tutta la lista viene ri-animata quando aggiungiamo un elemento, anche se abbiamo applicato il nostro trigger direttamente sull’elemento <li>. Non è proprio quello che volevamo, no? Noi vogliamo che solamente i nuovi elementi appaiano con un’animazione. Ecco perché ho usato la proprietà id e perché ho adottato un approccio immutabile: per spiegarvi trackBy.
Ottimizziamo la lista con trackBy e aggiustiamo l’animazione
Si tratta di uno strumento utilissimo che Angular ci permette di utilizzare in abbinata con ngFor. Infatti, grazie a trackBy, possiamo dire ad Angular qual è la proprietà che contraddistingue ogni elemento, in modo che, quando gli elementi cambieranno, Angular non dovrà ri-renderizzare gli elementi già presenti.
@Component({
selector: 'my-app',
template: `
<button (click)="addUser()">Add user</button>
<ul>
<li
*ngFor="let user of users; trackBy: trackByUserId"
@fadeIn
>{{ user.name }}</li>
</ul>
`,
animations: [
trigger('fadeIn', [
transition(':enter', [
style({ transform: 'scale(0.5)', opacity: 0 }),
animate(
'.3s cubic-bezier(.8, -0.6, 0.2, 1.5)',
style({ transform: 'scale(1)', opacity: 1 })
)
])
])
]
})
export class AppComponent implements OnInit {
users: User[] = [];
ngOnInit() {
this.users = [
{ id: Math.random(), name: 'Michele' }
]
}
addUser() {
this.users = [
...this.users.map(user => ({ ...user })),
{ id: Math.random(), name: 'New user' }
];
}
trackByUserId(index, user: User) {
return user.id;
}
}
Ok, ora ci siamo! TrackBy non solamente ci ha aiutato in fatto di performance, ma ha anche permesso alla nostra transizione :enter di funzionare correttamente, perché i vecchi elementi non vengono rigenerati, e quindi non vengono presi di mira dall’animazione.
Ho utilizzato un approccio immutabile proprio per fare questa dimostrazione. Anche se gli elementi cambiano, Angular ricorderà il loro id e non li renderizzerà nuovamente.
Ora arriva il bello: immaginate che all’interno del <li> ci sia un form con le informazioni dell’utente. Il visitatore fa delle modifiche, preme il tasto salva e… quel form viene animato! Sarebbe figo, no? Ci sono più modi di ottenere questo effetto, ma oggi vediamo come ottenerlo con AnimationBuilder.
Utilizziamo AnimationBuilder per animare gli elementi
Anche qui, comportiamoci bene e scriviamo una direttiva:
import { Directive, ElementRef } from '@angular/core';
import { AnimationBuilder, style, animate } from '@angular/animations';
@Directive({
selector: '[blink]',
exportAs: 'blink'
})
export class BlinkDirective {
constructor(
private animationBuilder: AnimationBuilder,
private el: ElementRef
) {}
}
Questa direttiva sarà la responsabile dell’animazione sul singolo utente: l’ho chiamata BlinkDirective perché l’animazione sarà una specie di lampeggio. Più o meno.
Come vedete, ho iniettato il servizio AnimationBuilder e ElementRef, che contiene il riferimento all’elemento sul quale è applicata la direttiva. Ho anche esportato la direttiva con exportAs, una parte fondamentale del nostro piano. Se non l’avete mai usata, scoprirete fra poco a cosa serve.
Ricordate di includere BlinkDirective nell'NgModule giusto!
Ora, dobbiamo scrivere la nostra animazione. Ma come la scriviamo? E come la lanciamo? Beh, per entrambi utilizzeremo AnimationBuilder!
@Directive({
selector: '[blink]',
exportAs: 'blink'
})
export class BlinkDirective {
constructor(
private animationBuilder: AnimationBuilder,
private el: ElementRef
) {}
start() {
// La nostra animazione!
const myAnimation = this.animationBuilder.build([
style({ transform: 'scale(1)', opacity: 1 }),
animate(150, style({ transform: 'scale(1.1)', opacity: .5 })),
animate(150, style({ transform: 'scale(1)', opacity: 1 }))
]);
const player = myAnimation.create(this.el.nativeElement);
// Facciamo partire l'animazione
player.play();
}
}
Sembra complicato, ma non lo è: all’interno del metodo build scriviamo la nostra animazione, esattamente come la scriviamo nelle transition che scriviamo nei metadati di un componente. In questo caso, si partirà da uno stile di base, poi l’oggetto verrà scalato e ridotta l’opacità, per poi ritornare normale. Il metodo build ci ritorna una AnimationFactory, una specie di “istruzione” su come effettuare una animazione. Dobbiamo però agganciarla ad un elemento del DOM.
Quindi, abbiamo creato un AnimationPlayer con il metodo create: questo player è il legame fra l’animazione e il nostro elemento (un po’ come una Subscription di RxJS!) e possiamo utilizzarlo per far partire, mettere in pausa, far ripartire o stoppare un’animazione.
Infine, chiamiamo il metodo play per far partire l’animazione. Non ci interessano altre funzionalità per il momento, ma se volete vedere ciò di cui è capace l’AnimationPlayer, ecco la documentazione!
Applichiamo la direttiva alla lista
Ora non ci resta che utilizzare questa direttiva: vi ricorderete che ho utilizzato la proprietà exportAs nei metadati della direttiva, giusto? Ecco a cosa ci serve: ad accedervi dall’esterno!
@Component({
selector: 'my-app',
template: `
<button (click)="addUser()">Add user</button>
<ul>
<li
*ngFor="let user of users; trackBy: trackByUserId"
@fadeIn
blink
#blinkDir="blink"
>{{ user.name }} <button (click)="blinkDir.start()">Make me blink</button></li>
</ul>
`,
animations: [ ... ]
})
export class AppComponent implements OnInit {
...
}
Ho applicato la direttiva blink ad ogni elemento della lista, l’ho messa in una variabile di template (quella con il cancelletto!) grazie a exportAs, e ho creato un button che, premuto, chiama il metodo start sulla nostra direttiva per far partire l’animazione. Ora la nostra lista è veramente animata!
Conclusione
A questo punto avrete capito che potete utilizzare AnimationBuilder un po’ come vi pare, perché potete far partire le animazioni direttamente dal vostro codice JavaScript: i casi d’uso sono tanti! Fate qualche esperimento e sbizzarritevi per impratichirvi con questo servizio, non c’è nulla di complicato.
Ecco invece l’esempio completo, se volete vedere il risultato finale! (Non fate i pigri e provate a riscriverlo da soli 😉 )