آنلاین

مهاجرت از بسته Animations در Angular

بسته @angular/animations از نسخه v20.2 منسوخ شده است؛ همین نسخه قابلیت‌های جدید animate.enter و animate.leave را برای افزودن animation به برنامه معرفی کرد. با این قابلیت‌ها می‌توانید تمام animationهای مبتنی بر @angular/animations را با CSS ساده یا کتابخانه‌های animation در JS جایگزین کنید. حذف @angular/animations از برنامه می‌تواند اندازه bundle مربوط به JavaScript را به‌شکل چشمگیری کاهش دهد. animationهای بومی CSS معمولاً عملکرد بهتری دارند، زیرا می‌توانند از hardware acceleration بهره ببرند. این راهنما فرایند بازآرایی کد از @angular/animations به animationهای بومی CSS را شرح می‌دهد.

نوشتن animation با CSS بومی

اگر تاکنون animation بومی CSS ننوشته‌اید، راهنماهای بسیار خوبی برای شروع وجود دارند. چند مورد از آن‌ها عبارت‌اند از: راهنمای CSS Animations در MDN راهنمای CSS3 Animations در W3Schools آموزش کامل CSS Animations CSS Animation برای مبتدیان

و چند ویدئو: یادگیری CSS Animation در ۹ دقیقه فهرست پخش آموزش CSS Animation از Net Ninja

برخی از این راهنماها و آموزش‌ها را بررسی کنید و سپس به این راهنما بازگردید.

ساخت animationهای قابل‌استفاده مجدد

درست مانند بسته animations، می‌توانید animationهای قابل‌استفاده مجددی بسازید که در سراسر برنامه به اشتراک گذاشته شوند. در نسخه مبتنی بر بسته animations، از تابع animation() در یک فایل مشترک TypeScript استفاده می‌شد. نسخه CSS بومی مشابه آن است، اما در یک فایل CSS مشترک قرار می‌گیرد.

با بسته Animations

animations.ts
// #docplaster
// #docregion animation-const, trigger-const
import {animation, style, animate, trigger, transition, useAnimation} from '@angular/animations';
// #enddocregion trigger-const

export const transitionAnimation = animation([
  style({
    height: '{{ height }}',
    opacity: '{{ opacity }}',
    backgroundColor: '{{ backgroundColor }}',
  }),
  animate('{{ time }}'),
]);
// #enddocregion animation-const

// #docregion animation-example
export const sharedAnimation = animation([
  style({
    height: 0,
    opacity: 1,
    backgroundColor: 'red',
  }),
  animate('1s'),
]);
// #enddocregion animation-example

// #docregion trigger-const
export const triggerAnimation = trigger('openClose', [
  transition('open => closed', [
    useAnimation(transitionAnimation, {
      params: {
        height: 0,
        opacity: 1,
        backgroundColor: 'red',
        time: '1s',
      },
    }),
  ]),
]);
// #enddocregion trigger-const

با CSS بومی

animations.css
/* #docregion animation-shared */
@keyframes sharedAnimation {
  to {
    height: 0;
    opacity: 1;
    background-color: 'red';
  }
}

.animated-class {
  animation: sharedAnimation 1s;
}
/* #enddocregion animation-shared */

/* #docregion animation-states */

.open {
  height: '200px';
  opacity: 1;
  background-color: 'yellow';
  transition: all 1s;
}

.closed {
  height: '100px';
  opacity: 0.8;
  background-color: 'blue';
  transition: all 1s;
}

/* #enddocregion animation-states */

/* #docregion animation-timing */

.example-element {
  animation-duration: 1s;
  animation-delay: 500ms;
  animation-timing-function: ease-in-out;
}

.example-shorthand {
  animation: exampleAnimation 1s ease-in-out 500ms;
}

/* #enddocregion animation-timing */

/* #docregion transition-timing */

.example-element {
  transition-duration: 1s;
  transition-delay: 500ms;
  transition-timing-function: ease-in-out;
  transition-property: margin-right;
}

.example-shorthand {
  transition: margin-right 1s ease-in-out 500ms;
}

/* #enddocregion transition-timing */

افزودن class با نام animated-class به یک element،‏ animation آن element را فعال می‌کند.

animate کردن یک transition

animate کردن state و styleها

بسته animations به شما امکان می‌داد با تابع state() درون یک component،‏ stateهای گوناگون تعریف کنید. برای مثال، stateهای open یا closed می‌توانند styleهای مربوط به هر state را در تعریف خود داشته باشند:

با بسته Animations

open-close.ts
// #docplaster
import {Component, input} from '@angular/core';
import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';

// #docregion component, events1
@Component({
  selector: 'app-open-close',
  // #docregion trigger-wildcard1, trigger-transition
  animations: [
    trigger('openClose', [
      // #docregion state1
      // ...
      // #enddocregion events1
      state(
        'open',
        style({
          height: '200px',
          opacity: 1,
          backgroundColor: 'yellow',
        }),
      ),
      // #enddocregion state1
      // #docregion state2
      state(
        'closed',
        style({
          height: '100px',
          opacity: 0.8,
          backgroundColor: 'blue',
        }),
      ),
      // #enddocregion state2, trigger-wildcard1
      // #docregion transition1
      transition('open => closed', [animate('1s')]),
      // #enddocregion transition1
      // #docregion transition2
      transition('closed => open', [animate('0.5s')]),
      // #enddocregion transition2, component
      // #docregion trigger-wildcard1
      transition('* => closed', [animate('1s')]),
      transition('* => open', [animate('0.5s')]),
      // #enddocregion trigger-wildcard1
      // #docregion trigger-wildcard2
      transition('open <=> closed', [animate('0.5s')]),
      // #enddocregion trigger-wildcard2
      // #docregion transition4
      transition('* => open', [animate('1s', style({opacity: '*'}))]),
      // #enddocregion transition4
      transition('* => *', [animate('1s')]),
      // #enddocregion trigger-transition
      // #docregion component, trigger-wildcard1, events1
    ]),
  ],
  // #enddocregion trigger-wildcard1
  templateUrl: 'open-close.html',
  styleUrls: ['open-close.css'],
})
// #docregion events
export class OpenClose {
  // #enddocregion events1, events, component
  logging = input(false);
  // #docregion component
  isOpen = true;

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

  // #enddocregion component
  // #docregion events1, events
  onAnimationEvent(event: AnimationEvent) {
    // #enddocregion events1, events
    if (!this.logging) {
      return;
    }
    // #docregion events
    // openClose is trigger name in this example
    console.warn(`Animation Trigger: ${event.triggerName}`);

    // phaseName is "start" or "done"
    console.warn(`Phase: ${event.phaseName}`);

    // in our example, totalTime is 1000 (number of milliseconds in a second)
    console.warn(`Total time: ${event.totalTime}`);

    // in our example, fromState is either "open" or "closed"
    console.warn(`From: ${event.fromState}`);

    // in our example, toState either "open" or "closed"
    console.warn(`To: ${event.toState}`);

    // the HTML element itself, the button in this case
    console.warn(`Element: ${event.element}`);
    // #docregion events1
  }
  // #docregion component
}
// #enddocregion component

همین رفتار را می‌توان به‌صورت بومی با classهای CSS و با استفاده از animation مبتنی بر keyframe یا styleهای transition پیاده‌سازی کرد.

با CSS بومی

animations.css
/* #docregion animation-shared */
@keyframes sharedAnimation {
  to {
    height: 0;
    opacity: 1;
    background-color: 'red';
  }
}

.animated-class {
  animation: sharedAnimation 1s;
}
/* #enddocregion animation-shared */

/* #docregion animation-states */

.open {
  height: '200px';
  opacity: 1;
  background-color: 'yellow';
  transition: all 1s;
}

.closed {
  height: '100px';
  opacity: 0.8;
  background-color: 'blue';
  transition: all 1s;
}

/* #enddocregion animation-states */

/* #docregion animation-timing */

.example-element {
  animation-duration: 1s;
  animation-delay: 500ms;
  animation-timing-function: ease-in-out;
}

.example-shorthand {
  animation: exampleAnimation 1s ease-in-out 500ms;
}

/* #enddocregion animation-timing */

/* #docregion transition-timing */

.example-element {
  transition-duration: 1s;
  transition-delay: 500ms;
  transition-timing-function: ease-in-out;
  transition-property: margin-right;
}

.example-shorthand {
  transition: margin-right 1s ease-in-out 500ms;
}

/* #enddocregion transition-timing */

فعال‌کردن state مربوط به open یا closed با تغییر classهای element در component انجام می‌شود. نمونه‌های انجام این کار را در راهنمای template ببینید.

در راهنمای template نمونه‌های مشابهی برای animate کردن مستقیم styleها نیز وجود دارد.

transitionها، زمان‌بندی و easing

تابع animate() در بسته animations امکان ارائه تنظیمات زمان‌بندی مانند duration،‏ delay و easing را می‌دهد. این کار با چند property یا propertyهای shorthand در CSS بومی نیز امکان‌پذیر است.

برای animation مبتنی بر keyframe در CSS،‏ animation-duration،‏ animation-delay و animation-timing-function را مشخص کنید یا به‌جای آن property مربوط به shorthand یعنی animation را به‌کار ببرید.

animations.css
/* #docregion animation-shared */
@keyframes sharedAnimation {
  to {
    height: 0;
    opacity: 1;
    background-color: 'red';
  }
}

.animated-class {
  animation: sharedAnimation 1s;
}
/* #enddocregion animation-shared */

/* #docregion animation-states */

.open {
  height: '200px';
  opacity: 1;
  background-color: 'yellow';
  transition: all 1s;
}

.closed {
  height: '100px';
  opacity: 0.8;
  background-color: 'blue';
  transition: all 1s;
}

/* #enddocregion animation-states */

/* #docregion animation-timing */

.example-element {
  animation-duration: 1s;
  animation-delay: 500ms;
  animation-timing-function: ease-in-out;
}

.example-shorthand {
  animation: exampleAnimation 1s ease-in-out 500ms;
}

/* #enddocregion animation-timing */

/* #docregion transition-timing */

.example-element {
  transition-duration: 1s;
  transition-delay: 500ms;
  transition-timing-function: ease-in-out;
  transition-property: margin-right;
}

.example-shorthand {
  transition: margin-right 1s ease-in-out 500ms;
}

/* #enddocregion transition-timing */

به‌طور مشابه، برای animationهایی که از @keyframes استفاده نمی‌کنند می‌توانید transition-duration،‏ transition-delay،‏ transition-timing-function و shorthand مربوط به transition را به‌کار ببرید.

animations.css
/* #docregion animation-shared */
@keyframes sharedAnimation {
  to {
    height: 0;
    opacity: 1;
    background-color: 'red';
  }
}

.animated-class {
  animation: sharedAnimation 1s;
}
/* #enddocregion animation-shared */

/* #docregion animation-states */

.open {
  height: '200px';
  opacity: 1;
  background-color: 'yellow';
  transition: all 1s;
}

.closed {
  height: '100px';
  opacity: 0.8;
  background-color: 'blue';
  transition: all 1s;
}

/* #enddocregion animation-states */

/* #docregion animation-timing */

.example-element {
  animation-duration: 1s;
  animation-delay: 500ms;
  animation-timing-function: ease-in-out;
}

.example-shorthand {
  animation: exampleAnimation 1s ease-in-out 500ms;
}

/* #enddocregion animation-timing */

/* #docregion transition-timing */

.example-element {
  transition-duration: 1s;
  transition-delay: 500ms;
  transition-timing-function: ease-in-out;
  transition-property: margin-right;
}

.example-shorthand {
  transition: margin-right 1s ease-in-out 500ms;
}

/* #enddocregion transition-timing */

فعال‌کردن animation

بسته animations نیازمند تعیین triggerها با تابع trigger() و قراردادن تمام stateها درون آن بود. در CSS بومی نیازی به این کار نیست. animationها با تغییر styleها یا classهای CSS فعال می‌شوند. به‌محض قرارگرفتن یک class روی element،‏ animation اجرا می‌شود. حذف class،‏ element را به CSS تعریف‌شده برای آن بازمی‌گرداند. در نتیجه برای اجرای همان animation به کد بسیار کمتری نیاز است. مثالی را ببینید:

با بسته Animations

ts
import {Component, signal} from '@angular/core';
import {trigger, transition, state, animate, style, keyframes} from '@angular/animations';

@Component({
  selector: 'app-open-close',
  animations: [
    trigger('openClose', [
      state(
        'open',
        style({
          height: '200px',
          opacity: 1,
          backgroundColor: 'yellow',
        }),
      ),
      state(
        'closed',
        style({
          height: '100px',
          opacity: 0.5,
          backgroundColor: 'green',
        }),
      ),
      // ...
      transition('* => *', [
        animate(
          '1s',
          keyframes([
            style({opacity: 0.1, offset: 0.1}),
            style({opacity: 0.6, offset: 0.2}),
            style({opacity: 1, offset: 0.5}),
            style({opacity: 0.2, offset: 0.7}),
          ]),
        ),
      ]),
    ]),
  ],
  templateUrl: 'open-close.html',
  styleUrl: 'open-close.css',
})
export class OpenClose {
  isOpen = signal(false);

  toggle() {
    this.isOpen.update((isOpen) => !isOpen);
  }
}
html
<!-- #docplaster -->
<nav>
  <button type="button" (click)="toggle()">Toggle Open/Close</button>
</nav>

<div [@openClose]="isOpen() ? 'open' : 'closed'" class="open-close-container">
  <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>
</div>
css
:host {
  display: block;
  margin-top: 1rem;
}

.open-close-container {
  border: 1px solid #dddddd;
  margin-top: 1em;
  padding: 20px 20px 0px 20px;
  color: #000000;
  font-weight: bold;
  font-size: 20px;
}

با CSS بومی

ts
// #docplaster
import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-open-close',
  templateUrl: 'open-close.html',
  styleUrls: ['open-close.css'],
})
export class OpenClose {
  isOpen = signal(true);
  toggle() {
    this.isOpen.update((isOpen) => !isOpen);
  }
}
html
<!-- #docplaster -->
<h2>Open / Close Example</h2>

<button type="button" class="toggle-btn" (click)="toggle()">Toggle Open/Close</button>

<div class="open-close-container" [class.open]="isOpen()">
  <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>
</div>
css
:host {
  display: block;
  margin-top: 1rem;
}

.open-close-container {
  border: 1px solid #dddddd;
  margin-top: 1em;
  padding: 20px 20px 0px 20px;
  font-weight: bold;
  font-size: 20px;
  height: 100px;
  opacity: 0.8;
  background: #3b82f6;
  color: #ebebeb;
  transition-property: height, opacity, background-color, color;
  transition-duration: 1s;
}

.toggle-btn {
  background: transparent;
  border: 1px solid var(--primary-contrast, black);
  color: var(--primary-contrast, black);
  padding: 10px 24px;
  border-radius: 8px;
  cursor: pointer;
}

.open {
  transition-duration: 0.5s;
  height: 200px;
  opacity: 1;
  background: #475569;
  color: #f9fafb;
}

transitionها و triggerها

stateهای ازپیش‌تعریف‌شده و تطبیق wildcard

بسته animations امکان تطبیق stateهای تعریف‌شده با یک transition از طریق رشته‌ها را فراهم می‌کند. برای مثال، animation از open به closed به‌شکل open => closed نوشته می‌شود. می‌توانید با wildcard هر state را به یک state مقصد تطبیق دهید، مانند * => closed؛ keyword مربوط به void نیز برای stateهای ورود و خروج به‌کار می‌رود. برای مثال، * => void برای خروج element از view و void => * برای ورود آن به view استفاده می‌شود.

هنگام animate کردن مستقیم با CSS، به این الگوهای تطبیق state نیازی نیست. بر اساس classها یا styleهایی که روی elementها تنظیم می‌کنید، می‌توانید transitionها و animationهای @keyframes قابل‌اعمال را مدیریت کنید. همچنین می‌توانید برای کنترل ظاهر element بلافاصله پس از ورود به DOM از @starting-style استفاده کنید.

محاسبه خودکار property با wildcard

بسته animations امکان animate کردن مواردی را فراهم می‌کند که در گذشته دشوار بوده‌اند؛ مانند animate کردن یک height مشخص به height: auto. اکنون می‌توانید این کار را با CSS خالص نیز انجام دهید.

با بسته Animations

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

@Component({
  selector: 'app-open-close',
  animations: [
    trigger('openClose', [
      state('true', style({height: '*'})),
      state('false', style({height: '0px'})),
      transition('false <=> true', animate(1000)),
    ]),
  ],
  templateUrl: 'auto-height.html',
  styleUrl: 'auto-height.css',
})
export class AutoHeight {
  isOpen = signal(false);

  toggle() {
    this.isOpen.update((isOpen) => !isOpen);
  }
}
html
<!-- #docplaster -->
<h2>Auto Height Example</h2>

<button type="button" (click)="toggle()">Toggle Open/Close</button>

<div class="container" [@openClose]="isOpen() ? true : false">
  <div class="content">
    <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>
  </div>
</div>
css
.container {
  display: block;
  overflow: hidden;
}

.container .content {
  padding: 20px;
  margin-top: 1em;
  font-weight: bold;
  font-size: 20px;
  background: #3b82f6;
  color: #ebebeb;
}

برای animate کردن به height خودکار می‌توانید از CSS Grid استفاده کنید.

با CSS بومی

ts
// #docplaster
import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-auto-height',
  templateUrl: 'auto-height.html',
  styleUrls: ['auto-height.css'],
})
export class AutoHeight {
  isOpen = signal(true);
  toggle() {
    this.isOpen.update((isOpen) => !isOpen);
  }
}
html
<!-- #docplaster -->
<h2>Auto Height Example</h2>

<button type="button" class="toggle-btn" (click)="toggle()">Toggle Open/Close</button>

<div class="container" [class.open]="isOpen()">
  <div class="content">
    <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>
  </div>
</div>
css
.container {
  display: grid;
  grid-template-rows: 0fr;
  overflow: hidden;
  transition: grid-template-rows 1s;
}

.container.open {
  grid-template-rows: 1fr;
}

.container .content {
  min-height: 0;
  transition: visibility 1s;
  padding: 0 20px;
  visibility: hidden;
  margin-top: 1em;
  font-weight: bold;
  font-size: 20px;
  background: #3b82f6;
  color: #ebebeb;
  overflow: hidden;
}

.container.open .content {
  visibility: visible;
}

.toggle-btn {
  background: transparent;
  border: 1px solid var(--primary-contrast, black);
  color: var(--primary-contrast, black);
  padding: 10px 24px;
  border-radius: 8px;
  cursor: pointer;
}

اگر لازم نیست از تمام مرورگرها پشتیبانی کنید، می‌توانید calc-size() را نیز بررسی کنید که راهکار واقعی animate کردن height خودکار است. برای اطلاعات بیشتر مستندات MDN و این آموزش را ببینید.

animate کردن ورود به view و خروج از آن

بسته animations علاوه بر الگوی تطبیق گفته‌شده برای ورود و خروج، aliasهای کوتاه :enter و :leave را نیز ارائه می‌کرد.

با بسته Animations

ts
// #docplaster
import {Component} from '@angular/core';
import {trigger, transition, animate, style} from '@angular/animations';

@Component({
  selector: 'app-insert-remove',
  animations: [
    trigger('myInsertRemoveTrigger', [
      transition(':enter', [style({opacity: 0}), animate('200ms', style({opacity: 1}))]),
      transition(':leave', [animate('200ms', style({opacity: 0}))]),
    ]),
  ],
  templateUrl: 'insert-remove.html',
  styleUrls: ['insert-remove.css'],
})
export class InsertRemove {
  isShown = false;

  toggle() {
    this.isShown = !this.isShown;
  }
}
html
<!-- #docplaster -->

<h2>Insert/Remove</h2>

<nav>
  <button type="button" (click)="toggle()">Toggle Insert/Remove</button>
</nav>

@if (isShown) {
  <div @myInsertRemoveTrigger class="insert-remove-container">
    <p>The box is inserted</p>
  </div>
}
css
:host {
  display: block;
}

.insert-remove-container {
  border: 1px solid #dddddd;
  margin-top: 1em;
  padding: 20px 20px 0px 20px;
  color: #000000;
  font-weight: bold;
  font-size: 20px;
}

با CSS بومی

ts
// #docplaster
import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-insert',
  templateUrl: 'insert.html',
  styleUrls: ['insert.css'],
})
export class Insert {
  isShown = signal(false);

  toggle() {
    this.isShown.update((isShown) => !isShown);
  }
}
html
<!-- #docplaster -->
<h2>Insert Element Example</h2>

<nav>
  <button type="button" class="toggle-btn" (click)="toggle()">Toggle Element</button>
</nav>

@if (isShown()) {
  <div class="insert-container" animate.enter="enter-animation">
    <p>The box is inserted</p>
  </div>
}
css
:host {
  display: block;
}

.insert-container {
  border: 1px solid #dddddd;
  margin-top: 1em;
  padding: 20px;
  font-weight: bold;
  font-size: 20px;
}

.insert-container p {
  margin: 0;
}

.enter-animation {
  animation: slide-fade 1s;
}

@keyframes slide-fade {
  from {
    opacity: 0;
    transform: translateY(20px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.toggle-btn {
  background: transparent;
  border: 1px solid var(--primary-contrast, black);
  color: var(--primary-contrast, black);
  padding: 10px 24px;
  border-radius: 8px;
  cursor: pointer;
}

با CSS بومی

ts
// #docplaster
import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-remove',
  templateUrl: 'remove.html',
  styleUrls: ['remove.css'],
})
export class Remove {
  isShown = signal(false);

  toggle() {
    this.isShown.update((isShown) => !isShown);
  }
}
html
<!-- #docplaster -->
<h2>Remove Element Example</h2>

<nav>
  <button type="button" class="toggle-btn" (click)="toggle()">Toggle Element</button>
</nav>

@if (isShown()) {
  <div class="insert-container" animate.leave="deleting">
    <p>The box is inserted</p>
  </div>
}
css
:host {
  display: block;
}

.insert-container {
  border: 1px solid #dddddd;
  margin-top: 1em;
  padding: 20px;
  font-weight: bold;
  font-size: 20px;
  opacity: 1;
  transition: opacity 200ms ease-in;

  @starting-style {
    opacity: 0;
  }
}

.insert-container p {
  margin: 0;
}

.deleting {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity 500ms ease-out,
    transform 500ms ease-out;
}

.toggle-btn {
  background: transparent;
  border: 1px solid var(--primary-contrast, black);
  color: var(--primary-contrast, black);
  padding: 10px 24px;
  border-radius: 8px;
  cursor: pointer;
}

برای اطلاعات بیشتر درباره animate.enter و animate.leave به راهنمای animationهای Enter و Leave مراجعه کنید.

animate کردن افزایش و کاهش

علاوه بر :enter و :leave گفته‌شده،‏ :increment و :decrement نیز وجود دارند. این موارد را نیز می‌توانید با افزودن و حذف classها animate کنید. برخلاف aliasهای داخلی بسته animation، با افزایش یا کاهش مقدار، classها به‌طور خودکار اعمال نمی‌شوند. می‌توانید class مناسب را به‌صورت programmatic اعمال کنید. مثالی را ببینید:

با بسته Animations

ts
// #docplaster
// #docregion
import {Component, signal} from '@angular/core';
import {trigger, transition, animate, style, query, stagger} from '@angular/animations';

@Component({
  selector: 'app-increment-decrement',
  templateUrl: 'increment-decrement.html',
  styleUrls: ['increment-decrement.css'],
  animations: [
    trigger('incrementAnimation', [
      transition(':increment', [
        animate('300ms ease-out', style({color: 'green', transform: 'scale(1.3, 1.2)'})),
      ]),
      transition(':decrement', [
        animate('300ms ease-out', style({color: 'red', transform: 'scale(0.8, 0.9)'})),
      ]),
    ]),
  ],
})
export class IncrementDecrement {
  num = signal(0);

  modify(n: number) {
    this.num.update((v) => (v += n));
  }
}
html
<h3>Increment and Decrement Example</h3>
<section>
  <p [@incrementAnimation]="num()">Number {{ num() }}</p>
  <div class="controls">
    <button type="button" (click)="modify(1)">+</button>
    <button type="button" (click)="modify(-1)">-</button>
  </div>
</section>
css
:host {
  display: block;
  font-size: 32px;
  margin: 20px;
  text-align: center;
}

section {
  border: 1px solid lightgray;
  border-radius: 50px;
}

p {
  display: inline-block;
  margin: 2rem 0;
  text-transform: uppercase;
}

.controls {
  padding-bottom: 2rem;
}

button {
  font: inherit;
  border: 0;
  background: lightgray;
  width: 50px;
  border-radius: 10px;
}

button + button {
  margin-left: 10px;
}

با CSS بومی

ts
// #docplaster
// #docregion
import {Component, ElementRef, OnInit, signal, viewChild} from '@angular/core';

@Component({
  selector: 'app-increment-decrement',
  templateUrl: 'increment-decrement.html',
  styleUrls: ['increment-decrement.css'],
})
export class IncrementDecrement implements OnInit {
  num = signal(0);
  el = viewChild<ElementRef<HTMLParagraphElement>>('el');

  ngOnInit() {
    this.el()?.nativeElement.addEventListener('animationend', (ev) => {
      if (ev.animationName.endsWith('decrement') || ev.animationName.endsWith('increment')) {
        this.animationFinished();
      }
    });
  }

  modify(n: number) {
    const targetClass = n > 0 ? 'increment' : 'decrement';
    this.num.update((v) => (v += n));
    this.el()?.nativeElement.classList.add(targetClass);
  }

  animationFinished() {
    this.el()?.nativeElement.classList.remove('increment', 'decrement');
  }

  ngOnDestroy() {
    this.el()?.nativeElement.removeEventListener('animationend', this.animationFinished);
  }
}
html
<h3>Increment and Decrement Example</h3>
<section>
  <p #el>Number {{ num() }}</p>
  <div class="controls">
    <button type="button" (click)="modify(1)">+</button>
    <button type="button" (click)="modify(-1)">-</button>
  </div>
</section>
css
:host {
  display: block;
  font-size: 32px;
  margin: 20px;
  text-align: center;
}

section {
  border: 1px solid lightgray;
  border-radius: 50px;
}

p {
  display: inline-block;
  margin: 2rem 0;
  text-transform: uppercase;
}

.increment {
  animation: increment 300ms;
}

.decrement {
  animation: decrement 300ms;
}

.controls {
  padding-bottom: 2rem;
}

button {
  font: inherit;
  border: 0;
  background: lightgray;
  width: 50px;
  border-radius: 10px;
}

button + button {
  margin-left: 10px;
}

@keyframes increment {
  33% {
    color: green;
    transform: scale(1.3, 1.2);
  }
  66% {
    color: green;
    transform: scale(1.2, 1.2);
  }
  100% {
    transform: scale(1, 1);
  }
}

@keyframes decrement {
  33% {
    color: red;
    transform: scale(0.8, 0.9);
  }
  66% {
    color: red;
    transform: scale(0.9, 0.9);
  }
  100% {
    transform: scale(1, 1);
  }
}

animationهای والد و فرزند

برخلاف بسته animations، وقتی چند animation در یک component مشخص شده‌اند، هیچ animation بر دیگری اولویت ندارد و چیزی مانع آغاز animationها نمی‌شود. هرگونه توالی animation باید در تعریف animation مربوط به CSS و با استفاده از delay در animation یا transition، یا با استفاده از animationend یا transitionend برای افزودن CSS بعدی که باید animate شود، مدیریت شود.

غیرفعال‌کردن یک animation یا تمام animationها

برای غیرفعال‌کردن animationهای تعریف‌شده در CSS بومی، چند گزینه دارید.

  1. یک class سفارشی بسازید که animation و transition را به none وادار کند.
css
.no-animation {
  animation: none !important;
  transition: none !important;
}

اعمال این class روی یک element از آغاز هر animation روی آن جلوگیری می‌کند. همچنین می‌توانید scope آن را به کل DOM یا بخشی از DOM گسترش دهید تا این رفتار اعمال شود. بااین‌حال، این کار مانع فعال‌شدن eventهای animation می‌شود. اگر برای حذف element منتظر eventهای animation هستید، این راهکار کار نمی‌کند. یک راه‌حل جایگزین، تنظیم durationها روی یک میلی‌ثانیه است.

  1. با media query مربوط به prefers-reduced-motion مطمئن شوید برای کاربرانی که حرکت کمتر را ترجیح می‌دهند، animation اجرا نمی‌شود.
  1. از افزودن programmatic کلاس‌های animation جلوگیری کنید.

callbackهای animation

بسته animations برای زمانی که می‌خواهید پس از پایان animation کاری انجام دهید، callbackهایی در اختیار شما قرار می‌داد. animationهای بومی CSS نیز این callbackها را دارند.

OnAnimationStart OnAnimationEnd OnAnimationIteration OnAnimationCancel

OnTransitionStart OnTransitionRun OnTransitionEnd OnTransitionCancel

Web Animations API قابلیت‌های بسیار بیشتری دارد. برای مشاهده تمام APIهای animation موجود، مستندات آن را بررسی کنید.

توالی‌های پیچیده

بسته animations قابلیت داخلی ساخت توالی‌های پیچیده را دارد. تمام این توالی‌ها بدون بسته animations نیز کاملاً قابل‌پیاده‌سازی هستند.

هدف‌گرفتن elementهای مشخص

در بسته animations می‌توانستید با تابع query()، مشابه document.querySelector()،‏ elementهای مشخص را بر اساس نام class در CSS پیدا و هدف‌گیری کنید. در دنیای animationهای بومی CSS نیازی به این کار نیست. در عوض، با selectorهای CSS می‌توانید subclassها را هدف بگیرید و transform یا animation دلخواه را اعمال کنید.

برای تغییر classهای nodeهای فرزند درون یک template می‌توانید از bindingهای class و style استفاده کنید تا animationها در نقطه مناسب افزوده شوند.

Stagger()

تابع stagger() به شما امکان می‌داد animation هر آیتم در فهرست را به‌اندازه زمان مشخصی به تأخیر بیندازید تا جلوه آبشاری ایجاد شود. می‌توانید این رفتار را با استفاده از animation-delay یا transition-delay در CSS بومی بازسازی کنید. نمونه‌ای از چنین CSS در ادامه آمده است.

با بسته Animations

ts
// #docplaster
// #docregion
import {Component, HostBinding, signal} from '@angular/core';
import {trigger, transition, animate, style, query, stagger} from '@angular/animations';

@Component({
  selector: 'app-stagger',
  templateUrl: 'stagger.html',
  styleUrls: ['stagger.css'],
  animations: [
    trigger('pageAnimations', [
      transition(':enter', [
        query('.item', [
          style({opacity: 0, transform: 'translateY(-10px)'}),
          stagger(200, [animate('500ms ease-in', style({opacity: 1, transform: 'none'}))]),
        ]),
      ]),
    ]),
  ],
})
export class Stagger {
  @HostBinding('@pageAnimations')
  items = [1, 2, 3];
}
html
<!-- #docplaster -->
<h2>Stagger Example</h2>

<ul class="items">
  @for (item of items; track item) {
    <li class="item">{{ item }}</li>
  }
</ul>
css
.items {
  list-style: none;
  padding: 0;
  margin: 0;
}

با CSS بومی

ts
// #docplaster
import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-stagger',
  templateUrl: './stagger.html',
  styleUrls: ['stagger.css'],
})
export class Stagger {
  show = signal(true);
  items = [1, 2, 3];

  refresh() {
    this.show.set(false);
    setTimeout(() => {
      this.show.set(true);
    }, 10);
  }
}
html
<!-- #docplaster -->
<h1>Stagger Example</h1>
<button type="button" class="toggle-btn" (click)="refresh()">Refresh</button>
<div class="items-container">
  @if (show()) {
    <ul class="items">
      @for (item of items; track $index) {
        <li class="item" style="--index: {{ $index }}">{{ item }}</li>
      }
    </ul>
  }
</div>
css
.items-container {
  min-height: 4.5rem;
}

.items {
  list-style: none;
  padding: 0;
  margin: 0;
}

.items .item {
  transition-property: opacity, transform;
  transition-duration: 500ms;
  transition-delay: calc(200ms * var(--index));

  @starting-style {
    opacity: 0;
    transform: translateX(-10px);
  }
}

.toggle-btn {
  background: transparent;
  border: 1px solid var(--primary-contrast, black);
  color: var(--primary-contrast, black);
  padding: 10px 24px;
  border-radius: 8px;
  cursor: pointer;
}

animationهای موازی

بسته animations تابع group() را برای اجرای هم‌زمان چند animation ارائه می‌کند. در CSS کنترل کاملی بر زمان‌بندی animation دارید. اگر چند animation تعریف شده باشد، می‌توانید همه را به‌طور هم‌زمان اعمال کنید.

css
.target-element {
  animation:
    rotate 3s,
    fade-in 2s;
}

در این مثال animationهای rotate و fade-in هم‌زمان اجرا می‌شوند.

animate کردن آیتم‌های یک فهرست مرتب‌شونده

مرتب‌سازی مجدد آیتم‌های فهرست با تکنیک‌های گفته‌شده به‌صورت پیش‌فرض کار می‌کند و اقدام ویژه دیگری لازم نیست. آیتم‌های حلقه @for به‌درستی حذف و دوباره اضافه می‌شوند و این کار animationهای ورود مبتنی بر @starting-styles را فعال می‌کند. همچنین می‌توانید برای همین رفتار از animate.enter استفاده کنید. همان‌طور که در مثال بالا دیدید، برای animate کردن elementها هنگام حذف از animate.leave استفاده کنید.

با بسته Animations

ts
// #docplaster
import {Component, signal} from '@angular/core';
import {trigger, transition, animate, query, style} from '@angular/animations';

@Component({
  selector: 'app-reorder',
  templateUrl: './reorder.html',
  styleUrls: ['reorder.css'],
  animations: [
    trigger('itemAnimation', [
      transition(':enter', [
        style({opacity: 0, transform: 'translateX(-10px)'}),
        animate('300ms', style({opacity: 1, transform: 'translateX(none)'})),
      ]),
      transition(':leave', [
        style({opacity: 1, transform: 'translateX(none)'}),
        animate('300ms', style({opacity: 0, transform: 'translateX(-10px)'})),
      ]),
    ]),
  ],
})
export class Reorder {
  show = signal(true);
  items = ['stuff', 'things', 'cheese', 'paper', 'scissors', 'rock'];

  randomize() {
    const randItems = [...this.items];
    const newItems = [];
    for (let i of this.items) {
      const max: number = this.items.length - newItems.length;
      const randNum = Math.floor(Math.random() * max);
      newItems.push(...randItems.splice(randNum, 1));
    }

    this.items = newItems;
  }
}
html
<!-- #docplaster -->
<h1>Reordering List Example</h1>
<button type="button" (click)="randomize()">Randomize</button>

<ul class="items">
  @for (item of items; track item) {
    <li @itemAnimation class="item">{{ item }}</li>
  }
</ul>
css
.items {
  list-style: none;
  padding: 0;
  margin: 0;
}

با CSS بومی

ts
// #docplaster
import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-reorder',
  templateUrl: './reorder.html',
  styleUrls: ['reorder.css'],
})
export class Reorder {
  show = signal(true);
  items = ['stuff', 'things', 'cheese', 'paper', 'scissors', 'rock'];

  randomize() {
    const randItems = [...this.items];
    const newItems = [];
    for (let i of this.items) {
      const max: number = this.items.length - newItems.length;
      const randNum = Math.floor(Math.random() * max);
      newItems.push(...randItems.splice(randNum, 1));
    }

    this.items = newItems;
  }
}
html
<!-- #docplaster -->
<h1>Reordering List Example</h1>
<button type="button" class="toggle-btn" (click)="randomize()">Randomize</button>

<ul class="items">
  @for (item of items; track item) {
    <li class="item" animate.leave="fade">{{ item }}</li>
  }
</ul>
css
.items {
  list-style: none;
  padding: 0;
  margin: 0;
}

.items .item {
  transition-property: opacity, transform;
  transition-duration: 500ms;

  @starting-style {
    opacity: 0;
    transform: translateX(-10px);
  }
}

.items .item.fade {
  animation: fade-out 500ms;
}

@keyframes fade-out {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

.toggle-btn {
  background: transparent;
  border: 1px solid var(--primary-contrast, black);
  color: var(--primary-contrast, black);
  padding: 10px 24px;
  border-radius: 8px;
  cursor: pointer;
}

مهاجرت کاربردهای AnimationPlayer

class مربوط به AnimationPlayer امکان دسترسی به animation و انجام کارهای پیشرفته‌تری مانند pause،‏ play،‏ restart و finish کردن animation از طریق کد را فراهم می‌کند. تمام این کارها را می‌توان به‌صورت بومی نیز انجام داد.

می‌توانید animationهای یک element را مستقیماً با Element.getAnimations() دریافت کنید. این method آرایه‌ای از تمام Animationهای روی آن element برمی‌گرداند. با API مربوط به Animation می‌توانید کارهایی بسیار بیشتر از قابلیت‌های AnimationPlayer در بسته animations انجام دهید. از اینجا می‌توانید cancel()،‏ play()،‏ pause()،‏ reverse() و بسیاری موارد دیگر را فراخوانی کنید. این API بومی باید تمام امکانات لازم برای کنترل animationها را فراهم کند.

transitionهای route

برای animate کردن میان routeها می‌توانید از view transition استفاده کنید. برای شروع به راهنمای animationهای transition در Route مراجعه کنید.