Skip to content

Latest commit

 

History

History
507 lines (397 loc) · 12.8 KB

File metadata and controls

507 lines (397 loc) · 12.8 KB

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.