آنلاین

transitionها و triggerهای animation

این راهنما stateهای ویژه transition مانند wildcard با نماد * و void را به‌تفصیل بررسی می‌کند. همچنین نشان می‌دهد این stateها چگونه برای elementهای در حال ورود به view یا خروج از آن استفاده می‌شوند. در این بخش چند trigger مربوط به animation،‏ callbackهای animation و animation مبتنی بر توالی با استفاده از keyframe نیز بررسی می‌شوند.

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

در Angular می‌توان stateهای transition را به‌طور صریح با تابع state()، یا با stateهای ازپیش‌تعریف‌شده wildcard یعنی * و void تعریف کرد.

state از نوع wildcard

ستاره * یا wildcard با هر state مربوط به animation تطبیق دارد. این ویژگی برای تعریف transitionهایی مفید است که مستقل از state آغاز یا پایان element در HTML اعمال می‌شوند.

برای مثال، transition مربوط به open => * زمانی اعمال می‌شود که state یک element از open به هر مقدار دیگری تغییر کند.

عبارت‌های state از نوع wildcard

نمونه کد زیر نیز با استفاده از state از نوع wildcard، مثال قبلی stateهای open و closed را ادامه می‌دهد. به‌جای تعریف تمام جفت‌های transition میان stateها، هر transition به closed یک ثانیه و هر transition به open نیم ثانیه طول می‌کشد.

به این ترتیب می‌توان stateهای جدید را بدون افزودن transition جداگانه برای هرکدام اضافه کرد.

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

برای مشخص‌کردن transition میان stateها در هر دو جهت، از syntax پیکان دوتایی استفاده کنید.

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

استفاده از state از نوع wildcard همراه با چند state مربوط به transition

در مثال button دوحالته، wildcard چندان مفید نیست؛ زیرا فقط دو state ممکن یعنی open و closed وجود دارد. به‌طور کلی، زمانی از stateهای wildcard استفاده کنید که یک element چند state احتمالی برای تغییر داشته باشد. اگر button بتواند از open به closed یا مقداری مانند inProgress تغییر کند، استفاده از wildcard می‌تواند حجم کد لازم را کاهش دهد.

state از نوع wildcard با ۳ state
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

transition مربوط به * => * هنگام هر تغییر میان دو state اعمال می‌شود.

transitionها به‌ترتیب تعریف‌شدن تطبیق داده می‌شوند. بنابراین می‌توانید transitionهای دیگری را روی transition مربوط به * => * اعمال کنید. برای مثال، تغییرهای style یا animationهایی را تعریف کنید که فقط بر open => closed اعمال شوند و سپس * => * را برای جفت stateهایی که به‌طور مشخص بیان نشده‌اند، به‌عنوان fallback به‌کار ببرید.

برای این کار، transitionهای خاص‌تر را پیش از * => * فهرست کنید.

استفاده از wildcard همراه با style

از wildcard یعنی * همراه با یک style استفاده کنید تا animation مقدار فعلی آن style را به‌کار بگیرد و بر اساس آن animate شود. wildcard مقدار fallback است که اگر state در حال animate در trigger اعلام نشده باشد، استفاده می‌شود.

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

state از نوع void

از state مربوط به void برای پیکربندی transition یک element هنگام ورود به صفحه یا خروج از آن استفاده کنید. animate کردن ورود به view و خروج از آن را ببینید.

ترکیب stateهای wildcard و void

stateهای wildcard و void را در یک transition ترکیب کنید تا animationهای ورود به صفحه و خروج از آن فعال شوند:

  • transition مربوط به * => void هنگام خروج element از view، مستقل از state قبلی آن، اعمال می‌شود.
  • transition مربوط به void => * هنگام ورود element به view، مستقل از state آن هنگام ورود، اعمال می‌شود.
  • state از نوع wildcard یعنی * با هر state از جمله void تطبیق دارد.

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

این بخش نحوه animate کردن elementها هنگام ورود به صفحه یا خروج از آن را نشان می‌دهد.

رفتار جدیدی اضافه کنید:

  • وقتی یک hero به فهرست heroها اضافه می‌شود، به نظر می‌رسد از سمت چپ به داخل صفحه پرواز می‌کند.
  • وقتی یک hero را حذف می‌کنید، به نظر می‌رسد به سمت راست از صفحه پرواز می‌کند.
hero-list-enter-leave.ts
import {Component, input, output} from '@angular/core';
import {trigger, state, style, animate, transition} from '@angular/animations';

import {Hero} from './hero';

@Component({
  selector: 'app-hero-list-enter-leave',
  template: `
    <ul class="heroes">
      @for (hero of heroes(); track hero) {
        <li [@flyInOut]="'in'">
          <button class="inner" type="button" (click)="removeHero(hero.id)">
            <span class="badge">{{ hero.id }}</span>
            <span class="name">{{ hero.name }}</span>
          </button>
        </li>
      }
    </ul>
  `,
  styleUrls: ['./hero-list-page.css'],
  // #docregion animationdef
  animations: [
    trigger('flyInOut', [
      state('in', style({transform: 'translateX(0)'})),
      transition('void => *', [style({transform: 'translateX(-100%)'}), animate(100)]),
      transition('* => void', [animate(100, style({transform: 'translateX(100%)'}))]),
    ]),
  ],
  // #enddocregion animationdef
})
export class HeroListEnterLeave {
  readonly heroes = input<Hero[]>([]);

  readonly remove = output<number>();

  removeHero(id: number) {
    this.remove.emit(id);
  }
}

در کد بالا، هنگامی که element در HTML به view متصل نیست، state مربوط به void اعمال شده است.

aliasهای :enter و :leave

:enter و :leave به‌ترتیب aliasهای transitionهای void => * و * => void هستند. چندین تابع animation از این aliasها استفاده می‌کنند.

ts
transition ( ':enter', [ … ] ); // alias for void => _
transition ( ':leave', [ … ] ); // alias for _ => void

هدف‌گرفتن element در حال ورود به view دشوارتر است، زیرا هنوز در DOM وجود ندارد. برای هدف‌گرفتن elementهای HTML که در view درج یا از آن حذف می‌شوند، از aliasهای :enter و :leave استفاده کنید.

استفاده از *ngIf و *ngFor همراه با :enter و :leave

transition مربوط به :enter هنگام قرارگرفتن هر view مربوط به *ngIf یا *ngFor در صفحه اجرا می‌شود و :leave هنگام حذف آن viewها از صفحه اجرا خواهد شد.

به‌عنوان یک قاعده کلی، هر element که Angular به DOM اضافه می‌کند از transition مربوط به :enter عبور می‌کند. تنها elementهایی که Angular مستقیماً از DOM حذف می‌کند از transition مربوط به :leave عبور می‌کنند. برای مثال، ممکن است view یک element به این دلیل از DOM حذف شود که والد آن در حال حذف‌شدن از DOM است.

این مثال trigger ویژه‌ای برای animation ورود و خروج با نام myInsertRemoveTrigger دارد. template مربوط به HTML شامل کد زیر است.

insert-remove.html
<!-- #docplaster -->

<h2>Insert/Remove</h2>

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

<!-- #docregion insert-remove-->
@if (isShown) {
  <div @myInsertRemoveTrigger class="insert-remove-container">
    <p>The box is inserted</p>
  </div>
}
<!-- #enddocregion insert-remove-->

در فایل component،‏ transition مربوط به :enter مقدار اولیه opacity را ۰ تنظیم می‌کند. سپس هم‌زمان با درج element در view، آن را animate می‌کند تا opacity به ۱ تغییر کند.

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

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

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

توجه کنید که این مثال نیازی به استفاده از state() ندارد.

transitionهای :increment و :decrement

تابع transition() مقادیر selector دیگری یعنی :increment و :decrement را نیز می‌پذیرد. از این موارد برای آغاز transition هنگام افزایش یا کاهش یک مقدار عددی استفاده کنید.

برای اطلاعات بیشتر درباره این methodها، صفحه توالی‌های پیچیده را ببینید.

hero-list-page.ts
// #docplaster
// #docregion
import {Component, HostBinding, OnInit} from '@angular/core';
import {trigger, transition, animate, style, query, stagger} from '@angular/animations';
import {HEROES} from './mock-heroes';
import {Hero} from './hero';

// #docregion filter-animations
@Component({
  // #enddocregion filter-animations
  selector: 'app-hero-list-page',
  templateUrl: 'hero-list-page.html',
  styleUrls: ['hero-list-page.css'],
  // #docregion page-animations, filter-animations
  animations: [
    // #enddocregion filter-animations
    trigger('pageAnimations', [
      transition(':enter', [
        query('.hero', [
          style({opacity: 0, transform: 'translateY(-100px)'}),
          stagger(30, [
            animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({opacity: 1, transform: 'none'})),
          ]),
        ]),
      ]),
    ]),
    // #enddocregion page-animations
    // #docregion increment
    // #docregion filter-animations
    trigger('filterAnimation', [
      transition(':enter, * => 0, * => -1', []),
      transition(':increment', [
        query(
          ':enter',
          [
            style({opacity: 0, width: 0}),
            stagger(50, [animate('300ms ease-out', style({opacity: 1, width: '*'}))]),
          ],
          {optional: true},
        ),
      ]),
      transition(':decrement', [
        query(':leave', [stagger(50, [animate('300ms ease-out', style({opacity: 0, width: 0}))])]),
      ]),
    ]),
    // #enddocregion  increment
  ],
})
export class HeroListPage implements OnInit {
  // #enddocregion filter-animations
  @HostBinding('@pageAnimations')
  public animatePage = true;

  // #docregion filter-animations
  heroesTotal = -1;

  get heroes() {
    return this._heroes;
  }
  private _heroes: Hero[] = [];

  ngOnInit() {
    this._heroes = HEROES;
  }

  updateCriteria(criteria: string) {
    criteria = criteria ? criteria.trim() : '';

    this._heroes = HEROES.filter((hero) =>
      hero.name.toLowerCase().includes(criteria.toLowerCase()),
    );
    const newTotal = this.heroes.length;

    if (this.heroesTotal !== newTotal) {
      this.heroesTotal = newTotal;
    } else if (!criteria) {
      this.heroesTotal = -1;
    }
  }
}
// #enddocregion filter-animations

مقادیر Boolean در transitionها

اگر trigger یک مقدار Boolean به‌عنوان مقدار binding داشته باشد، می‌توان آن را با عبارت transition() که true و false یا 1 و 0 را مقایسه می‌کند تطبیق داد.

open-close.html
<!-- #docplaster -->
<nav>
  <button type="button" (click)="toggle()">Toggle Boolean/Close</button>
</nav>

<!-- #docregion trigger-boolean -->
<div [@openClose]="isOpen ? true : false" class="open-close-container">
  <!-- #enddocregion trigger-boolean -->
  <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
  <!-- #docregion trigger-boolean -->
</div>
<!-- #enddocregion trigger-boolean -->

در قطعه‌کد بالا، template مربوط به HTML یک element از نوع <div> را با عبارت state مربوط به isOpen و مقادیر ممکن true و false، به triggerای با نام openClose متصل می‌کند. این الگو جایگزینی برای ساخت دو state نام‌دار مانند open و close است.

در metadata مربوط به @Component زیر property مربوط به animations:، وقتی state به true ارزیابی می‌شود، ارتفاع element مرتبط در HTML یک style از نوع wildcard یا مقدار پیش‌فرض است. در این حالت animation از همان ارتفاعی استفاده می‌کند که element پیش از شروع animation داشته است. وقتی element در حالت closed است، ارتفاع آن به ۰ animate می‌شود و در نتیجه نامرئی خواهد بود.

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

@Component({
  selector: 'app-open-close-boolean',
  // #docregion trigger-boolean
  animations: [
    trigger('openClose', [
      state('true', style({height: '*'})),
      state('false', style({height: '0px'})),
      transition('false <=> true', animate(500)),
    ]),
  ],
  // #enddocregion trigger-boolean
  templateUrl: 'open-close.2.html',
  styleUrls: ['open-close.css'],
})
export class OpenCloseBooleanComponent {
  isOpen = false;

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

چند trigger مربوط به animation

می‌توانید بیش از یک trigger مربوط به animation برای یک component تعریف کنید. triggerهای animation را به elementهای مختلف متصل کنید؛ رابطه والد و فرزند میان elementها بر نحوه و زمان اجرای animationها تأثیر می‌گذارد.

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

هر بار که یک animation در Angular فعال می‌شود، animation والد همیشه اولویت دارد و animationهای فرزند مسدود می‌شوند. برای اجرای animation فرزند، animation والد باید هر element دارای animation فرزند را query کند و سپس با تابع animateChild() اجازه اجرای animationها را بدهد.

غیرفعال‌کردن animation روی یک element در HTML

یک binding ویژه کنترل animation با نام @.disabled را می‌توان روی element در HTML قرار داد تا animationهای آن element و تمام elementهای تو‌در‌تو خاموش شوند. وقتی مقدار آن true باشد، binding مربوط به @.disabled از render شدن تمام animationها جلوگیری می‌کند.

نمونه کد زیر نحوه استفاده از این قابلیت را نشان می‌دهد.

html
<nav>
  <button type="button" (click)="toggleAnimations()">Toggle Animations</button>
  <button type="button" (click)="toggle()">Toggle Open/Closed</button>
</nav>
<!-- #docregion toggle-animation -->
<div [@.disabled]="isDisabled">
  <div [@childAnimation]="isOpen ? 'open' : 'closed'" class="open-close-container">
    <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
  </div>
</div>
<!-- #enddocregion toggle-animation -->
ts
// #docplaster
// #docregion
import {Component} from '@angular/core';
import {trigger, transition, state, animate, style} from '@angular/animations';

// #docregion toggle-animation
@Component({
  // #enddocregion toggle-animation
  selector: 'app-open-close-toggle',
  templateUrl: 'open-close.4.html',
  styleUrls: ['open-close.css'],
  // #docregion toggle-animation
  animations: [
    trigger('childAnimation', [
      // ...
      // #enddocregion toggle-animation
      state(
        'open',
        style({
          width: '250px',
          opacity: 1,
          backgroundColor: 'yellow',
        }),
      ),
      state(
        'closed',
        style({
          width: '100px',
          opacity: 0.8,
          backgroundColor: 'blue',
        }),
      ),
      transition('* => *', [animate('1s')]),
      // #docregion toggle-animation
    ]),
  ],
})
export class OpenCloseChild {
  isDisabled = false;
  isOpen = false;
  // #enddocregion toggle-animation
  toggleAnimations() {
    this.isDisabled = !this.isDisabled;
  }

  toggle() {
    this.isOpen = !this.isOpen;
  }
  // #docregion toggle-animation
}
// #enddocregion toggle-animation

وقتی binding مربوط به @.disabled مقدار true دارد، trigger مربوط به @childAnimation آغاز نمی‌شود.

وقتی animationهای یک element درون template مربوط به HTML با host binding به نام @.disabled خاموش شوند، animationهای تمام elementهای داخلی نیز خاموش می‌شوند. نمی‌توانید چند animation را روی یک element به‌صورت انتخابی خاموش کنید.<!-- vale off -->

بااین‌حال، animationهای انتخابی فرزند را می‌توان به یکی از روش‌های زیر روی والد غیرفعال اجرا کرد:

آن elementها همچنان می‌توانند animate شوند.

  • animation والد می‌تواند با تابع query()،‏ elementهای داخلی قرارگرفته در بخش‌های غیرفعال template مربوط به HTML را جمع‌آوری کند.
  • animation فرزند می‌تواند توسط والد query شود و سپس با تابع animateChild() animate شود.

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

برای خاموش‌کردن تمام animationهای یک برنامه Angular،‏ host binding با نام @.disabled را روی بالاترین component در Angular قرار دهید.

app.ts
// #docplaster
// #docregion imports
import {Component, HostBinding, inject} from '@angular/core';
import {
  trigger,
  state,
  style,
  animate,
  transition,
  // ...
} from '@angular/animations';

// #enddocregion imports
import {ChildrenOutletContexts, RouterLink, RouterOutlet} from '@angular/router';
import {slideInAnimation} from './animations';

// #docregion decorator, toggle-app-animations, define
@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrls: ['app.css'],
  imports: [RouterLink, RouterOutlet],
  animations: [
    // #enddocregion decorator
    slideInAnimation,
    // #docregion decorator
    // #enddocregion toggle-app-animations, define
    // animation triggers go here
    // #docregion toggle-app-animations, define
  ],
})
// #enddocregion decorator, define
export class AppComponent {
  @HostBinding('@.disabled')
  public animationsDisabled = false;
  // #enddocregion toggle-app-animations

  // #docregion get-route-animations-data
  private contexts = inject(ChildrenOutletContexts);

  getRouteAnimationData() {
    return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation'];
  }
  // #enddocregion get-route-animations-data

  toggleAnimations() {
    this.animationsDisabled = !this.animationsDisabled;
  }
  // #docregion toggle-app-animations
}
// #enddocregion toggle-app-animations

callbackهای animation

تابع trigger() مربوط به animation هنگام شروع و پایان، callback منتشر می‌کند. مثال زیر componentای را نشان می‌دهد که دارای trigger با نام openClose است.

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

در template مربوط به HTML،‏ event مربوط به animation از طریق $event به‌شکل @triggerName.start و @triggerName.done بازگردانده می‌شود؛ triggerName نام trigger مورد استفاده است. در این مثال، trigger با نام openClose به‌شکل زیر ظاهر می‌شود.

open-close.html
<!-- #docplaster -->
<nav>
  <button type="button" (click)="toggle()">Toggle Open/Close</button>
</nav>

<!-- #docregion callbacks -->
<div
  [@openClose]="isOpen ? 'open' : 'closed'"
  (@openClose.start)="onAnimationEvent($event)"
  (@openClose.done)="onAnimationEvent($event)"
  class="open-close-container"
>
  <!-- #enddocregion callbacks -->
  <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
  <!-- #docregion callbacks -->
</div>
<!-- #enddocregion callbacks -->

یکی از کاربردهای احتمالی callbackهای animation، پوشاندن زمان انتظار یک فراخوانی کند API مانند جست‌وجو در database است. برای مثال، می‌توان button با نام InProgress را طوری تنظیم کرد که تا پایان عملیات backend،‏ animation تکرارشونده خودش را اجرا کند.

پس از پایان animation فعلی می‌توان animation دیگری را فراخوانی کرد. برای مثال، پس از تکمیل فراخوانی API،‏ button از state مربوط به inProgress به closed می‌رود.

animation می‌تواند باعث شود کاربر عملیات را سریع‌تر از آنچه واقعاً هست احساس کند.

callbackها می‌توانند ابزار debugging باشند؛ برای مثال همراه با console.warn() برای مشاهده روند برنامه در Developer JavaScript Console مرورگر. قطعه‌کد زیر برای مثال اصلی ــ button دارای دو state مربوط به open و closed ــ خروجی console log ایجاد می‌کند.

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

Keyframeها

برای ساخت animation چندمرحله‌ای که مراحل آن به‌ترتیب اجرا می‌شوند، از keyframe استفاده کنید.

تابع keyframe() در Angular چند تغییر style را در یک بخش زمان‌بندی ممکن می‌کند. برای مثال، button می‌تواند به‌جای محوشدن، در یک بازه زمانی دوثانیه‌ای چند بار تغییر رنگ دهد.

keyframeها

کد این تغییر رنگ می‌تواند به‌شکل زیر باشد.

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

@Component({
  selector: 'app-status-slider',
  templateUrl: 'status-slider.html',
  styleUrls: ['status-slider.css'],
  animations: [
    trigger('slideStatus', [
      state('inactive', style({backgroundColor: 'blue'})),
      state('active', style({backgroundColor: '#754600'})),

      // #docregion keyframesWithOffsets
      transition('* => active', [
        animate(
          '2s',
          keyframes([
            style({backgroundColor: 'blue', offset: 0}),
            style({backgroundColor: 'red', offset: 0.8}),
            style({backgroundColor: '#754600', offset: 1.0}),
          ]),
        ),
      ]),
      transition('* => inactive', [
        animate(
          '2s',
          keyframes([
            style({backgroundColor: '#754600', offset: 0}),
            style({backgroundColor: 'red', offset: 0.2}),
            style({backgroundColor: 'blue', offset: 1.0}),
          ]),
        ),
      ]),
      // #enddocregion keyframesWithOffsets

      // #docregion keyframes
      transition('* => active', [
        animate(
          '2s',
          keyframes([
            style({backgroundColor: 'blue'}),
            style({backgroundColor: 'red'}),
            style({backgroundColor: 'orange'}),
          ]),
        ),
        // #enddocregion keyframes
      ]),
    ]),
  ],
})
export class StatusSlider {
  status: 'active' | 'inactive' = 'inactive';

  toggle() {
    if (this.status === 'active') {
      this.status = 'inactive';
    } else {
      this.status = 'active';
    }
  }
}

Offset

keyframeها شامل یک offset هستند که نقطه وقوع هر تغییر style در animation را تعریف می‌کند. offsetها معیارهایی نسبی از صفر تا یک هستند که آغاز و پایان animation را مشخص می‌کنند. اگر دست‌کم یک بار استفاده شوند، باید روی تمام مراحل keyframe اعمال شوند.

تعریف offset برای keyframeها اختیاری است. اگر آن‌ها را حذف کنید، offsetهایی با فاصله مساوی به‌طور خودکار اختصاص می‌یابند. برای مثال، سه keyframe بدون offset ازپیش‌تعریف‌شده، مقادیر ۰،‏ ۰٫۵ و ۱ را دریافت می‌کنند. تعیین offset برابر ۰٫۸ برای transition میانی در مثال بالا می‌تواند به‌شکل زیر باشد.

keyframeها همراه با offset

کدی که offset در آن مشخص شده به‌شکل زیر است.

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

@Component({
  selector: 'app-status-slider',
  templateUrl: 'status-slider.html',
  styleUrls: ['status-slider.css'],
  animations: [
    trigger('slideStatus', [
      state('inactive', style({backgroundColor: 'blue'})),
      state('active', style({backgroundColor: '#754600'})),

      // #docregion keyframesWithOffsets
      transition('* => active', [
        animate(
          '2s',
          keyframes([
            style({backgroundColor: 'blue', offset: 0}),
            style({backgroundColor: 'red', offset: 0.8}),
            style({backgroundColor: '#754600', offset: 1.0}),
          ]),
        ),
      ]),
      transition('* => inactive', [
        animate(
          '2s',
          keyframes([
            style({backgroundColor: '#754600', offset: 0}),
            style({backgroundColor: 'red', offset: 0.2}),
            style({backgroundColor: 'blue', offset: 1.0}),
          ]),
        ),
      ]),
      // #enddocregion keyframesWithOffsets

      // #docregion keyframes
      transition('* => active', [
        animate(
          '2s',
          keyframes([
            style({backgroundColor: 'blue'}),
            style({backgroundColor: 'red'}),
            style({backgroundColor: 'orange'}),
          ]),
        ),
        // #enddocregion keyframes
      ]),
    ]),
  ],
})
export class StatusSlider {
  status: 'active' | 'inactive' = 'inactive';

  toggle() {
    if (this.status === 'active') {
      this.status = 'inactive';
    } else {
      this.status = 'active';
    }
  }
}

می‌توانید keyframeها را در یک animation با duration،‏ delay و easing ترکیب کنید.

keyframeهای دارای ضربان

با تعریف styleها در offsetهای مشخص در طول animation، از keyframeها برای ایجاد جلوه ضربان استفاده کنید.

در ادامه مثالی از ساخت جلوه ضربان با keyframeها آمده است:

  • stateهای اصلی open و closed همراه با تغییرهای اولیه ارتفاع، رنگ و opacity که طی یک ثانیه رخ می‌دهند.
  • توالی keyframe در میانه قرار می‌گیرد و باعث می‌شود button در همان بازه یک‌ثانیه‌ای به‌شکل نامنظم ضربان داشته باشد.
keyframeهای دارای ضربان نامنظم

قطعه‌کد این animation می‌تواند به‌شکل زیر باشد.

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

@Component({
  selector: 'app-open-close',
  animations: [
    // #docregion trigger
    trigger('openClose', [
      state(
        'open',
        style({
          height: '200px',
          opacity: 1,
          backgroundColor: 'yellow',
        }),
      ),
      state(
        'close',
        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}),
          ]),
        ),
      ]),
    ]),
    // #enddocregion trigger
  ],
  templateUrl: 'open-close.html',
  styleUrls: ['open-close.css'],
})
export class OpenCloseKeyframeComponent {
  isOpen = false;

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

  logging = input(false);
  onAnimationEvent(event: AnimationEvent) {
    if (!this.logging) {
      return;
    }
  }
}

propertyها و واحدهای قابل animate

animationهای Angular بر web animationها ساخته شده‌اند؛ بنابراین می‌توانید هر propertyای را که مرورگر قابل animate می‌داند animate کنید. این موارد شامل موقعیت، اندازه، transform، رنگ، border و موارد دیگر هستند. W3C فهرستی از propertyهای قابل animate را در صفحه CSS Transitions نگهداری می‌کند.

برای propertyهای دارای مقدار عددی، با ارائه مقدار به‌صورت رشته درون کوتیشن و همراه با پسوند مناسب، واحد را تعریف کنید:

'50px'

  • ۵۰ پیکسل:

'3em'

  • اندازه نسبی فونت:

'100%'

  • درصد:

می‌توانید مقدار را به‌صورت عدد نیز ارائه دهید. در این حالت Angular واحد پیش‌فرض را پیکسل یا px در نظر می‌گیرد. نوشتن ۵۰ پیکسل به‌شکل 50 با '50px' یکسان است.

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

گاهی مقدار یک property ابعادی در style تا هنگام runtime مشخص نیست. برای مثال، width و height یک element اغلب به محتوای آن یا اندازه صفحه وابسته است. animate کردن این propertyها با CSS معمولاً دشوار است.

در این موارد می‌توانید در style() از مقدار ویژه wildcard یعنی * برای property استفاده کنید. مقدار آن property مشخص در runtime محاسبه و سپس وارد animation می‌شود.

مثال زیر triggerای با نام shrinkOut دارد که هنگام خروج element در HTML از صفحه استفاده می‌شود. animation ارتفاع element پیش از خروج را دریافت کرده و آن را از همان ارتفاع به صفر animate می‌کند.

hero-list-auto.ts
import {Component, Output, EventEmitter, input} from '@angular/core';
import {trigger, state, style, animate, transition} from '@angular/animations';

import {Hero} from './hero';

@Component({
  selector: 'app-hero-list-auto',
  templateUrl: 'hero-list-auto.html',
  styleUrls: ['./hero-list-page.css'],
  // #docregion auto-calc
  animations: [
    trigger('shrinkOut', [
      state('in', style({height: '*'})),
      transition('* => void', [style({height: '*'}), animate(250, style({height: 0}))]),
    ]),
  ],
  // #enddocregion auto-calc
})
export class HeroListAuto {
  readonly heroes = input<Hero[]>([]);

  @Output() remove = new EventEmitter<number>();

  removeHero(id: number) {
    this.remove.emit(id);
  }
}

خلاصه keyframeها

تابع keyframes() در Angular امکان تعیین چند style میانی در یک transition را فراهم می‌کند. می‌توان از offset اختیاری برای تعریف نقطه وقوع هر تغییر style در animation استفاده کرد.

مطالب بیشتر درباره animationهای Angular

ممکن است مطالب زیر نیز برایتان مفید باشند: