Renderless Components: работа с компонентами во Vue.js

Renderless Components: работа с компонентами во Vue.js

От автора: сегодня мы поговорим про Vue js компоненты. Вы когда-нибудь брали сторонний UI компонент только, чтобы обнаружить, что из-за одной маленькой настройки вам придется выбросить весь пакет?

Очень сложно создавать пользовательские элементы типа выпадающих списков, виджетов дат и полей с автокомплитером с множеством неожиданных крайних случаев.

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

Разберем в качестве примера этот элемент с тегами:

У компонента есть несколько интересных сценариев:

Он не позволяет добавлять дубликаты

Он не позволяет добавлять пустые теги

Он обрезает проблемы у тегов

Теги добавляются по нажатию клавиши enter

Теги удаляются по нажатию на иконку x

Если вам нужен такой компонент в проекте, то получение его пакета и логики определенно сэкономит вам время и силы.

А если вам нужно слегка переделать внешний вид?

Компонент ниже имеет полностью то же поведение, что и предыдущий, но его макет значительно отличается:

Можно попробовать поддерживать оба макета в одном компоненте через костыли CSS и опции настроек компонента, но есть способ лучше.

Scoped Slots

В Vue.js slots – это алейсхолдер элементы в компоненте, которые заменяются контентом, который передается через родителя/потребителя:

Scoped slots похожи на обычные слоты, но с возможностью передавать параметры из дочерних компонентов наверх к родителю/потребителю.

Обычные слоты – это передача HTML в компонент. Scoped slots – это передача колбека, который принимает данные и возвращает HTML.

Параметры передаются к родителю путем добавления свойств в элемент слота в дочернем компоненте, после чего родитель получает доступ к этим параметрам путем деструктуризации их из специального атрибута slot-scope.

Пример компонента LinksList со scoped slot для каждого элемента списка, который передает данные для каждого элемента обратно к родителю через свойство :link:

Родитель получает доступ через slot-scope и использует его в слот шаблоне путем вставки свойства :link в слот элемент в компонент LinksList.

Типы слот свойств

В слот можно передавать что угодно, но мне удобно разделять все слот свойства на 3 категории.

Данные

Данные – простейший тип слот свойства: строки, числа, Булевы значения, массивы, объекты и т.д. В нашем примере со ссылками link – это пример свойства данных. Это просто объект со свойствами:

Родитель может рендерить эти данные или использовать их, чтобы решить, что рендерить:

Действия

Свойства действий – это функции дочернего компонента, которые родитель может вызывать для запуска некого поведения в дочернем компоненте.

Например, можно передать действие bookmark в родителя, и оно добавит в закладки ссылку:

Родитель может выполнить это действие, когда пользователь кликнет на кнопку рядом со ссылкой, которой нет в закладках:

Привязки

Привязки – это коллекции атрибутов или обработчиков событий, которые ограничиваются определенным элементом с помощью v-bind или v-on.

Они полезны, когда нужно инкапсулировать детали реализации о том, как работать с предоставленным элементом.

Например, вместо того, чтобы потребитель обрабатывал v-show и @click для кнопок добавления в закладки можно предоставить привязки bookmarkButtonAttrs и bookmarkButtonEvents, которые передадут эти детали в сам компонент:

Если потребитель хочет, то теперь можно применить эти привязки к кнопкам bookmark вслепую, не зная, что они делают:

Renderless Components

Renderless Components – это компоненты, которые не рендерят свой HTML. Они просто управляют состоянием и поведением, предоставляя один scoped slot, который позволяет родителю/потребителю контролировать рендеринг. renderless component рендерят ровно то, что вы в них передадите без дополнительных элементов:

Так в чем их польза?

Разделение представления и поведения

Renderless components работают только с состоянием и поведением, поэтому они не участвуют в дизайне и макете.

То есть если вы сможете найти способ вытащить все интересное поведение из компонента UI, как наш пример с тегами, в renderless component, вы сможете повторно использовать renderless component для реализации любого макета с тегами.

Ниже представлено два макета с тегами, но на этот раз под управлением одного renderless component:

Как это работает?

Структура Renderless Component

Renderless component обладает одним scoped slot, где потребитель может предоставить целый шаблон для рендера. Базовый скелет renderless component:

В нем нет шаблона, он не рендерит свой HTML. Он использует функцию render, которая выполняет scoped slot по умолчанию, передавая в него любые слот свойства, и возвращает результат.

Любой родитель/потребитель этого компонента может деструктурировать exampleProp из slot-scope и использовать его в своем шаблоне:

Рабочий пример

Давайте создадим renderless версию тегов с нуля. Начнем с пустого renderless component, который не передает слот свойства:

… и родительского компонента со статичным неинтерактивным UI, который мы передаем в слот дочернего компонента:

По частям мы приведем в работу этот компонент, добавляя состояние и поведение в renderless component и открывая его нашему макету через slot-scope.

Листинг теги

Во-первых, давайте заменим статичный список тегов на динамичный.

Компонент ввода тегов – это пользовательский элемент формы. Поэтому как в оригинальном примере, теги должны быть в родителе и должны быть ограничены компонентом через v-model.

Начнем с добавления свойства value в компонент и его передачи вверх в виде слот свойства tags:

Затем добавим привязку v-model в родителя, получим теги из slot-scope и пробежимся по ним в цикле через v-for:

Это слот свойство – отличный пример простого свойства данных.

Удаление тегов

Теперь давайте удалим тег по клику на кнопку x.

Добавим новый метод removeTag в наш компонент и передадим ссылку на этот метод вверх в родителя в виде слот свойства:

Теперь добавим обработчик @click для кнопки в родителе, которая вызывает removeTag с текущим тегом:

Это слот свойство – пример свойства действия.

Добавление новых тегов по enter

Добавление новых тегов немного сложнее, чем два последних примера.

Чтобы понять почему, давайте разберем, как бы это было реализовано в обычном компоненте:

Мы отслеживаем новый тег (до его добавления) в свойстве newTag и привязываем это свойство к полю через v-model. После нажатия на enter мы проверяем тег на валидность, добавляем его в список и очищаем поле ввода.

Возникает вопрос – как передать v-model привязку через scoped slot? Если вы хорошо знаете Vue, вы можете знать, что v-model представляет собой лишь синтаксический сахар для привязки атрибута :value и привязки события @input:

То есть мы можем обработать это поведение в нашем renderless component, внеся пару изменений:

Добавим локальное свойство данных newTag в компонент

Вернем свойство привязки атрибута, которое привязывает :value к newTag

Вернем свойство привязки события, которое привязывает @keydown.enter к addTag и @input к обновлению newTag

Осталось лишь привязать эти свойства к полю ввода в родителе:

Явное добавление новых тегов

В нашем примере пользователь добавляет новые теги путем ввода и нажатия enter. Легко представить ситуацию, когда кто-то захочет иметь кнопку, по клику на которую будут добавляться новые теги.

Сделать это легко. Нам лишь нужно передать ссылку на наш метод addTag в slot scope:

При проектировании renderless components как этот лучше перебрать с количеством слот свойств, чем не добрать. Потребителю нужно лишь деструктурировать необходимые свойства. Поэтому передача лишних свойств никак на них не повлияет.

Рабочее демо

Ниже представлено рабочее демо renderless компонента ввода тегов, которое мы создали:

Настоящий компонент не содержит HTML, а в родителе, где мы задали шаблон, нет поведения. Аккуратно, правда?

Альтернативный макет

Мы создали renderless версию поля ввода тегов. Теперь легко можно реализовать альтернативные макеты, написав любой HTML и применив слот свойства в нужных местах.

Ниже представлен пример с реализацией для вертикального макета из начала статьи с использованием нового renderless component:

Создание упрямых компонентов-оберток

Вы можете посмотреть на эти примеры и подумать «ого, нужно писать так много HTML, когда нужно добавить еще один объект этого компонента тегов!». И вы будете правы.

Нужно действительно много писать каждый раз, когда нужен элемент ввода тегов:

… в отличие от того, что было у нас в начале:

Но есть легкий фикс: создайте упрямый компонент-обертку!

Вот как выглядит наш оригинальный компонент в терминах renderless поля ввода тегов:

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

Сходим с ума

Как только вы понимаете, что компонент не должен рендерить что-либо и может предоставлять только данные, типы поведений, которые можно смоделировать с компонентом, становятся безлимитными.

Например, ниже представлен компонент fetch-data, который принимает URL как свойство, получает JSON из URL и передает ответ обратно к родителю:

Правильно ли так делать все AJAX запросы? Возможно, нет. Но это точно интересно!

Заключение

Разбиение компонента на компонент представления и renderless component — невероятно полезный шаблон, который стоит изучить. Он может облегчить повторное использование кода, но не всегда.

Используйте этот подход, если:

Вы пишите библиотеку и хотите упростить пользователям кастомизацию внешнего вида компонентов

У вас много компонентов в проекте с похожим поведением, но разными макетами

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

Автор: Adam Wathan

Источник: //adamwathan.me/

Редакция: Команда webformyself.

Метки:

Похожие статьи:

Комментарии Вконтакте: