آنلاین

متحرک‌سازی applicationها با animate.enter و animate.leave

animationهای خوب طراحی‌شده می‌توانند application شما را شهودی‌تر و جذاب‌تر کنند، اما فقط جنبه ظاهری ندارند. animationها می‌توانند application و تجربه کاربر را به چند روش بهتر کنند:

  • بدون animation، transitionهای web page می‌توانند ناگهانی و آزاردهنده به نظر برسند.
  • motion تجربه کاربر را بسیار بهتر می‌کند، بنابراین animationها به کاربران فرصت می‌دهند response application به actionهایشان را تشخیص دهند.
  • animationهای خوب می‌توانند توجه کاربر را در طول یک workflow به نرمی هدایت کنند.

Angular برای animate کردن elementهای application شما، animate.enter و animate.leave را فراهم می‌کند. این دو feature در زمان مناسب CSS classهای enter و leave را اعمال می‌کنند یا functionهایی را فراخوانی می‌کنند تا animationها از libraryهای third-party اعمال شوند. animate.enter و animate.leave directive نیستند. آن‌ها API خاصی هستند که مستقیماً توسط Angular compiler پشتیبانی می‌شوند. می‌توان از آن‌ها مستقیم روی elementها و همچنین به عنوان host binding استفاده کرد.

animate.enter

می‌توانید از animate.enter برای animate کردن elementها هنگام ورود آن‌ها به DOM استفاده کنید. می‌توانید enter animationها را با CSS classها، چه با transitionها و چه با keyframe animationها، تعریف کنید.

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

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

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

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

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

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

.enter-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;
}

وقتی animation کامل شود، Angular class یا classهایی را که در animate.enter مشخص کرده‌اید از DOM حذف می‌کند. Animation classها فقط وقتی animation فعال است وجود دارند.

می‌توانید animate.enter را همراه با هر feature دیگر Angular مثل control flow یا dynamic expressionها استفاده کنید. animate.enter هم یک class string منفرد را می‌پذیرد \(با چند class که با space جدا شده‌اند\)، و هم آرایه‌ای از class stringها را.

یک نکته سریع درباره استفاده از CSS transitionها: اگر به جای keyframe animation از transition استفاده می‌کنید، classهایی که با animate.enter به element اضافه می‌شوند stateای را نشان می‌دهند که transition به سمت آن animate می‌کند. CSS پایه element همان چیزی است که element وقتی هیچ animationای اجرا نمی‌شود شبیه آن خواهد بود، که احتمالاً مشابه end state مربوط به CSS transition است. بنابراین هنوز باید آن را با @starting-style pair کنید تا state مناسب from برای کار کردن transition داشته باشید.

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

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

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

  enterClass = signal('enter-animation');
}
html
<!-- #docplaster -->
<h2><code>animate.enter</code> Binding Example</h2>

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

@if (isShown()) {
  <div class="enter-container" [animate.enter]="enterClass()">
    <p>The box is entering.</p>
  </div>
}
css
:host {
  display: block;
  height: 200px;
}

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

.enter-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;
}

animate.leave

می‌توانید از animate.leave برای animate کردن elementها هنگام خروج آن‌ها از DOM استفاده کنید. می‌توانید leave animationها را با CSS classها، چه با transformها و چه با keyframe animationها، تعریف کنید.

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

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

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

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

@if (isShown()) {
  <div class="leave-container" animate.leave="leaving">
    <p>Goodbye</p>
  </div>
}
css
:host {
  display: block;
  height: 200px;
}

.leave-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;
  }
}

.leave-container p {
  margin: 0;
}

.leaving {
  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;
}

وقتی animation کامل شود، Angular به صورت خودکار element animateشده را از DOM حذف می‌کند.

animate.leave را می‌توان همراه با signalها و bindingهای دیگر هم استفاده کرد. می‌توانید animate.leave را با یک class یا چند class استفاده کنید. آن را یا به صورت یک string ساده با spaceها مشخص کنید یا به صورت string array.

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

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

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

  farewell = signal('leaving');
}
html
<!-- #docplaster -->
<h2><code>animate.leave</code> Binding Example</h2>

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

@if (isShown()) {
  <div class="leave-container" [animate.leave]="farewell()">
    <p>Goodbye</p>
  </div>
}
css
:host {
  display: block;
  height: 200px;
}

.leave-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;
  }
}

.leave-container p {
  margin: 0;
}

.leaving {
  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;
}

ترتیب حذف element

در نحوه اجرای animationهای animate.leave و زمان رخ دادن animation کمی ظرافت وجود دارد. animate.leave وقتی کار می‌کند که روی elementای قرار بگیرد که در حال حذف شدن است، و اگر animate.leave روی elementای قرار بگیرد که descendent همان element در حال حذف است، animationهای child قبل از حذف parent node از DOM اجرا می‌شوند. این تضمین می‌کند که بتوانید با اطمینان child elementها را animate کنید بدون اینکه parent node زودتر از موعد ناپدید شود.

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

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

  toggle() {
    this.isShown.update((isShown) => !isShown);
  }
}
html
<!-- #docplaster -->
<h2><code>animate.leave</code> Parent Sub-tree Animation Example</h2>

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

@if (isShown()) {
  <div class="leave-parent">
    <!-- the child node with `animate.leave` will animate first
         before the parent node is removed from the DOM. -->
    <div class="leave-container" animate.leave="leaving">
      <p>Goodbye</p>
    </div>
  </div>
}
css
:host {
  display: block;
  height: 200px;
}

.leave-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;
  }
}

.leave-container p {
  margin: 0;
}

.leaving {
  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;
}

Event Bindingها، Functionها و Libraryهای third-party

هر دو animate.enter و animate.leave از syntax مربوط به event binding پشتیبانی می‌کنند که امکان function call را فراهم می‌کند. می‌توانید از این syntax برای فراخوانی function در کد کامپوننت خود یا استفاده از animation libraryهای third-party مثل GSAP، anime.js یا هر JavaScript animation library دیگر استفاده کنید.

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

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

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

  leavingFn(event: AnimationCallbackEvent) {
    // Example of calling GSAP
    // gsap.to(event.target, {
    //   duration: 1,
    //   x: 100,
    //   // arrow functions are handy for concise callbacks
    //   onComplete: () => event.animationComplete()
    // });
    event.animationComplete();
  }
}
html
<!-- #docplaster -->
<h2><code>animate.leave</code> Function Example</h2>

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

@if (isShown()) {
  <div class="leave-container" (animate.leave)="leavingFn($event)">
    <p>Goodbye</p>
  </div>
}
css
:host {
  display: block;
  height: 200px;
}

.leave-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;
  }
}

.leave-container p {
  margin: 0;
}

.leaving {
  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;
}

object مربوط به $event نوع AnimationCallbackEvent دارد. این object شامل element به عنوان target است و functionای به نام animationComplete() فراهم می‌کند تا وقتی animation تمام شد framework را باخبر کنید.

اگر هنگام استفاده از animate.leave، animationComplete() را فراخوانی نکنید، Angular پس از delay چهارثانیه‌ای این function را به صورت خودکار فراخوانی می‌کند. می‌توانید مدت delay را با provide کردن token مربوط به MAXANIMATIONTIMEOUT بر حسب millisecond configure کنید.

ts
{ provide: MAX_ANIMATION_TIMEOUT, useValue: 6000 }

سازگاری با Legacy Angular Animations

نمی‌توانید legacy animationها را همراه با animate.enter و animate.leave داخل همان کامپوننت استفاده کنید. انجام این کار باعث می‌شود enter classها روی element باقی بمانند یا nodeهای leaving حذف نشوند. اما استفاده از legacy animationها و animationهای جدید animate.enter و animate.leave داخل همان application مشکلی ندارد. تنها caveat مربوط به content projection است. اگر content را از کامپوننتی با legacy animationها به کامپوننت دیگری با animate.enter یا animate.leave project کنید، یا برعکس، همان رفتاری رخ می‌دهد که انگار آن‌ها با هم در همان کامپوننت استفاده شده‌اند. این پشتیبانی نمی‌شود.

Testing

TestBed پشتیبانی built-in برای فعال یا غیرفعال کردن animationها در test environment شما فراهم می‌کند. CSS animationها برای اجرا به browser نیاز دارند و بسیاری از APIها در test environment در دسترس نیستند. به صورت پیش‌فرض، TestBed animationها را در test environmentهای شما غیرفعال می‌کند.

اگر می‌خواهید test کنید animationها در browser test، مثلاً end-to-end test، واقعاً animate می‌شوند، می‌توانید TestBed را با مشخص کردن animationsEnabled: true در test configuration، برای فعال کردن animationها configure کنید.

ts
TestBed.configureTestingModule({animationsEnabled: true});

این کار animationها را در test environment شما configure می‌کند تا normal رفتار کنند.

بیشتر درباره animationهای Angular

ممکن است به موارد زیر هم علاقه‌مند باشید: