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های open و closed را ادامه میدهد. بهجای تعریف تمام جفتهای transition میان stateها، هر transition به closed یک ثانیه و هر transition به open نیم ثانیه طول میکشد.
به این ترتیب میتوان stateهای جدید را بدون افزودن transition جداگانه برای هرکدام اضافه کرد.
// #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 پیکان دوتایی استفاده کنید.
// #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 میتواند حجم کد لازم را کاهش دهد.
// #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 componenttransition مربوط به * => * هنگام هر تغییر میان دو state اعمال میشود.
transitionها بهترتیب تعریفشدن تطبیق داده میشوند. بنابراین میتوانید transitionهای دیگری را روی transition مربوط به * => * اعمال کنید. برای مثال، تغییرهای style یا animationهایی را تعریف کنید که فقط بر open => closed اعمال شوند و سپس * => * را برای جفت stateهایی که بهطور مشخص بیان نشدهاند، بهعنوان fallback بهکار ببرید.
برای این کار، transitionهای خاصتر را پیش از * => * فهرست کنید.
استفاده از wildcard همراه با style
از wildcard یعنی * همراه با یک style استفاده کنید تا animation مقدار فعلی آن style را بهکار بگیرد و بر اساس آن animate شود. wildcard مقدار fallback است که اگر state در حال animate در trigger اعلام نشده باشد، استفاده میشود.
// #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 componentstate از نوع 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 را حذف میکنید، به نظر میرسد به سمت راست از صفحه پرواز میکند.
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ها استفاده میکنند.
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 شامل کد زیر است.
<!-- #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 به ۱ تغییر کند.
// #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ها، صفحه توالیهای پیچیده را ببینید.
// #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 را مقایسه میکند تطبیق داد.
<!-- #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 میشود و در نتیجه نامرئی خواهد بود.
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ها جلوگیری میکند.
نمونه کد زیر نحوه استفاده از این قابلیت را نشان میدهد.
<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 -->// #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 قرار دهید.
// #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-animationscallbackهای animation
تابع trigger() مربوط به animation هنگام شروع و پایان، callback منتشر میکند. مثال زیر componentای را نشان میدهد که دارای trigger با نام openClose است.
// #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 بهشکل زیر ظاهر میشود.
<!-- #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 ایجاد میکند.
// #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 componentKeyframeها
برای ساخت animation چندمرحلهای که مراحل آن بهترتیب اجرا میشوند، از keyframe استفاده کنید.
تابع keyframe() در Angular چند تغییر style را در یک بخش زمانبندی ممکن میکند. برای مثال، button میتواند بهجای محوشدن، در یک بازه زمانی دوثانیهای چند بار تغییر رنگ دهد.
کد این تغییر رنگ میتواند بهشکل زیر باشد.
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 میانی در مثال بالا میتواند بهشکل زیر باشد.
کدی که offset در آن مشخص شده بهشکل زیر است.
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 در همان بازه یکثانیهای بهشکل نامنظم ضربان داشته باشد.
قطعهکد این animation میتواند بهشکل زیر باشد.
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 میکند.
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
ممکن است مطالب زیر نیز برایتان مفید باشند: