Одна из моих любимых функций Angular - анимация. Осмысленная анимация может значительно улучшить юзабилити вашего приложения. Конечно, анимация частей HTML-страницы была возможна и до Angular, но интеграция с фреймворком и простота использования позволили мне это сделать. Теперь, когда я создаю страницу, я думаю, можно ли ее улучшить с помощью анимации.

В этом посте я покажу вам один из таких примеров - анимированную панель слайдов. Это компонент пользовательского интерфейса, с которым вы должны быть знакомы по мобильным платформам. Вот как это выглядит в одном из наших приложений:

На картинке выше вы видите два элемента пользовательского интерфейса - список адресов и форму редактирования адреса. Когда пользователь нажимает кнопку редактирования рядом с элементом адреса в списке, список начинает скользить влево. При этом форма редактирования сдвигается справа. Когда пользователь нажимает кнопку НАЗАД или СОХРАНИТЬ, процесс анимации обратный. Здесь анимация не только красиво выглядит, но и помогает пользователям понять, что произошло, дает им представление о контексте и структуре пользовательского интерфейса.

Выдвижная панель, на которой размещен список адресов и форма редактирования, реализована как отдельный компонент многократного использования. Мы используем его еще в паре мест в нашем приложении. Ниже я покажу вам, как это можно реализовать.

Главная идея

Прежде чем мы перейдем к коду, давайте обсудим общую идею того, как мы собираемся реализовать панель. Он будет представлен угловым компонентом. Шаблон компонента будет состоять из трех основных элементов - родительского элемента div и двух дочерних элементов div. Ширина родительского элемента в два раза больше ширины компонента. Дочерние элементы выровнены в ряд внутри родительского бок о бок. Каждый дочерний элемент занимает половину ширины родительского элемента. Все, что не соответствует ширине компонента, скрывается. Когда мы переключаем дочерние элементы, мы анимируем переход с помощью Angular Animation API. Вот визуальное представление идеи:

Реализация

Начнем с объявления базового шаблона компонента Angular:

import { Component } from '@angular/core';
@Component({
  selector: 'my-slide-panel',
  styleUrls: [ './slide-panel.component.scss' ],
  templateUrl: './slide-panel.component.html'
})
export class SlidePanelComponent {}

Теперь нам нужно добавить свойство, которое будет хранить информацию о отображаемом в данный момент дочернем элементе. Поскольку потребители нашего компонента должны иметь возможность изменять это состояние, это будет входное свойство:

import {
  ChangeDetectionStrategy,
  Component,
  Input
} from '@angular/core';
@Component({
  //...
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SlidePanelComponent {
  @Input() activePane: PaneType = 'left';
}
type PaneType = 'left' | 'right';

Мы можем безопасно включить стратегию обнаружения изменений OnPush для нашей панели, поскольку нет другого способа обновить пользовательский интерфейс компонента, кроме изменения значения свойства input.

Еще одна интересная вещь, которую мы здесь используем, - это PaneType. Мы используем строковые литералы TypeScript в сочетании с типами объединения, чтобы ограничить возможные значения, которые могут быть присвоены свойству activePane. Это увеличивает безопасность типов в нашем приложении.

Теперь давайте определим шаблон:

<div class="parent">
  <div><ng-content select="[leftPane]"></ng-content></div>
  <div><ng-content select="[rightPane]"></ng-content></div>
</div>

Как я уже говорил выше, у нас есть один родительский и два дочерних элемента. Для каждого дочернего элемента определен элемент ng-content. Это называется проекцией содержимого в Angular и позволяет потребителям нашего компонента передавать настраиваемую HTML-разметку, которая будет отображаться внутри нашего компонента вместо элементов ng-content. Компонент может предоставить несколько таких областей, различая элементы ng-content с уникальными значениями атрибутов select. В нашем случае это [leftPane] и [rightPane].

Чтобы правильно выровнять родительские и дочерние элементы, нам нужно определить CSS внутри файла slide-panel.component.scss:

:host {
  display: block;
  overflow: hidden; /* Hide everything that doesn't fit compoennt */
}
.parent {
  height: 100%;
  width: 200%;      /* Make the parent element to take up twice
                       of the component's width */
  display: flex;    /* Align all children in a row */
  div { flex: 1; }  /* Evenly divide width between children */
}

Селектор : host позволяет нам применять стили к элементу HTML, который будет представлять наш компонент при вставке на страницу. Он обертывает все элементы, определенные в шаблоне компонента. Содержимое нашего компонента будет занимать вдвое больше ширины компонента, но мы хотим отображать только половину содержимого за раз. Чтобы скрыть все, что не подходит, мы объявляем overflow: hidden.

Затем мы определяем стили для родительского элемента. Чтобы он занимал вдвое больше ширины компонента, мы добавляем width: 200%. Но это пространство должно быть равномерно разделено между дочерними элементами родителей, мы достигаем этого с помощью гибкого макета.

Добавление анимации

Примечание. Угловая анимация - большая тема. Я не буду объяснять детали API анимации в этом посте. Я просто дам вам рабочее решение и выделю ключевые моменты. Если вам интересно изучить его дальше, лучше всего начать с официального руководства по Angular.

Angular Animation API поставляется в виде отдельного пакета. Его следует установить с помощью команды npm install --save @angular/animations (или с помощью yarn add @angular/animations). Далее нам нужно импортировать BrowserAnimationsModule в корневой модуль приложения Angular:

import { NgModule } from '@angular/core';
import {
  BrowserAnimationsModule
} from '@angular/platform-browser/animations';
@NgModule({
  imports: [
    //...
    BrowserAnimationsModule
  ],
  //...
})
export class AppModule {}

Теперь мы можем применить анимацию к панели слайдов:

import {
  animate, state, style, transition, trigger
} from '@angular/animations';
@Component({
  //...
  animations: [
    trigger('slide', [
      state('left', style({ transform: 'translateX(0)' })),
      state('right', style({ transform: 'translateX(-50%)' })),
      transition('* => *', animate(300))
  ])
})
export class SlidePanelComponent {
  @Input() activePane: PaneType = 'left';
}

Функция trigger() создает именованный дескриптор «слайд», который мы можем использовать в шаблоне для запуска анимации. Функции state() определяют состояния, которые может принимать наш компонент - влево или вправо. Когда компонент принимает одно из состояний, стили, определенные для этого состояния, применяются к анимированному элементу (см. Код шаблона ниже). Функция transition() определяет, как компонент переходит из одного состояния в другое.

Чтобы прикрепить анимацию к элементу в шаблоне, мы используем специальный синтаксис:

<div class="panes" [@slide]="activePane">
  <div><ng-content select="[leftPane]"></ng-content></div>
  <div><ng-content select="[rightPane]"></ng-content></div>
</div>

Его можно читать так: при изменении свойства activePane применить slide анимацию к текущему элементу div. Значения свойства activePane должны соответствовать одному из определенных состояний анимации.

В нашем случае левый дочерний элемент виден по умолчанию. Когда значение activePane обновляется до ‘right’, включается анимация и применяет стили, определенные для состояния right, к родительскому элементу div. transformX(-50%) перемещает родительский элемент div по горизонтальной оси влево. Таким образом, левая часть родительского элемента становится скрытой, а правая часть - видимой. Обычно это происходит немедленно, но из-за определенного нами перехода он длится 300 миллисекунд, поэтому пользователь видит плавную анимацию.

Пример использования

Наконец, давайте добавим образец кода, который использует наш компонент. Вы можете найти рабочее приложение здесь. Ниже я просто показываю часть шаблона AppComponent:

<my-slide-panel [activePane]="isLeftVisible ? 'left' : 'right'">
  <div leftPane>LEFT</div>
  <div rightPane>RIGHT</div>
</my-slide-panel>
<button (click)="isLeftVisible = !isLeftVisible">
  Toggle panes
</button>

Мы объявляем компонент my-slide-panel, который имеет два дочерних элемента div. К дочерним элементам прикреплены атрибуты leftPane и rightPane. Это происходит из-за селекторов, которые мы определили для элементов ng-content внутри шаблона панели слайдов. Затем мы привязываем свойство activePane к выражению, которое либо оценивается как ‘left’, либо ‘right’ в зависимости от значения флага isLeftVisible. Флаг переключается кнопкой ниже каждый раз, когда пользователь нажимает на него.

Теперь все должно работать. В демонстрационном приложении я предоставляю дополнительные стили CSS для AppComponent, чтобы сделать демонстрацию более наглядной. Если вы запустите его, вы должны увидеть следующий интерфейс:

Резюме

Как видите, добавить анимацию в приложение Angular несложно. Более сложный вопрос - где это имеет смысл. Но если вам удастся идентифицировать эти места, даже базовые методы анимации могут улучшить ваше приложение и помочь пользователям лучше понять его.