Перейти к содержимому

Morph

Плагин Morph позволяет «преобразовать» элемент на странице в предоставленный HTML-шаблон, при этом сохраняется любое состояние браузера или Alpine внутри «морфированного» элемента.

Это удобно для обновления HTML из серверного запроса без потери состояния страницы Alpine. Подобная утилита лежит в основе таких полностековых фреймворков, как Laravel Livewire и Phoenix LiveView.

Лучше всего понять его назначение можно с помощью следующей интерактивной визуализации. Попробуйте!

<div x-data="{ slide: 1 }" class="border rounded">
<div>
<img :src="'https://alpinejs.dev/img/morphs/morph'+slide+'.png'">
</div>
<div class="flex w-full items-baseline justify-between mb-4">
<div class="w-1/2 px-4">
<button @click="slide = (slide === 1) ? 13 : slide - 1" class="w-full">Предыдущая картинка</button>
</div>
<div class="w-1/2 px-4">
<button @click="slide = (slide % 13) + 1" class="w-full">Следующая картинка</button>
</div>
</div>
</div>

Установка

Вы можете использовать этот плагин, включив его из тега <script> или установив с помощью менеджера пакетов.

Через CDN

Вы можете подключить CDN-сборку этого плагина с помощью тега <script>, только подключать нужно ДО основного JS-файла Alpine.

<!-- Morph Plugin -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/morph@3/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>

Через менеджер пакетов

Вы можете установить Morph для использования внутри вашей сборки следующим образом:

Окно терминала
npm i @alpinejs/morph

Затем инициализируйте его в своей сборке:

import Alpine from 'alpinejs';
import morph from '@alpinejs/morph';
Alpine.plugin(morph);
Alpine.start();

Alpine.morph()

Функция Alpine.morph(el, newHtml) позволяет императивно изменить узел DOM на основе переданного HTML. Она принимает следующие параметры:

ПараметрОписание
elDOM-элемент на странице.
newHtmlСтрока HTML, используемая в качестве шаблона для преобразования элемента DOM.
options (необязательный)Объект параметров, используемый в основном для внедрения перехватчиков жизненного цикла.

Приведём пример использования Alpine.morph() для обновления компонента Alpine с помощью нового HTML: (В реальных приложениях этот новый HTML, скорее всего, будет поступать с сервера):

<div x-data="{ message: 'Измените меня, затем нажмите кнопку!' }" id="morph-demo-1" class="space-y-2">
<input type="text" x-model="message" />
<span x-text="message"></span>
</div>
<button id="morph-button-1">Преобразовать</button>
<script>
document.addEventListener('alpine:init', () => {
document.querySelector('#morph-button-1').addEventListener('click', () => {
let el = document.querySelector('#morph-demo-1');
Alpine.morph(
el,
`
<div x-data="{ message: 'Измените меня, затем нажмите кнопку!' }" id="morph-demo-1" class="space-y-2">
<h4>Посмотрите, как были добавлены новые элементы</h4>
<input type="text" x-model="message">
<span x-text="message"></span>
<h4>Но состояние этого компонента не изменилось? Магия!</h4>
</div>
`
);
});
});
</script>

Хуки жизненного цикла

Плагин «Morph» работает путём сравнения двух деревьев DOM, живого элемента и переданного в HTML.

Morph одновременно обходит оба дерева и сравнивает каждый узел и его дочерние элементы. Если он находит различия, он «исправляет» (изменяет) текущее дерево DOM, чтобы оно соответствовало переданному в дереве HTML.

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

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

Параметр или функцияОписание
elЭто всегда актуальный, текущий элемент DOM на странице, который будет «исправлен» (изменен Morph).
toElЭто «элемент шаблона». Это временный элемент, представляющий то, к чему будет привязан живой el. Он никогда не появится на странице и должен использоваться только в справочных целях.
childrenOnly()Это функция, которую можно вызвать внутри хука, чтобы сообщить Morph пропустить текущий элемент и «исправить» только его дочерние элементы.
skip()Функция, которая при вызове внутри хука «пропускает» сравнение/исправление себя и дочерних элементов текущего элемента.

Вот доступные хуки жизненного цикла (передаются в качестве третьего параметра в Alpine.morph(..., options)):

Наименование хукаОписание
updating(el, toEl, childrenOnly, skip)Вызывается перед исправлением el с помощью сравнения toEl.
updated(el, toEl)Вызывается после того, как Morph исправил el.
removing(el, skip)Вызывается перед тем, как Morph удаляет элемент из живого DOM.
removed(el)Вызывается после того, как Morph удалил элемент из действующего DOM.
adding(el, skip)Вызывается перед добавлением нового элемента.
added(el)Вызывается после добавления нового элемента в действующее дерево DOM.
key(el)Функция многократного использования, позволяющая определить, как Morph «ключит» элементы в дереве перед сравнением/исправлением. Подробнее об этом здесь
lookaheadЛогическое значение, указывающее Morph включить дополнительную функцию в его алгоритме, которая «заглядывает вперед», чтобы гарантировать, что элемент DOM, который собирается быть удален, вместо этого должен быть просто «перемещен» в более поздний родственный элемент.

Вот код всех этих хуков жизненного цикла для более конкретной информации:

Alpine.morph(el, newHtml, {
updating(el, toEl, childrenOnly, skip) {
//
},
updated(el, toEl) {
//
},
removing(el, skip) {
//
},
removed(el) {
//
},
adding(el, skip) {
//
},
added(el) {
//
},
key(el) {
// По умолчанию Alpine использует HTML-атрибут `key=""`.
return el.id;
},
lookahead: true, // По умолчанию: false
});

Ключи

Утилиты, различающие Dom, такие как Morph, изо всех сил стараются точно «преобразовать» исходный DOM в новый HTML. Однако бывают случаи, когда невозможно определить, следует ли просто заменить элемент или заменить его полностью.

Из-за этого ограничения в Morph есть «ключевая» система, которая позволяет разработчикам «принудительно» сохранять определённые элементы, а не заменять их.

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

<!-- «Живой» DOM на странице: -->
<ul>
<li>Вася</li>
<li>Петя</li>
<li>Ваня</li>
</ul>
<!-- Новый HTML для «преобразования»: -->
<ul>
<li>Ваня</li>
<li>Вася</li>
<li>Петя</li>
</ul>

Учитывая описанную выше ситуацию, Morph не имеет возможности узнать, что узел «Ваня» был перемещен в дереве DOM. Он просто думает, что «Вася» был изменен на «Ваня», а «Ваня» изменен на «Петя».

На самом деле это не то, чего мы хотим, мы хотим, чтобы Morph сохранял исходные элементы и вместо их изменения ПЕРЕМЕЩАЛ их внутри <ul>.

Добавляя ключи к каждому узлу, мы можем сделать это следующим образом:

<!-- «Живой» DOM на странице: -->
<ul>
<li key="1">Вася</li>
<li key="2">Петя</li>
<li key="3">Ваня</li>
</ul>
<!-- Новый HTML для «преобразования»: -->
<ul>
<li key="3">Ваня</li>
<li key="1">Вася</li>
<li key="2">Петя</li>
</ul>

Теперь, когда у элементов <li> есть «ключи», Morph сопоставит их в обоих деревьях и переместит соответствующим образом.

Вы можете настроить то, что Morph считает «ключом», с помощью опции конфигурации key:. Подробнее об этом здесь

Проверка знаний

  1. Как внедрить перехватчики жизненного цикла с помощью функции Alpine.morph()?

  2. Какой хук вызывается перед добавлением нового элемента?