Skip to content

thinktecture/angular-days-2025-spring-animations

 
 

Repository files navigation

Level up your Angular App: Mit Animationen zur UX, die begeistert (Hands-on)

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!

Installationsanweisungen

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.

Hands-on Lab

1. CSS Animationen

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 ist
Lö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.


2. Angular Animations

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 funktioniert
Lö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.


3. View Transition API

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.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • SCSS 51.9%
  • TypeScript 32.1%
  • HTML 7.2%
  • MDX 6.1%
  • CSS 2.7%