Lesson 11-Angular Animations

Angular Animation Basics

Triggers

Animation triggers are the core of Angular animations, defining when and how animations occur. Triggers are bound to DOM elements and can include multiple states and transitions.

// app.component.ts
import { Component } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';

@Component({
  selector: 'app-root',
  template: `
    <div [@flyInOut]="isShown ? 'in' : 'out'" (click)="toggle()">
      Click me!
    </div>
  `,
  animations: [
    trigger('flyInOut', [
      state('in', style({ transform: 'translateX(0)' })),
      state('out', style({ transform: 'translateX(-100%)' })),
      transition('in => out', animate('0.5s ease-out')),
      transition('out => in', animate('0.5s ease-in'))
    ])
  ]
})
export class AppComponent {
  isShown = true;

  toggle() {
    this.isShown = !this.isShown;
  }
}

States

States represent specific phases of an animation. In the example above, in and out are two states, each defining the element’s styles.

Transitions

Transitions define the animation effect between states. For instance, transition('in => out', animate('0.5s ease-out')) specifies a 0.5-second animation with an ease-out easing function when moving from the in state to the out state.

Animation Queries and Groups

Animation queries allow referencing other animations, while animation groups enable running multiple animations simultaneously.

// app.component.ts
import { trigger, state, style, transition, animate, group, query, animateChild } from '@angular/animations';

animations: [
  trigger('flyInOut', [
    state('in', style({ transform: 'translateX(0)' })),
    state('out', style({ transform: 'translateX(-100%)' })),
    transition('in => out', animate('0.5s ease-out')),
    transition('out => in', animate('0.5s ease-in'))
  ]),
  trigger('fade', [
    state('in', style({ opacity: 1 })),
    state('out', style({ opacity: 0 })),
    transition('in => out', animate('0.5s ease-out')),
    transition('out => in', animate('0.5s ease-in'))
  ]),
  group([
    query('@flyInOut', animateChild()),
    query('@fade', animateChild())
  ])
]

Advanced Animation Usage

Animation Parameters

Animation parameters enable dynamic adjustments to aspects like duration or easing functions.

// app.component.ts
import { Component, Input } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';

@Component({
  selector: 'app-root',
  template: `...`,
  animations: [
    trigger('flyInOut', [
      state('in', style({ transform: 'translateX(0)' })),
      state('out', style({ transform: 'translateX(-100%)' })),
      transition('in => out', animate('{{duration}}ms ease-out', { params: { duration: 1000 } })),
      transition('out => in', animate('{{duration}}ms ease-in', { params: { duration: 1000 } }))
    ])
  ]
})
export class AppComponent {
  @Input() duration: number = 1000; // Animation duration in milliseconds
}

Animation Events

Animation events allow executing code at different animation stages, such as start or end.

// app.component.ts
import { Component, HostListener } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `...`
})
export class AppComponent {
  @HostListener('animationstart', ['$event'])
  onAnimationStart(event: AnimationEvent) {
    console.log('Animation started:', event);
  }

  @HostListener('animationend', ['$event'])
  onAnimationEnd(event: AnimationEvent) {
    console.log('Animation ended:', event);
  }
}

Animations in Real-World Projects

In real projects, you might animate list items, apply fade-in/fade-out effects to modals, or add sliding effects to navigation menus. Below is an example of animating list items:

// app.component.ts
import { Component } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';

@Component({
  selector: 'app-root',
  template: `
    <ul>
      <li *ngFor="let item of items" [@listItem]="item.status">
        {{ item.text }}
      </li>
    </ul>
  `,
  animations: [
    trigger('listItem', [
      state('void', style({ opacity: 0, transform: 'scale(0.5)' })),
      state('visible', style({ opacity: 1, transform: 'scale(1)' })),
      transition(':enter', animate('0.5s ease-out')),
      transition(':leave', animate('0.5s ease-in'))
    ])
  ]
})
export class AppComponent {
  items = [
    { text: 'Item 1', status: 'visible' },
    { text: 'Item 2', status: 'visible' },
    { text: 'Item 3', status: 'visible' }
  ];

  addItem() {
    this.items.push({ text: `New Item ${this.items.length + 1}`, status: 'void' });
    setTimeout(() => {
      this.items[this.items.length - 1].status = 'visible';
    }, 0);
  }
}

Animations with Routing

Angular’s animation system integrates seamlessly with the routing system, enabling smooth transitions between pages.

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';

const routes: Routes = [
  { path: '', component: HomeComponent, pathMatch: 'full' },
  { path: 'about', component: AboutComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
// app.component.ts
import { Component } from '@angular/core';
import { routeAnimation } from './route-animation';

@Component({
  selector: 'app-root',
  template: `
    <router-outlet></router-outlet>
  `,
  animations: [routeAnimation]
})
export class AppComponent { }
// route-animation.ts
import { trigger, state, style, transition, animate } from '@angular/animations';

export const routeAnimation = trigger('routeAnimation', [
  state('*', style({ position: 'relative' })),
  transition('* => isLeft', slideTo('left')),
  transition('* => isRight', slideTo('right')),
  transition('isLeft => *', slideFrom('left')),
  transition('isRight => *', slideFrom('right')),
]);

function slideTo(direction: string) {
  return [
    style({ position: 'relative' }),
    animate('0.5s ease-out', style({ transform: `translateX(${direction === 'left' ? '-100%' : '100%'})` }))
  ];
}

function slideFrom(direction: string) {
  return [
    style({ position: 'relative', transform: `translateX(${direction === 'left' ? '-100%' : '100%'})` }),
    animate('0.5s ease-in', style({ transform: `translateX(0%)` }))
  ];
}

Animation Testing

Testing animations ensures consistent behavior across scenarios. Use Angular’s testing tools like Karma and Jasmine for unit and end-to-end animation tests.

// animation.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { AnimationComponent } from './animation.component';

describe('AnimationComponent', () => {
  let component: AnimationComponent;
  let fixture: ComponentFixture<AnimationComponent>;
  let de: DebugElement;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [AnimationComponent]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(AnimationComponent);
    component = fixture.componentInstance;
    de = fixture.debugElement.query(By.css('.animated-element'));
    fixture.detectChanges();
  });

  it('should animate on click', () => {
    de.triggerEventHandler('click', null);
    fixture.whenStable().then(() => {
      expect(de.nativeElement.style.transform).toBe('translateX(100%)');
    });
  });
});

Animation Collaboration and State Machines

In complex UI scenarios, animation states may depend on multiple conditions. State machines, supported by @angular/animations, enable complex animation logic.

// app.component.ts
import { Component } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="toggle()">Toggle</button>
    <div [@stateMachine]="state"></div>
  `,
  animations: [
    trigger('stateMachine', [
      state('state1', style({ backgroundColor: 'red' })),
      state('state2', style({ backgroundColor: 'blue' })),
      transition('state1 => state2', animate('0.5s ease-in')),
      transition('state2 => state1', animate('0.5s ease-out'))
    ])
  ]
})
export class AppComponent {
  state = 'state1';

  toggle() {
    this.state = this.state === 'state1' ? 'state2' : 'state1';
  }
}

Responsive Animations

In responsive design, animations may need to adapt based on screen size or device type. Use media queries or Angular’s ViewportRuler for responsive animations.

// responsive.component.ts
import { Component, OnInit } from '@angular/core';
import { ViewportRuler } from '@angular/cdk/overlay';
import { trigger, state, style, transition, animate } from '@angular/animations';

@Component({
  selector: 'app-responsive',
  template: `
    <div [@responsiveAnimation]="animationState"></div>
  `,
  animations: [
    trigger('responsiveAnimation', [
      state('small', style({ width: '50px' })),
      state('large', style({ width: '200px' })),
      transition('small => large', animate('0.5s')),
      transition('large => small', animate('0.5s'))
    ])
  ]
})
export class ResponsiveComponent implements OnInit {
  animationState = 'small';

  constructor(private viewportRuler: ViewportRuler) {}

  ngOnInit() {
    this.viewportRuler.getViewportSize().subscribe(size => {
      this.animationState = size.width < 600 ? 'small' : 'large';
    });
  }
}

Animations and Performance

While animations enhance user experience, excessive animations in large applications can negatively impact performance. Here are strategies to optimize animation performance:

  • Use requestAnimationFrame: Ensure animations update on each frame, not on every render.
  • Avoid Forced Synchronous Layouts: Refrain from modifying DOM styles directly in animations, as this triggers layout recalculations.
  • Use CSS Animations Instead: For simple effects, CSS animations may be more efficient, leveraging hardware acceleration.
  • Minimize Repaints and Reflows: Avoid animating layout-related properties (e.g., width, height, top, left), which cause reflows and repaints.
  • Use Compositing Layers: Apply animations to transform or opacity properties to leverage GPU acceleration.
  • Lazy Load Animations: For non-visible animations, use lazy loading to load resources only when needed.

Case Study: Dynamic Loading Animations

In some cases, you may want to load animations dynamically based on runtime conditions. This can be achieved using AnimationBuilder and Renderer2.

// dynamic.component.ts
import { Component, Renderer2, ElementRef, ViewChild } from '@angular/core';
import { AnimationBuilder, AnimationPlayer, style, animate, keyframes } from '@angular/animations';

@Component({
  selector: 'app-dynamic',
  template: `
    <div #dynamicElement></div>
  `
})
export class DynamicComponent {
  @ViewChild('dynamicElement') dynamicElement: ElementRef;

  constructor(
    private renderer: Renderer2,
    private animationBuilder: AnimationBuilder
  ) {}

  loadAnimation() {
    const factory = this.animationBuilder.build([
      animate('1s', keyframes([
        style({ opacity: 0, offset: 0 }),
        style({ opacity: 1, offset: 1 })
      ]))
    ]);

    const player: AnimationPlayer = factory.create(this.dynamicElement.nativeElement);
    player.play();
  }
}
Share your love