Angular Dynamic Page Transitions
Reist mit Lukas in alle möglichen Richtungen durch dynamische Page Transitions in Angular Apps.
Page Transitions, bzw. Seitenübergänge, bezeichnen die Art, wie der Übergang von einer Seite zu einer anderen ausgestaltet ist. Gerade bei (Web-)Apps sind dabei Animationen sehr beliebt. Sie helfen dabei zu verstehen, wohin man sich bewegt: Geht man in einem Prozess vorwärts? Dann kommt die neue Seite gleich von rechts reingeflogen. Bewegt man sich im Prozess rückwärts, so ist die Animation umgekehrt. Die Art der Animation kann dabei mannigfaltig erdacht werden, aber wichtig ist: bestimmte Seitenwechsel sollten bestimmte Übergänge haben.
Warum Angular?
In diesem Artikel sehen wir uns die Implementierung von Page Transitions in Angular an. Das hat zwei Gründe:
- Als klassisches WebApp-Framework, mit dem auch komplexe Applikationen umzusetzen sind, ist Angular prädestiniert für Projekte, die Page Transitions benötigen.
- Wenn man sich das Angular-Animations-Framework ansieht, oder nach Implementierungsbeispielen sucht, so findet man meist Beispiele, die statisch zwischen zwei “States” animieren. Im echten Leben, mit echten Projekten, sind diese Ansätze aber nicht sonderlich hilfreich. Deshalb möchte ich euch zeigen, wie wir diese Herausforderung angehen.
Angulars Animation Framework
Angular kommt mit seinem eigenen Animation Framework, das wir für unser Projekt natürlich gerne nutzen möchten. Es ermöglicht uns, komplexe und ansprechende Animationen zu erstellen und bietet dazu eine Vielzahl von Funktionen, darunter CSS-Transitionen, Keyframe-Animationen und Animationen auf der Grundlage von Eingangsereignissen. Mit dem Angular Animation Framework können wir auch Animationen erstellen, die auf Statusänderungen basieren, beispielsweise wenn ein Element angezeigt oder ausgeblendet wird. Dazu werden sogenannte „States“ definiert.
Eine der größten Stärken des Angular Animation Frameworks ist seine nahtlose Integration mit Angular. Es ermöglicht uns, die Animationen direkt in den Komponenten zu definieren und sie automatisch in die Anwendung zu integrieren. Darüber hinaus bietet das Framework grundsätzlich eine umfassende Dokumentation, wobei wir später noch einen Kritikpunkt finden werden.
Um die Page Transitions umzusetzen, werden wir uns vor allem die Eingangsereignisse in Kombination mit den States ansehen, aber dazu später mehr.
Kurz noch zu den wichtigsten Begriffen, die im Folgenden nun immer wieder auftauchen werden und die innerhalb des Animation Frameworks ihre eigene Bedeutung haben:
Trigger: Es wird oft als “Auslöser” beschrieben, das ist allerdings etwas zu kurz gegriffen. Der Trigger verknüpft eine Animation (bzw. mehrere, je nach Definition) mit einem Element. Der Wert, den man dem Trigger übergeben kann, bzw. der sich zur Laufzeit ändern kann, bezeichnet den State.
State: ein State bezeichnet den “Status” eines Elements. Transitions (Animationen) werden von einem Status zu einem anderen ausgeführt. Die (CSS-)Eigenschaften des Elements in einem State können definiert werden, damit für die folgenden Transitions klar ist, zwischen welchen Werten die Animation stattfindet. Eine Beispielnotation für eine Animation, die einem Element zugewiesen wird, wäre: <div [@openClose]=“openState“>…</div>
Ändert sich der Wert von openState von “open” zu “closed”, so wird die Animation ausgeführt, die im Trigger “openClose” für diese Transition “open => closed” definiert ist.
Transition: Eine Transition definiert, zwischen welchen States welche Animation ausgeführt werden soll. Zum Beispiel wird eine Animation vom State “open” zu State “closed” definiert. Mit einem Stern (*) kann ein beliebiger State Teil der Transition sein (z.B.: “void => *” würde eine Transition von “nicht vorhanden” zu “irgendein State” beschreiben)
Eine kleine Besonderheit sind die vordefinierten Transitions: “:enter” und “:leave”: sie sind unabhängig vom State-Wert und lösen die Animation dann aus, wenn das Element erstellt wird (bspw. durch ngIf oder Routing), bzw. zerstört wird. Sie sind eine Kurzform von “void => *” bzw. “* => void”
Das Problem
Wie eingangs bereits erwähnt, findet man selten echte Beispiele, die einem bei echten Projekten helfen, wenn es darum geht, dynamisch zu entscheiden, in welche Richtung eine Animation ablaufen soll.
Was man immer wieder findet:
- State Changes werden dazu benutzt, immer wieder dieselbe Animation zu triggern (https://angular.io/guide/route-animations)
- Es wird für einzelnen State-Änderungen (im Sinne von: ‘HomePage’ => ‘Subpage 1’) explizit definiert, welche Animation getriggert wird (zb hier: https://medium.com/ngconf/animating-angular-route-transitions-ef02b871cc30)
Während ersteres eigentlich nicht wirklich unseren Wunsch abdeckt, dass die Animation das widerspiegelt, was die/der User:in macht, funktioniert letzteres natürlich schon. Allerdings macht diese Art der Definition bei größeren Projekten keinen Spaß mehr. Es müsste dann ja jeder State (in diesem Fall: jede Seite) jede Page Transition zu jeder anderen Seite definieren, was schon bei wenigen Seiten recht unübersichtlich wird.
Was wir machen wollen:
- Zur Laufzeit entscheiden, welche Animation zwischen den Seiten verwendet wird
Klingt nach einem guten Ziel? Dann los!
Der Ansatz
Ziel unserer Animationen ist, dass durch das Routing die Animation ausgelöst wird. Unsere Seiten haben einen Trigger, der sich “@BasePageAnimation” nennt und wir werden uns auf die Transitions “void => state” und “state => void” beschränken.
Die States werden die Animationsrichtung bezeichnen: zb. “left” soll dafür sorgen, dass die Animationsrichtung nach links gerichtet ist: die neue Seite bewegt sich von rechts (außerhalb des Bildschirms) nach links herein, die alte Seite verschwindet nach links außen.
Folgende States wird es geben
- “left”: Bewegung nach links: die alte Seite verschwindet nach links, die neue Seite kommt von rechts rein
- “right”: vice versa, Bewegung nach rechts
- “up”: Bewegung nach oben
- “down”: Bewegung nach unten
- “fade”: keine Bewegung, alte Seite wird transparent, neue wird opak
- “zoom-in”: Skalierungsanimation, alte Seite skaliert recht groß und fadet dabei aus, neue Seite skaliert von 0,7 auf 1
- “zoom-out”: Vice versa
- (und als kleine Spielerei auch noch eine Variation von left und right, aber das seht ihr dann unten in der Beispiel App)
Mit diesen States können wir die üblichsten Page Transitions abdecken.
Die Grundidee ist, dass erst beim Klick auf einen Link oder Button entschieden wird, welche Transition ausgeführt wird.
Und nun ab in den Code:
Am Beispiel
Unser wunderschönes Beispielprojekt hat zwei Routen: die A-Seite und die B-Seite. Zwischen diesen beiden wird navigiert. Das sind auch zwei unabhängige Komponenten (AComponent, BComponent), die allerdings von der selben AnimationBaseComponent erben.
Warum AnimationBaseComponent?
Die AnimationBaseComponent kümmert sich um alles, was die Komponenten brauchen, damit die Seitenanimationen funktionieren:
Sie setzt den Animation Trigger auf das Host-Element, importiert die Animationen, nutzt das NavigationService für das Abrufen der Navigationsrichtung, etc.
@Component({
selector: 'app-animation-base',
template: '',
styleUrls: ['animation-base-page.scss'],
animations: [basePageAnimations],
})
Die Animations werden der BaseComponent übergeben.
export class AnimationBaseComponent implements OnDestroy {
@HostBinding("@basePageAnimation") containerAnimation = "fade";
Der Animation Trigger wird über HostBinding gesetzt.
private _navigationSubscription: Subscription;
constructor(protected _navigationService: NavigationService) {
this.containerAnimation =
this._navigationService.getBasePageAnimationDirection();
this._navigationSubscription = this._navigationService.animationDirection.subscribe((direction:BasePageAnimationDirection) =>
this.setAnimationDirection(direction)
);
}
Im Constructor wird ContainerAnimation (Richtung) geholt, bzw. auf die animationDirection subscribed…
public setAnimationDirection(direction:BasePageAnimationDirection) {
this.containerAnimation = direction;
}
… und die lokale Variable aktualisiert.
Das NavigationService kümmert sich darum
Das Navigation Service kümmert sich um zwei Dinge: Einerseits entscheidet es über die Animationsrichtung, andererseits informiert es die beteiligten Komponenten über jene Richtung.
Dabei müssen wir 2 Fälle extra beachten:
Die neue Komponente
Wird die neue Komponente initialisiert, durchläuft sie ihren Konstruktor, in dem das NavigationService die aktuelle Animationsrichtung (animationDirection) abruft und dem Trigger zuweist.
Dadurch, dass der Konstruktor noch vor dem klassischen Lifecycle von Angular Komponenten befindet, ist der State (z.B.: “left”) bereits gesetzt, wenn die Animation nach passenden Transitions sucht -> in diesem Fall: “void => left”
public getBasePageAnimationDirection():BasePageAnimationDirection {
return this._animationDirection;
}
navigationService: Hier wird die aktuelle Animationsrichtungsvariable geholt.
Die alte Komponente
Hier müssen wir darauf achten, dass die verschwindende Komponente die Animationsrichtung bekommt, bevor die Zerstörung beginnt. Die Komponente subscribed sich zwar auf Aktualisierungen der animationDirection, allerdings muss diese Änderung rechtzeitig erfolgen. Deshalb wird bei einem Klick nicht direkt der Angular Router verwendet, sondern das NavigationService, das zuerst die Entscheidung trifft, welche Richtung nun verwendet werden soll, diese Information an alle subscribenden Komponenten ausspielt und danach erst die Navigationsänderung ausführt.
Somit ist die Transition zum Zeitpunkt der Zerstörung der Komponente dann beispielsweise “left => void” und die Animation wird korrekt ausgeführt.
public navigateByUrl(
url:string,
animationDirection?:BasePageAnimationDirection
) {
let direction = animationDirection? animationDirection : this._animationDirection;
this.setBasePageAnimationDirection(direction);
// emit current animation data
this.animationDirection.next(direction);
timer(1).subscribe((i) => {
this._router.navigate([url]);
});
}
NavigationService: Die navigateByUrl-Funktion setzt die Richtung, emittiert den Wert an die Subscriber und navigiert dann auf die Route.
Zusätzliche Erklärungen des Beispielprojekts:
Dieses Beispiel ist ein bisschen gekürzt, aber nicht in der grundlegenden Funktion. Es wird in diesem Beispiel die Animationsrichtung vom jeweiligen Button mitübergeben und nicht vom Service errechnet oder auf Basis der URL geändert – aber das könnte man ganz einfach im NavigationService machen.
Die Richtung jedenfalls ist nicht vordefiniert im Sinne von “Der Übergang von Seite A zu Seite B soll so aussehen”, sondern wird durch den AnimationDirection-String direkt definiert.
Ein Nachteil dieser Lösung ist, dass das “native” Routing über das Router-Service, bzw. via ng-route-Parameter nicht verwendet werden kann.
Da der State gesetzt sein muss, bevor die eine Komponente destroyed und die andere erstellt wird, müssen wir den Umweg über ein eigenes Navigation Service machen.
Und so siehts aus:
Hier seht ihr nun das Ergebnis: klickt euch durch 😉
Das Projekt, das ihr hier seht, gibts auch für euch auf github: https://github.com/snookas/ng-dynamic-page-transitions
Ich hoffe, unser Ansatz ist interessant und vielleicht sogar hilfreich für euch! Wie macht ihr dynamische Page Transitions in Angular? Habt ihr vielleicht eine ganz andere Idee?
Du willst mit jemanden über das Thema plaudern?
Einen kostenlosen Termin mit CEO Susanne vereinbaren!UX Snacks Vol.09
That’s a wrap on UX Snacks 2024. Am 7. November hat die vierte und letzte Ausgabe in diesem Jahr stattgefunden und wir nehmen mit diesem Recap ganz viel positive UX-Energie mit ins neue Jahr. Und keine Angst: Schon bald verkünden wir die Daten für 2025.
Jetzt lesenFolge #62 mit Susanne Liechtenecker
In Folge 62 besinnt sich Susanne auf die Anfänge dieses Podcasts und begrüßt keinen Gast, sondern erzählt über das Buch "Jäger, Hirten, Kritiker" von Richard David Precht und warum es sie inspiriert hat.
Jetzt anhören