Компоненты без рендеринга в Vue

Vue.js
Компоненты без рендеринга в Vue

Специальная оговорка: содержание этой статьи взято из@Adam Wathanиз"Renderless Components in Vue.js" статья.

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

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

Например, элемент управления вводом тегов (Tag input control):

Этот компонент обладает некоторыми интересными свойствами:

  • это не позволит вам добавить дубликаты
  • он не позволит вам добавлять пустые теги
  • Между тегами автоматически добавляются пробелы
  • когда пользователь нажимаетEnterключ, метка добавляется автоматически
  • когда пользователь нажимает наxтеги удаляются, когда

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

Что делать, если вам нужно, чтобы он выглядел немного иначе?

Этот компонент ведет себя так же, как и предыдущие компоненты, но с заметными отличиями в макете и стиле:

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

слот с прицелом

Во Вью,slot— это элемент-заполнитель в компоненте, который заменяется содержимым, переданным родителем или потребителем.

Если вы столкнулись с VueslotДля получения соответствующих знаний рекомендуется уделить время прочтению учебных заметок »Распределение содержимого компонентов Vue (slot)".

<!-- Card.vue -->
<template>
    <div class="card">
        <div class="card-header">
            <slot name="header"></slot>
        </div>
        <div class="card-body">
            <slot name="body"></slot>
        </div>
    </div>
</template>

<!-- Parent or Consumer -->
<card>
    <h1 slot="header">Special Features</h1>
    <div slot="body">
        <h5>Fish and Chips</h5>
        <p>Super delicious tbh.</p>
    </div>
</card>

<!-- Renders: -->
<div class="card">
    <div class="card-header">
        <h1>Special Features</h1>
    </div>
    <div class="card-body">
        <div>
            <h5>Fish and Chips</h5>
            <p>Super delicious tbh.</p>
        </div>
    </div>
</div>

слоты с ограниченной областью действия (slot-scope) и обычные слоты (slot), ноВозможность передавать параметры от дочерних компонентов к родительским компонентам или потребителям.

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

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

Деконструкция:slot-scopeЗначение на самом деле является допустимым выражением JavaScript, которое может появляться в позициях параметров сигнатуры функции. Это означает, что в поддерживаемых средах (однофайловые компоненты или современные браузеры) вы также можете использовать деструктуризацию ES2015 в выражениях.

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

<!-- LinkList.vue -->
<template>
    <!-- ... -->
    <li v-for="link in links">
        <slot name="link" :link="link"></slot>
    </li>
    <!-- ... -->
</template>

<!-- Parent or Consumer -->
<links-list>
    <a slot="link" slot-scope="{ link }" :href="link.href">
        {{ link.title }}
    </a>
</links-list>

вLinksListкомпонент добавлен:linkприписывать<slot>элемент, родительский узел (родительский элемент или потребитель) теперь может быть передан черезslot-scopeполучить к нему доступ иslotиспользовать его в шаблоне.

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

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

данные

Простейшим аналогом атрибутов данных являетсяdata:String,Numbers,Boolean,Arrayа такжеObjectи т.п.

существуетlinksВ примереlinkявляется примером свойства данных, которое представляет собой просто объект с некоторыми свойствами:

<!-- LinksList.vue -->
<template>
    <!-- ... -->
        <li v-for="link in links">
            <slot name="link" :link="link"></slot>
        </li>
    <!-- ... -->
</template>

<script>
export default {
    data() {
        return {
            links: [
                { 
                    href: 'http://...', 
                    title: 'First Link', 
                    bookmarked: true 
                },
                { 
                    href: 'http://...', 
                    title: 'Second Link', 
                    bookmarked: false 
                },
                // ...
            ]
        }
    }
}
</script>

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

<!-- Parent or Consumer -->
<links-list>
    <div slot="link" slot-scope="{ link }">
        <star-icon v-show="link.bookmarked"></star-icon>
        <a :href="link.href">
            {{ link.title }}
        </a>
    </div>
</links-list>

Действия

Атрибут действия — это функция, предоставляемая дочерним компонентом, которую родительский компонент (или потребитель) может вызвать для вызова некоторого поведения в дочернем компоненте. Например, мы можем положитьbookmarkДействие (Action) передается родительскому узлу ссылки:

<!-- LinksList.vue -->
<template>
    <!-- ... -->
    <li v-for="link in links">
        <slot name="link" :link="link" :bookmark="bookmark"></slot>
    </li>
    <!-- ... -->
</template>

<script>
    export default {
        data() {
            // ...
        },
    methods: {
        bookmark(link) {
            link.bookmarked = true
        }
    }
}
</script>

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

<!-- Parent/Consumer -->
<links-list>
    <div slot="link" slot-scope="{ link, bookmark }">
        <star-icon v-show="link.bookmarked"></star-icon>
        <a :href="link.href">{{ link.title }}</a>
        <button v-show="!link.bookmarked" @click="bookmark(link)">Bookmark</button>
    </div>
</links-list>

Жалюзи

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

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

<!-- LinksList.vue -->
<template>
    <!-- ... -->
    <li v-for="link in links">
        <slot name="link"
            :link="link"
            :bookmark="bookmark"
            :bookmarkButtonAttrs="{
                style: [ link.bookmarked ? { display: none } : {} ]
            }"
            :bookmarkButtonEvents="{
                click: () => bookmark(link)
            }"
        ></slot>
    </li>
    <!-- ... -->
</template>

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

<!-- Parent/Consumer -->
<links-list>
    <div slot="link" slot-scope="{ link, bookmarkButtonAttrs, bookmarkButtonEvents }">
        <star-icon v-show="link.bookmarked"></star-icon>
        <a :href="link.href">{{ link.title }}</a>
        <button v-bind="bookmarkButtonAttrs" v-on="bookmarkButtonEvents">Bookmark</button>
    </div>
</links-list>

нет компонента рендеринга

Компонент без рендеринга — это компонент, которому не нужно рендерить какой-либо собственный HTML. Вместо этого он только управляет состоянием и поведением. Он предоставляет отдельную область, предоставляя родительскому компоненту или потребителю полный контроль над тем, что должно быть отображено.

Компоненты без рендеринга отображают именно то, что вы передаете, без каких-либо дополнительных элементов.

<!-- Parent or Consumer -->
<renderless-component-example>
    <h1 slot-scope="{}">
        Hello world!
    </h1>
</renderless-component-example>

<!-- Renders: -->
<h1>Hello world!</h1>

Почему это полезно?

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

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

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

Так как же это достигается?

Структура компонента без рендеринга

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

Базовая структура компонента без рендеринга выглядит следующим образом:

Vue.component('renderless-component-example', {
    // Props, data, methods, etc.
    render() {
        return this.$scopedSlots.default({
            exampleProp: 'universe',
        })
    },
})

это неtemplate, а также не отображает свой собственный HTML; вместо этого он используетrender()функция, который вызывает слот с областью действия по умолчанию с любым слотом и возвращает результат.

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

<!-- Parent/Consumer -->
<renderless-component-example>
    <h1 slot-scope="{ exampleProp }">
        Hello {{ exampleProp }}!
    </h1>
</renderless-component-example>

<!-- Renders: -->
<h1>Hello universe!</h1>

кейс

Давайте создадим неотрендеренную версию элемента управления вводом метки с нуля. Мы начнем с пустого компонента без слотов:

// Renderless Tags Input Component
Vue.component('renderless-tags-input', {
    render () {
        return this.$scopedSlots.default({})
    }
})

Существует также родительский компонент (который будетdiv#appкак родительский компонент) со статическим неинтерактивным пользовательским интерфейсом, который передается дочернему компоненту.slot:

<div id="app">
    <renderless-tags-input>
        <div slot-scope="{}" class="tags-input">
            <span class="tags-input-tag">
                <span>Testing</span>
                <span>Design</span>
                <button type="button" class="tags-input-remove">×</button>
            </span>
            <input class="tags-input-text" placeholder="Add tag...">
        </div>
    </renderless-tags-input>
</div>

Чтобы визуализированный эффект выглядел лучше, добавьте немного кода CSS, эффект, который вы увидите в браузере, выглядит следующим образом:

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

список тегов

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

Сначала мы даем компонентpropsдобавить одинvalueценность какtagsСвойства слота:

// Renderless Tags Input Component
Vue.component('renderless-tags-input', {
    props: ['value'],
    render () {
        return this.$scopedSlots.default({
            tags: this.value
        })
    }
})

Далее в родительский узел добавимv-modelПривязать к родительскому компоненту,tagsотslot-scopeЭкстракт и использоватьv-forПеребрать его:

<div id="app">
    <renderless-tags-input v-model="tags">
        <div slot-scope="{ tags }" class="tags-input">
            <span class="tags-input-tag" v-for="tag in tags">
                <span>{{ tag }}</span>
                <button type="button" class="tags-input-remove">×</button>
            </span>
            <input class="tags-input-text" placeholder="Add tag...">
        </div>
    </renderless-tags-input>
</div>

let app = new Vue({
    el: '#app',
    data () {
        return {
            tags: ['Tesing', 'Design']
        }
    }
})

В настоящее время наблюдается следующий эффект:

Невозможно добавить тег после ввода тега в поле ввода и нажатия клавиши «Назад», но мы можем изменить его в консоли браузера.app.tagsзначение для имитации. например, прохождение.push()ДатьtagsДобавьте несколько значений на массив и увидеть эффект:

Этот слот — хороший пример простого атрибута данных (Data Prop).

убрать тэг

Чтобы сделать следующее, нажмитеxкнопку для удаления соответствующей метки. Добавить новый компонент в компонентremoveTagметод и передать ссылку на метод родительскому компоненту какslotАтрибуты.

// Renderless Tags Input Component
Vue.component('renderless-tags-input', {
    props: ['value'],
    methods: {
        removeTag: function (tag) {
            this.$emit('input', this.value.filter(t => t !== tag))
        }
    },
    render () {
        return this.$scopedSlots.default({
            tags: this.value,
            removeTag: this.removeTag
        })
    }
})

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

<div id="app">
    <renderless-tags-input v-model="tags">
        <div slot-scope="{ tags, removeTag }" class="tags-input">
            <span class="tags-input-tag" v-for="tag in tags">
                <span>{{ tag }}</span>
                <button type="button" class="tags-input-remove" @click="removeTag(tag)">×</button>
            </span>
            <input class="tags-input-text" placeholder="Add tag...">
        </div>
    </renderless-tags-input>
</div>

вы нажимаете на каждую вкладкуxкнопку, вы можете удалить соответствующую метку, эффект будет следующим:

Этот слот является примером типичного реквизита действия.

добавить новую вкладку

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

// Renderless Tags Input Component
Vue.component('renderless-tags-input', {
    props: ['value'],
    data () {
        return {
            newTag: ''
        }
    },
    methods: {
        removeTag: function (tag) {
            this.$emit('input', this.value.filter(t => t !== tag))
        },
        addTag: function () {
            if (this.newTag.trim().length === 0 || this.value.includes(this.newTag.trim())) {
                return
            }
            this.$emit('input', [...this.value, this.newTag.trim()])
            this.newTag = ''
        }
    },
    render () {
        return this.$scopedSlots.default({
            tags: this.value,
            removeTag: this.removeTag
        })
    }
})

<div id="app">
    <renderless-tags-input v-model="tags">
        <div slot-scope="{ tags, removeTag }" class="tags-input">
            <span class="tags-input-tag" v-for="tag in tags">
                <span>{{ tag }}</span>
                <button type="button" class="tags-input-remove" @click="removeTag(tag)">×</button>
            </span>
            <input @keydown.enter.prevent="addTag" v-model="newTag" class="tags-input-text" placeholder="Add tag...">
        </div>
    </renderless-tags-input>
</div>    

мы вnewTagсвойство для отслеживания новых тегов (до их добавления) и использованияv-modelпривязать свойство кinputначальство. Как только пользователь нажимаетEnterключ, убедитесь, что тег действителен, затем добавьте его в список, затем снимитеinputсодержание в.

Вопрос здесь в том, как нам пройти слот действияv-modelпривязка?

Если вы хорошо знаете Vue, вы, вероятно, знаетеv-modelна самом деле просто:valueсвязывание свойств и@inputСинтаксический сахар для привязки событий:

<div id="app">
    <renderless-tags-input v-model="tags">
        <div slot-scope="{ tags, removeTag }" class="tags-input">
            <span class="tags-input-tag" v-for="tag in tags">
                <span>{{ tag }}</span>
                <button type="button" class="tags-input-remove" @click="removeTag(tag)">×</button>
            </span>
            <input @keydown.enter.prevent="addTag" :value="newTag" @input="(e) => newTag = e.target.value" class="tags-input-text" placeholder="Add tag...">
        </div>
    </renderless-tags-input>
</div>   

Примечание. После запуска приведенного выше кода в вашем браузере в это время браузер выдаст сообщение об ошибке: «ReferenceError: newTag не определен». Если вы похожи на меня, если вы не понимаете, почему вы бросаете это неправильно, не волнуйтесь, пожалуйста, читайте дальше.

Это означает, что мы можем справиться с этим поведением в нашем компоненте Renderless с несколькими изменениями:

  • добавить локальный компонентnewTagатрибут данных
  • использовать:valueсвязыватьnewTag, передавая связанное свойство
  • использовать@keydown.enterсвязыватьaddTagа также@inputсвязыватьnewTag, передавая связанное событие

Продолжайте смотреть на код:

// Renderless Tags Input Component
Vue.component('renderless-tags-input', {
    props: ['value'],
    data () {
        return {
            newTag: ''
        }
    },
    methods: {
        removeTag: function (tag) {
            this.$emit('input', this.value.filter(t => t !== tag))
        },
        addTag: function () {
            if (this.newTag.trim().length === 0 || this.value.includes(this.newTag.trim())) {
                return
            }
            this.$emit('input', [...this.value, this.newTag.trim()])
            this.newTag = ''
        }
    },
    render () {
        return this.$scopedSlots.default({
            tags: this.value,
            removeTag: this.removeTag,
            inputAttrs: {
                value: this.newTag
            },
            inputEvents: {
                input: (e) => { this.newTag = e.target.value },
                keydown: (e) => {
                    if (e.keyCode === 13) {
                        e.preventDefault()
                        this.addTag()
                    }
                }
            }
        })
    }
})

Теперь нам просто нужно привязать эти свойства к родительскому элементу.inputНа элементе:

<div id="app">
    <renderless-tags-input v-model="tags">
        <div slot-scope="{ tags, removeTag, inputAttrs, inputEvents }" class="tags-input">
            <span class="tags-input-tag" v-for="tag in tags">
                <span>{{ tag }}</span>
                <button type="button" class="tags-input-remove" @click="removeTag(tag)">×</button>
            </span>
            <input v-bind="inputAttrs" v-on="inputEvents" class="tags-input-text" placeholder="Add tag...">
        </div>
    </renderless-tags-input>
</div>   

в это время тыinputвведите значение и нажмитеEnterКогда вы можете добавлять теги в обычном режиме:

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

В текущем макете пользовательinputНовый тег можно добавить, введя новый тег в элемент и нажав клавишу Enter. Но легко представить сценарий, в котором кто-то может захотеть предоставить кнопку, которую пользователь может щелкнуть, чтобы добавить новую метку.

Достичь такой функции на самом деле очень просто, все, что вам нужно сделать, этоaddTagСсылка на методslotВ рамках:

// Renderless Tags Input Component
Vue.component('renderless-tags-input', {
    props: ['value'],
    data () {
        return {
            newTag: ''
        }
    },
    methods: {
        removeTag: function (tag) {
            this.$emit('input', this.value.filter(t => t !== tag))
        },
        addTag: function () {
            if (this.newTag.trim().length === 0 || this.value.includes(this.newTag.trim())) {
                return
            }
            this.$emit('input', [...this.value, this.newTag.trim()])
            this.newTag = ''
        }
    },
    render () {
        return this.$scopedSlots.default({
            tags: this.value,
            removeTag: this.removeTag,
            inputAttrs: {
                value: this.newTag
            },
            inputEvents: {
                input: (e) => { this.newTag = e.target.value },
                keydown: (e) => {
                    if (e.keyCode === 13) {
                        e.preventDefault()
                        this.addTag()
                    }
                }
            },
            addTag: this.addTag
        })
    }
})

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

Вот пример эффекта компонента ввода метки без рендеринга, который мы создали:

Фактический компонент не содержит HTML, а родительский компонент, в котором мы определяем шаблон, не содержит поведения. Разве это не аккуратно и красиво, правда?

другой макет

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

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

Создайте свои собственные фиксированные компоненты контейнера

Вы можете увидеть некоторые примеры этого и подумать: «Вау, каждый раз, когда мне нужно добавить еще один экземпляр этого компонента тега, мне нужно написать много HTML!». Когда вам нужен ввод метки, определенно больше работы написать это:

<renderless-tags-input v-model="tags">
    <div class="tags-input" slot-scope="{ tags, removeTag, inputAttrs, inputEvents }">
        <span class="tags-input-tag" v-for="tag in tags">
            <span>{{ tag }}</span>
            <button type="button" class="tags-input-remove" @click="removeTag(tag)">×</button>
        </span>

        <input class="tags-input-text" placeholder="Add tag..." v-on="inputEvents" v-bind="inputAttrs">
    </div>
</renderless-tags-input>

Это то, что мы сделали в нашем начальном примере. Но есть более простое решение:Создайте контейнерный компонент!

<tags-input v-model="tags"></tags-input>

<tags-input>Компонент выглядит так:

<!-- InlineTagsInput.vue -->
<template>
    <renderless-tags-input :value="value" @input="(tags) => { $emit('input', tags) }">
        <div class="tags-input" slot-scope="{ tag, removeTag, inputAttrs, inputEvents }">
            <span class="tags-input-tag" v-for="tag in tags">
                <span>{{ tag }}</span>
                <button type="button" class="tags-input-remove" @click="removeTag(tag)">×</button>
            </span>

            <input class="tags-input-text" placeholder="Add tag..."
                v-bind="inputAttrs"
                v-on="inputEvents"
            >
        </div>
    </renderless-tags-input>
</template>

<script>
    export default {
        props: ['value'],
    }
</script>

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

<tags-input v-model="tags"></tags-input>

Например, два примера, показанные в начале статьи.

более крутые компоненты

Как только вы осознаете, что компоненты не визуализируются, вы сможете делать еще более безумные вещи таким образом. Например, вотfetch-dataкомпонент, который начинается сurlтак какpropsстоимость имущества, отURLПолучите данные JSON и передайте их родительскому компоненту. Возьмем следующий пример:

Суммировать

Декомпозиция компонента на презентационный компонент и компонент без рендеринга — очень полезный шаблон, облегчающий повторное использование компонента. Тем не менее, это не означает, что оно всегда того стоит.

Если выполняются следующие условия, рекомендуется использовать этот метод без компонентов рендеринга:

  • Вы создаете библиотеку и хотите, чтобы пользователи могли более легко настраивать внешний вид (эффекты стиля) компонентов.
  • В вашем проекте несколько компонентов, или их поведение очень похоже, но разные макеты.

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