Angular Days 2025, 20. März 2025, 13:30–17:00
Trainer: Sascha Lehmann (@derLehmann_S) (@Thinktecture)
Animationen sind das Geheimnis hinter einer User Experience, die im Gedächtnis bleibt – aber hier gilt nicht ‚viel hilft viel‘! In diesem Hands-on-Workshop zeigt Sascha Lehmann von Thinktecture das ‚kleine Einmaleins der Animationen‘ und wie Sie Ihre Angular-App mit durchdachtem Einsatz aufs nächste Level bringen. Lernen Sie, CSS-, Angular-Animationen und die brandaktuelle View-Transition-API geschickt einzusetzen und wiederverwendbar zu strukturieren. Verleihen Sie Ihrer Angular-App dadurch den perfekten Schliff!
Sie können mitentwickeln: Bitte bringen Sie dazu Ihr Notebook mit installiertem Google Chrome (Canary), Git, Node.js und WebStorm oder Visual Studio Code mit.
Es ist ein uneingeschränkter Internetzugriff erforderlich (ohne Gruppenrichtlinien, Unternehmensproxys und -firewalls), bitte im Zweifel das Privatnotebook einpacken.
Nach dem Klonen des Repositorys führen Sie bitte folgende Kommandos auf der Kommandozeile aus:
npm i
npm start
NOTE
Wenn Sie auf dem Branch view-transitions wechseln müssen Sie anschließend einen forcierten Install vornehmen, da dort mit der Preview Version von Angular gearbeitet wird und es deshalb zu Problemen mit Peer-Dependencies kommen kann.
npm i --force
Das Projekt beruht auf er Angular CLI Version 19.2.3.
Checke zunächst den Branch css-animations aus um darauf die Übung auszuführen.
1. Aufgabe Navigation-Dawer
Implementiere eine CSS Animation, sodass der Navigation-Drawer beim Laden der App von links nach rechts in die View "slided".
Lösung 1. Aufgabe
:host {
// ...
animation: slideInFromLeft 400ms both ease-out;
}
@keyframes slideInFromLeft {
from {
transform: translateX(-100%);
}
}
2. Aufgabe
Implementiere für die **Home-Component** folgende CSS Animation: 1. Die Conference-Cards sowie die Messages werden **gemeinsam** in die View animiert (Art und Weise sind deiner Kreativität überlassen ;-) ) 2. Anschließend wird die Search-Bar verzögert in die View animiert. 3. Zusatz: Die beiden Animationen sollen erst starten, **nachdem** die Animation des **Navigation-Drawer** abgeschlossen istLösung 2. Aufgabe
@mixin fade {
animation-name: fadeIn;
animation-duration: 400ms;
animation-fill-mode: both;
animation-timing-function: ease-in-out;
animation-delay: 500ms;
}
sl-search-bar {
// ...
animation-name: slideInFromTop;
animation-duration: 350ms;
animation-fill-mode: both;
animation-timing-function: ease-in-out;
animation-delay: calc(400ms + 500ms); // Dely Drawer Animation + Conference & Messages Animation
}
.conferences-container {
// ...
@include fade;
}
.messages-container {
// ...
@include fade;
}
@keyframes slideInFromTop {
from {
transform: translateY(-100%);
opacity: 0;
}
}
3. Aufgabe
Implementiere für die Conferences-Component folgende Animationen: 1. Animiere die Liste der Conference-Cards gemeinsam in die View. (Du kannst gerne kreativ werden) 2. In der Component befindet sich auch ein Conference-Info-Drawer der erscheint, wenn auf eine Card geklickt wird. Animiere diesen so, dass er beim Erscheinen von rechts in die View slidet. 3. Zusatz: Kannst du ihn auch wieder heraussliden lassen?
Lösung 3. Aufgabe
conference-info-drawer.component.scss
:host {
// ...
animation-name: slideInFromRight;
animation-duration: 800ms;
animation-timing-function: ease-in-out;
}
@keyframes slideInFromRight {
from {
transform: translateX(100%);
}
}
conferences.component.scss
.cards {
//...
animation-name: fadeInFromBottom;
animation-duration: 400ms;
animation-fill-mode: both;
}
@keyframes fadeInFromBottom {
from {
opacity: 0.6;
transform: translateY(1rem);
}
}
4. Aufgabe (Zusatz)
Implementiere für die **Messages-Component** eine "staggering" Animation. D.h. dass alle List-Items nacheinander, mit einem leichten Delay in die View animiert werden.Lösung 4. Aufgabe
<div class="messages">
<sl-list-item
*ngFor="let message of messages$ | async; index as i"
[style.--message-index]="i+1"
[model]="message"
(close)="deleteMessage($event)"
class="message"
></sl-list-item>
</div>
.messages {
//...
.message {
animation: fallIn 350ms both ease-in-out;
animation-delay: calc(var(--message-index) * 100ms);
}
}
@keyframes fallIn {
from {
opacity: 0;
transform: scale(0.6) translateY(-0.5rem);
}
}
Gesamtlösung: Wenn du dir meine beispielhafte Gesamtlösung anschauen möchtst kannst du dir den Branch css-animations-solution auschecken.
Checke zunächst den Branch angular-animations aus um darauf die Übung auszuführen.
1. Aufgabe
Implementiere Slide-In und Slide-Out Angular-Animations für den Navigation-Drawer und den Conference-Info-Drawer.
Zusatz: Überlege wie könnte man den Animationscode wiederverwendbarer gestalten?
Lösung 1. Aufgabe
navigation-drawer.component.ts
@Component({
// ...
animations: [slideInOutAnimationFactory('X', '-100%', '0')],
})
export class NavigationDrawerComponent {
@HostBinding('@slideInOut')
private slideInOut = true;
}
conference-info-drawer.component.ts
@Component({
// .....
animations: [slideInOutAnimationFactory('X', '100%', '0')],
})
export class ConferenceInfoDrawerComponent implements OnInit, OnChanges {
// ....
@HostBinding('@slideInOut')
private slideInOut = true;
// ...
}
slide.animation.ts
export const slideInOutAnimationFactory = (
direction: AnimationDirection,
from: string,
to: string,
duration: string = getCSSPropertyValue('--md-sys-motion-duration-medium-3'),
easing: string = getCSSPropertyValue('--md-sys-motion-easing-emphasized'),
): AnimationTriggerMetadata =>
trigger('slideInOut', [
transition(':enter', [
style({
opacity: 0.8,
transform: `translate${direction}(${from})`,
}),
animate(
`${duration} ${easing}`,
style({
opacity: 1,
transform: `translate${direction}(${to})`,
}),
),
]),
transition(':leave', [
style({
transform: `translate${direction}(${to})`,
}),
animate(
`${getCSSPropertyValue('--md-sys-motion-duration-short-3')} ${easing}`,
style({
opacity: 0.6,
transform: `translate${direction}(${from})`,
}),
),
]),
]);
```
</details>
<details><summary>2. Aufgabe</summary>
1. Implementiere die "staggering" Animation der **Messages-Component** aus der CSS-Animation-Aufabe nun mit Hilfe von Angular-Animations
2. Implementiere eine "delete" Animation, bei der das gelöschte List-Item nach links aus der View gleitet.
</details>
<details><summary>Lösung 2. Aufgabe</summary>
**messages.component.html**
```html
<div @listContainer class="messages">
<sl-list-item
@listItem
*ngFor="let message of messages$ | async"
[model]="message"
(close)="deleteMessage($event)"
></sl-list-item>
</div>
messages.component.ts
@Component({
//...
animations: [ListContainer, ListItem],
})
export class MessagesComponent {
// ...
}
list.animation.ts
export const ListItem = trigger('listItem', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-1.5rem)', scale: 0.8 }),
animate(
`${getCSSPropertyValue('--md-sys-motion-duration-medium-3')} ${getCSSPropertyValue(
'--md-sys-motion-easing-decelerating',
)}`,
style({ opacity: 1, transform: 'translateY(0)', scale: 1 }),
),
]),
transition(':leave', [
sequence([
animate(
`${getCSSPropertyValue('--md-sys-motion-duration-short-3')} ${getCSSPropertyValue(
'--md-sys-motion-easing-accelerating',
)}`,
style({
transform: `translateX(-200%)`,
}),
),
animate(
`${getCSSPropertyValue('--md-sys-motion-duration-short-3')} ease`,
style({
height: 0,
}),
),
]),
]),
]);
export const ListContainer = trigger('listContainer', [
transition(':enter, :leave', [
query('@*', [
stagger(getCSSPropertyValue('--md-sys-motion-duration-stagger-delay'), [animateChild()]),
]),
]),
]);
3. Aufgabe
1. Implementiere generelle Routing-Animationen 2. Zusatz: Sorge dafür, dass beim Navigieren auf die **Messages-Component** die zuvor implementierte List-Animation wieder/immernoch funktioniertLösung 3. Aufgabe
app.component.html
<sl-navigation-drawer *slScreenMediumAndLarge></sl-navigation-drawer>
<main [@routerAnimation]="prepareRoute(outlet)">
<router-outlet #outlet="outlet"></router-outlet>
</main>
<sl-bottom-bar *slScreenXSmallAndSmall></sl-bottom-bar>
app.component.ts
@Component({
// ...
animations: [RouterAnimations],
})
export class AppComponent {
public prepareRoute(outlet: RouterOutlet) {
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
}
}
app.routes.ts
export const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent, data: { animation: 'home' } },
{ path: 'messages', loadChildren: () => import('./components/messages/message.routes') },
{ path: 'conferences', loadChildren: () => import('./components/conferences/conference.routes') },
];
conference.routes.ts
const conferenceRoutes: Routes = [
{ path: '', component: ConferencesComponent, data: { animation: 'conferences' } },
{ path: ':id', component: ConferenceDetailComponent, data: { animation: 'detail' } },
];
messages.routes.ts
const messageRoutes: Routes = [
{ path: '', component: MessagesComponent, data: { animation: 'messages' } },
{ path: ':id', component: MessageDetailComponent, data: { animation: 'detail' } },
];
router.animation.ts
const routeReset = [
style({ position: 'relative' }),
query(
':enter, :leave',
[
style({
width: 'calc(100% - 7rem)',
marginLeft: '7rem',
position: 'fixed',
top: 0,
left: 0,
opacity: 0,
}),
],
{ optional: true },
),
];
const defaultLeave = [
style({ opacity: 1 }),
animate(
getCSSPropertyValue('--md-sys-motion-duration-short-3'),
style({ opacity: 0, transform: 'translateY(1rem)' }),
),
];
const defaultEnter = [
style({ opacity: 0, transform: 'translateY(1rem)' }),
animate(
getCSSPropertyValue('--md-sys-motion-duration-medium-3'),
style({ opacity: 1, transform: 'translateY(0)' }),
),
];
export const RouterAnimations: AnimationTriggerMetadata = trigger('routerAnimation', [
transition('* => messages, messages => *', [
...routeReset,
group([
query(':leave', [...defaultLeave], {
optional: true,
}),
query(
':enter',
[...defaultEnter, query('@listContainer', [animateChild()], { optional: true })],
{
optional: true,
},
),
]),
]),
transition('* => *', [
...routeReset,
group([
query(':leave', [...defaultLeave], { optional: true }),
query(':enter', [...defaultEnter], { optional: true }),
]),
]),
]);
Gesamtlösung: Wenn du dir meine beispielhafte Gesamtlösung anschauen möchtst kannst du dir den Branch angular-animations-solution auschecken.
Dieser Übungsteil dient dazu, dass ihr ein euch ein bisschen mit der View-Transition-API vertraut machen und experimentiern könnt. Deshalb gibt es auch keine konkrete Aufgabe die es zu lösen gibt. Seid etwas kreativ und versucht einmal verschiedene Bestanddteile und Übergänge zu animieren.