Этот пост является частью Hotwire Summer: нового сезона контента на Boring Rails!
Если вы уже создавали элементы пользовательского интерфейса с помощью StimulusJS, вы наверняка писали код, чтобы показать или скрыть элемент на странице. Независимо от того, открываете ли вы модальное окно или выдвигаете панель, все мы писали код контроллера, например:
this.modalTarget.classList.remove("hidden")
Чтобы поднять дизайн пользовательского интерфейса на новый уровень, можно использовать переходы, чтобы элементы не сразу появлялись или исчезали с экрана. Можно использовать переход непрозрачности, чтобы элементы плавно перетекали друг в друга, и использовать перевод, чтобы сдвинуть их на место.
Одна из проблем при попытке сделать такую анимацию с помощью свойств CSS transition
, как описал Себастьян Де Дейн в этом отличном учебнике по переходам enter и leave, заключается в том, что вы не можете изменить свойство display
элемента до того, как произойдет переход. Мы можем использовать основные стили transition
для таких вещей, как тонкое изменение цвета кнопки при наведении, но для плавной анимации, когда элементы показываются или скрываются, нам нужно нечто большее.
Один из шаблонов, который появился в сообществах Vue и Alpine, заключается в использовании серии атрибутов data
для определения желаемых классов CSS во время жизненного цикла перехода. TailwindUI также следует этому шаблону при определении того, как анимировать свои компоненты. Она доказала свою эффективность и простоту понимания.
Каждый проект имеет свои собственные соглашения об именовании, но все они определяют шесть основных «этапов» жизненного цикла:
- Entering: классы, которые должны быть на элементе в течение всего времени, пока он входит на страницу (иногда называется «Entering Active», «Enter Active», «Enter» и т.д.).
- Enter From: начальная точка, из которой будет осуществляться переход при входе на страницу (иногда называется «Enter Start»).
- Enter To: конечная точка перехода при входе на страницу (иногда называется «Enter End»).
- Leaving: классы, которые должны находиться на элементе в течение всего времени, пока он покидает страницу (иногда называется «Leaving Active», «Leave Active», «Leave» и т. д.)
- Leave From: начальная точка, из которой будет осуществляться переход при уходе со страницы (иногда называется «Leave Start»)
- Leave To: конечная точка перехода при уходе со страницы (иногда называется «Leave End»).
Этот рисунок из документации Vue очень помог мне представить, как все это работает:
Подход с использованием атрибутов data
хорошо работает с классами утилит перехода Tailwind и чувствует себя как дома в философии StimulusJS по дополнению HTML-разметки.
Vue (Transition
) и Alpine (x-transition
) предоставляют встроенную, стороннюю поддержку таких переходов, но для Rails-приложения, использующего Hotwire, нам придется добавить эту функциональность самостоятельно.
Варианты
Я изучил несколько различных вариантов в этой области. Вот наиболее популярные подходы, с которыми я столкнулся:
stimulus-transitions
Эта библиотека предоставляет контроллер transition
, который вы можете импортировать и зарегистрировать. Вы используете этот контроллер как обычный контроллер Stimulus в своем приложении:
<div data-controller="transition"
data-transition-enter-active="enter-class"
data-transition-enter-from="enter-from-class"
data-transition-enter-to="enter-to-class"
data-transition-leave-active="or-use multiple classes"
data-transition-leave-from="or-use multiple classes"
data-transition-leave-to="or-use multiple classes">
<!-- content -->
</div>
Контроллер будет автоматически определять, когда элемент отображается или скрывается, и запускать переходы. Также есть возможность прослушивания пользовательских событий transition:end-enter
и transition:end-leave
, если вы хотите запустить дополнительный код после завершения переходов.
Контроллеру transition
необходимо что-то для запуска стиля отображения элемента, поэтому вам понадобится контроллер на уровне приложения, чтобы запустить этот процесс.
stimulus-use/useTransition
Проект stimulus-use
представляет собой коллекцию повторно используемых поведений для Stimulus. Если вы знакомы с React, этот проект похож на систему hooks
React, но для контроллеров Stimulus.
В этом пакете доступен один конкретный микс-ин — useTransition
. Вы можете вызвать его из собственного контроллера Stimulus, и он запустит переходы на элементе (либо считывая данные из атрибутов data-
, либо вы можете указать классы в JavaScript в качестве опций).
На момент написания этой статьи данный микс-ин был помечен как «бета» релиз.
el-transition
Эта библиотека не является специфичной для Stimulus, но реализует тот же шаблон перехода Vue/Alpine. Поскольку это ванильный Javascript, здесь нет встроенных крючков для жизненного цикла Stimulus или контроллеров для регистрации. Вы импортируете функции enter
и leave
напрямую и затем вызываете их, предоставляя элемент для перехода.
import {enter, leave} from 'el-transition'
// in your stimulus controller somewhere
enter(this.modalTarget)
leave(this.modalTarget)
Вся библиотека на самом деле представляет собой один файл на 60 строк, так что вы можете даже просто бросить его в свой проект, если хотите использовать его.
Моя рекомендация: el-transition
Все три библиотеки могут делать то, что я хотел: применять переходы атрибутов data-
в стиле Vue/Alpine.
Я добился наибольшего успеха с el-transition
и выбрал его для своего проекта.
Мне понравилось, что он очень прост и не привязан к фреймворку. У него минимальная площадь поверхности, и мне не нужно было полагаться на внешнюю библиотеку для обновления на новые версии Stimulus, если в нем произошли изменения.
Дополнительным бонусом было то, что функции enter
и leave
возвращали объекты Promise
, которые работали намного лучше для координации нескольких элементов, которым нужно перейти (это довольно часто встречается в компонентах TailwindUI).
Создание скользящего меню Tailwind UI
Давайте применим этот совет на практике, создав слайд-меню из TailwindUI.
Начните с захвата шаблона HTML-кода (для этой статьи мы используем один из бесплатных образцов).
В данном случае мы создадим контроллер slide-over
, с целями для трех частей в разметке Tailwind (backdrop
, panel
и closeButton
), а затем еще одну для всего меню (container
).
Обратите внимание на несколько комментариев к коду, которые выделяют различные части компонента и способы их перехода.
<!--
Background backdrop, show/hide based on slide-over state.
Entering: "ease-in-out duration-500"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in-out duration-500"
From: "opacity-100"
To: "opacity-0"
-->
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"></div>
Для каждой из этих частей компонента мы привяжем их как цели Stimulus, а также добавим атрибуты data
для соответствия спецификации.
<div data-controller="slide-over">
...
<div data-slide-over-target="backdrop"
class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"
data-transition-enter="ease-in-out duration-500"
data-transition-enter-start="opacity-0"
data-transition-enter-end="opacity-100"
data-transition-leave="ease-in-out duration-500"
data-transition-leave-start="opacity-100"
data-transition-leave-end="opacity-0"></div>
...
</div>
Повторите это для других элементов, которые мы хотим анимировать. Мы также добавим базовую <button>
для отображения панели при нажатии.
Вот что представляет собой полная разметка:
<div data-controller="slide-over">
<button class="form-input" data-action="slide-over#show">Show slideover</button>
<!-- This example requires Tailwind CSS v2.0+ -->
<div data-slide-over-target="container" class="relative z-10 hidden" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
<!--
Background backdrop, show/hide based on slide-over state.
Entering: "ease-in-out duration-500"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in-out duration-500"
From: "opacity-100"
To: "opacity-0"
-->
<div data-slide-over-target="backdrop"
class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"
data-transition-enter="ease-in-out duration-500"
data-transition-enter-start="opacity-0"
data-transition-enter-end="opacity-100"
data-transition-leave="ease-in-out duration-500"
data-transition-leave-start="opacity-100"
data-transition-leave-end="opacity-0"></div>
<div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden">
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10 pointer-events-none">
<!--
Slide-over panel, show/hide based on slide-over state.
Entering: "transform transition ease-in-out duration-500 sm:duration-700"
From: "translate-x-full"
To: "translate-x-0"
Leaving: "transform transition ease-in-out duration-500 sm:duration-700"
From: "translate-x-0"
To: "translate-x-full"
-->
<div data-slide-over-target="panel"
class="relative w-screen max-w-md pointer-events-auto"
data-transition-enter="transform transition ease-in-out duration-500 sm:duration-700"
data-transition-enter-start="translate-x-full"
data-transition-enter-end="translate-x-0"
data-transition-leave="transform transition ease-in-out duration-500 sm:duration-700"
data-transition-leave-start="translate-x-0"
data-transition-leave-end="translate-x-full">
<!--
Close button, show/hide based on slide-over state.
Entering: "ease-in-out duration-500"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in-out duration-500"
From: "opacity-100"
To: "opacity-0"
-->
<div data-slide-over-target="closeButton"
class="sm:-ml-10 sm:pr-4 absolute top-0 left-0 flex pt-4 pr-2 -ml-8"
data-transition-enter="ease-in-out duration-500"
data-transition-enter-start="opacity-0"
data-transition-enter-end="opacity-100"
data-transition-leave="ease-in-out duration-500"
data-transition-leave-start="opacity-100"
data-transition-leave-end="opacity-0">
<button type="button" data-action="slide-over#hide" class="hover:text-white focus:outline-none focus:ring-2 focus:ring-white text-gray-300 rounded-md">
<span class="sr-only">Close panel</span>
<!-- Heroicon name: outline/x -->
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex flex-col h-full py-6 overflow-y-auto bg-white shadow-xl">
<div class="sm:px-6 px-4">
<h2 class="text-lg font-medium text-gray-900" id="slide-over-title">Panel title</h2>
</div>
<div class="sm:px-6 relative flex-1 px-4 mt-6">
<!-- Replace with your content -->
<div class="sm:px-6 absolute inset-0 px-4">
<div class="h-full border-2 border-gray-200 border-dashed" aria-hidden="true">
Your content goes here! How about a lazy-loaded turbo-frame?
</div>
</div>
<!-- /End replace -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
А теперь нам нужно реализовать контроллер Stimulus для запуска переходов.
import { Controller } from "@hotwired/stimulus"
import { enter, leave } from 'el-transition'
export default class extends Controller {
static targets = ["container", "backdrop", "panel", "closeButton"]
show() {
this.containerTarget.classList.remove("hidden")
enter(this.backdropTarget)
enter(this.closeButtonTarget)
enter(this.panelTarget)
}
hide() {
Promise.all([
leave(this.backdropTarget),
leave(this.closeButtonTarget),
leave(this.panelTarget)
]).then(() => {
this.containerTarget.classList.add("hidden")
})
}
}
Когда вызывается действие show
(щелчок по кнопке), мы удаляем класс hidden
для всего контейнера и затем запускаем функцию enter
из el-transition
на каждой из целей, которые мы хотим анимировать. Это приведет к затуханию фона и кнопки закрытия и скольжению по панели с использованием классов Tailwind, которые мы определили в атрибутах data
.
Когда мы запускаем действие hide
(щелчок по кнопке закрытия), мы делаем все в обратном порядке. Мы запускаем функцию leave
, и панель сдвигается обратно, а фон и кнопка закрытия исчезают. Когда все переходы выполнены, мы скрываем весь контейнер. Используя Promise.all
, мы можем дождаться завершения всех отдельных переходов (помните, что они могут иметь разную продолжительность!), прежде чем скрыть контейнер.
Нет необходимости в setTimeout
или мигании содержимого, когда переход завершен, а затем удален!
Это не так удобно, как вставлять фрагменты React или Vue из TailwindUI, но очень близко!
Возможно, вы захотите сделать еще один шаг вперед, извлекая разметку в партицию или используя ViewComponent для очистки кода, но это остается на усмотрение читателя.
Завершение
Очень полезно следить за другими фронтенд-сообществами и привносить идеи в свою экосистему. Vue и Alpine создали действительно четкий, понятный шаблон для определения CSS-переходов, и мы можем использовать эту работу в проекте StimulusJS/Hotwire с помощью небольшой библиотеки.
Эти переходы требуют немного времени, чтобы разобраться с ними, но они добавляют хороший уровень полировки вашим компонентам пользовательского интерфейса при минимальных усилиях: именно такие высокоэффективные методы мы хотим использовать при создании приложений в стиле «скучного Rails».