Полное руководство по слотам Vue

Полное руководство по слотам Vue Изучение

Компоненты — это сердце разработки современных веб-приложений. Каждое приложение состоит из ряда компонентов, плавно сшитых вместе, чтобы работать как единое целое. Эти компоненты должны быть максимально гибкими и многоразовыми, чтобы их можно было использовать в разных ситуациях и даже в разных приложениях. Один из основных механизмов, используемых многими фреймворками для удовлетворения таких требований — в частности, Vue — называется «слот».

Слоты — это мощный и универсальный механизм распределения и композиции контента. Вы можете думать о слотах как о настраиваемых шаблонах (например, подобных шаблонам PHP), которые вы можете использовать в разных местах, для разных случаев использования, создавая разные эффекты. Например, в таких фреймворках пользовательского интерфейса, как Vuetify, слоты используются для создания общих компонентов, таких как компонент оповещения. В этих типах компонентов слоты используются в качестве заполнителей для содержимого по умолчанию и любого дополнительного / необязательного содержимого, такого как значки, изображения и т. Д.

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

В этом руководстве мы рассмотрим, как использовать возможности слотов в контексте Vue 3. Приступим.

Базовое использование слотов

По сути, Vue предлагает два типа слотов: простой слот и слот с ограниченной областью видимости. Начнем с простого. Рассмотрим следующий пример:
const app = Vue.createApp({})

const app = Vue.createApp({})

app.component('primary-button', {
  template: `
    <button>
      <slot>OK</slot>
    </button>`
})

app.mount('#app')

Здесь у нас есть основной компонент кнопки. Мы хотим, чтобы текст кнопки был настраиваемым, поэтому мы используем slotкомпонент внутри buttonэлемента, чтобы добавить заполнитель для текста. Нам также нужно общее значение по умолчанию (запасное) на случай, если мы не предоставим настраиваемое. Vue использует в качестве содержимого слота по умолчанию все, что мы помещаем в slotкомпонент. Поэтому мы просто помещаем текст «ОК» внутри компонента. Теперь мы можем использовать компонент так:

<div id="app">
  <primary-button></primary-button>
</div>

Результатом является кнопка с текстом «ОК», потому что мы не предоставили никакого значения. Но что, если мы хотим создать кнопку с настраиваемым текстом? В этом случае мы предоставляем собственный текст в реализации компонента следующим образом:

<div id="app">
  <primary-button>Subscribe</primary-button>
</div>

Здесь Vue берет пользовательский текст «Подписка» и использует его вместо текста по умолчанию.

Читайте также:  Log4Shell: 4 вывода для разработчиков в 2022 году

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

Компонент построения котировки дня

Теперь мы создадим компонент цитаты, который отображает цитату дня. Вот код:

const app = Vue.createApp({}) 

app.component('quote', {
  template: `
    <article>
      <h2>The quote of the day says:</h2>
      <p class="quote-text">
        <slot></slot>
      </p>
    </article>`
})

app.mount('#app')
<div id="app">
  <quote>
    <div class="quote-box">
      "Creativity is just connecting things."
      <br><br>
      - Steve Jobs
    </div>
  </quote>
</div>
.quote-box {
  background-color: lightgreen;
  width: 300px;
  padding: 5px 10px;
}

.quote-text {
  font-style: italic;
}

В этом примере мы создаем заголовок, содержание которого будет постоянным, а затем помещаем компонент слота внутри абзаца, содержание которого будет варьироваться в зависимости от цитаты текущего дня. Когда компонент визуализируется, Vue будет отображать заголовок из компонента цитаты, за которым следует контент, который мы помещаем в теги цитат. Также обратите внимание на классы CSS, используемые как при создании, так и при реализации цитаты. Мы можем стилизовать наши компоненты обоими способами в зависимости от наших потребностей.

Компонент «Цитата дня» работает нормально, но нам все равно нужно обновить цитату вручную. Сделаем его динамичным с помощью API Fav Quotes :

const app = Vue.createApp({   
  data() {
    return {
      quoteOfTheDay: null,
      show: false
    };
  },
  methods: {
    showQuote() {
      axios.get('https://favqs.com/api/qotd').then(result => {
        this.quoteOfTheDay = result.data
        this.show = true
      }); 
    }
  }
})

...

app.mount('#app')
<div id="app">
  <quote>
    <button v-if="show == false" @click="showQuote">Show Quote of the Day</button>
    <div v-if="show" class="quote-box">
      {{ quoteOfTheDay.quote.body }} 
      <br><br>
      - {{ quoteOfTheDay.quote.author }}
    </div>
  </quote>
</div>

Здесь мы используем AXIOS, чтобы сделать вызов к «Цитата дня» API конечной точки, а затем мы используем bodyи authorсвойства, из возвращенного объекта JSON, чтобы заполнить цитату. Таким образом, нам больше не нужно добавлять расценки вручную; это делается для нас автоматически.

Использование нескольких слотов

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

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

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

const app = Vue.createApp({})

app.component('card', {
  template: `
    <div class="container">
      <header>
        <slot name="header"></slot>
      </header>
      <main>
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>`
})

app.mount('#app')
<div id="app">
  <card>
    <template v-slot:header>
      <h2>Card Header Title</h2>
    </template>

    <template v-slot:default>
      <p>
        Lorem ipsum leo risus, porta ac consectetur ac, vestibulum at eros. Donec id elit non mi porta gravida at eget metus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras mattis consectetur purus sit amet fermentum.
      </p>
    </template>

    <template v-slot:footer>
      <a href="#">Save</a> -
      <a href="#">Edit</a> -
      <a href="#">Delete</a>
    </template>
  </card>
</div>

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

Когда мы используем cardкомпонент, мы должны использовать templateэлемент с v-slotдирективой с именем слота: v-slot:[slot-name].

Примечание: у v-slotдирективы есть сокращение, в котором используется специальный символ, #за которым следует имя слота. Так, например, вместо v-slot:header, мы можем написать #header.

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

Использование именованных слотов с компонентом карты Bulma

Давайте возьмем компонент Bulma’s Card и немного подправим его:

const app = Vue.createApp({})

app.component('card', {
  template: `
    <div class="container">
      <div class="card">
        <header class="card-header">
          <slot name="header"></slot>
        </header>
        <main class="card-content">
          <slot></slot>
        </main>
        <footer class="card-footer">
          <slot name="footer"></slot>
        </footer>
      </div>
    </div>`
})

app.mount('#app')
.container {
  width: 300px;
}
<div id="app">
  <card>
    <template v-slot:header>
      <p class="card-header-title">
        Card Header Title
      </p>
    </template>

    <template v-slot:default>
      <p>
        Lorem ipsum leo risus, porta ac consectetur ac, vestibulum at eros. Donec id elit non mi porta gravida at eget metus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras mattis consectetur purus sit amet fermentum.
      </p>
    </template>

    <template v-slot:footer>
      <a href="#" class="card-footer-item">Save</a>
      <a href="#" class="card-footer-item">Edit</a>
      <a href="#" class="card-footer-item">Delete</a>
    </template>
  </card>
</div>

Здесь мы используем классы из компонента Bulma Card в качестве базового каркаса и добавляем слот для каждого раздела (заголовок, контент, нижний колонтитул). Затем, когда мы добавляем контент, все структурируется правильно.

Использование слотов с заданной областью

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

Создание многоцелевого компонента списка

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

const app = Vue.createApp({
  data() {
    return {
      tasks: ['Reading a book', 'Buying vegetables', 'Going for a walk']
    }
  }
})

app.component('list', {
  props: ['items', 'name'],
  template: `
    <h3>{{ name }}</h3>
    <ul>
      <li v-for="item in items">
        <slot :item="item"></slot>
      </li>
    </ul>`
})

app.mount('#app')
<div id="app">
  <list name="My tasks list:" :items='tasks' v-slot="{ item: task }">
    <span>{{ task }}</span>
  </list>
</div>

Здесь мы создаем list компонент с помощью itemsи nameprops. nameБудет использоваться, чтобы дать название в списке и itemsбудет содержать данные, которые мы хотим в список. В шаблоне мы добавляем nameопору к заголовку над списком, а затем используем v-forдирективу для визуализации каждого отдельного элемента. Чтобы предоставить данные от дочернего элемента к родительскому, мы привязываем элемент как атрибут слота ( ).

В родительском у нас есть массив задач. Когда мы используем список, мы предоставляем свойство name, затем привязываем его itemsк tasksмассиву и используем его v-slotдля доступа к дочерним данным.

Примечание: весь связанный реквизит в слоте называется игровым реквизит, и мы можем подвергать их помощи v-slot:[slot-name]=»slotProps», а затем мы используем единую опору, как это: {{ slotProps.item }}. Но в этом примере я использую деструктуризацию объекта, что является более элегантным и прямым способом получения свойств объекта. Также он позволяет вам переименовывать свойства объекта (как я, переименовывая itemв task), что более гибко для различных типов списков.

Назначение нашего компонента списка — использовать в различных сценариях. Итак, допустим, мы хотим использовать его как список покупок. Вот как это сделать:

const app = Vue.createApp({
  data() {
    return {
      products: [
        {name: 'Tomatoes', quantity: '4'},
        {name: 'Cucumbers', quantity: '2'},
        {name: 'Red onion', quantity: '1'},
      ]
    }
  }
})

...

app.mount('#app')
<div id="app">
  <list name="My shopping list:" :items='products' v-slot="{ item: product }">
    <span>{{ product.quantity }} {{ product.name }}</span>
  </list>
</div>

Здесь, у нас есть список продуктов, где каждый элемент имеет nameи в quantityсобственность. Мы используем listкомпонент почти так же, как и в предыдущем примере, за исключением того, что здесь элемент списка имеет два свойства.

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

Слоты против реквизита

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

Чтобы проиллюстрировать сказанное выше, мы возьмем пример из Tailwind CSS, в котором для создания vacation-cardкомпонента используются только реквизиты :

const app = Vue.createApp({})

app.component('vacation-card', {
  props: ['url', 'img', 'imgAlt', 'eyebrow', 'title', 'pricing'],
  template: `
    <div class="m-5 shadow-md w-80">
      <img class="rounded" :src="img" :alt="imgAlt">
      <div class="p-2">
        <div>
          <div class="text-xs text-gray-600 uppercase font-bold">{{ eyebrow }}</div>
          <div class="font-bold text-gray-700 leading-snug">
            <a :href="url" class="hover:underline">{{ title }}</a>
          </div>
          <div class="mt-2 text-sm text-gray-600">{{ pricing }}</div>
        </div>
      </div>
    </div>`
})

app.mount('#app')
<div id="app">
  <vacation-card
    url="/vacations/cancun"
    img="https://images.unsplash.com/photo-1452784444945-3f422708fe5e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=512&q=80"
    imgAlt="Beach in Cancun"
    eyebrow="Private Villa"
    title="Relaxing All-Inclusive Resort in Cancun"
    pricing="$299 USD per night"
  >
  </vacation-card>
</div>

Как видите, для предоставления всех необходимых данных / контента этот vacation-cardкомпонент использует шесть свойств. Это определенно делает его многоразовым, но и сложным в обслуживании. Мы можем создать более понятную и удобную в обслуживании версию, используя слоты вместо некоторых реквизитов.

Перепишем компонент со слотами:

const app = Vue.createApp({})

app.component('vacation-card', {
  props: ['url', 'img', 'imgAlt'],
  template: `
  <div class="m-5 shadow-md w-80">
    <img class="rounded" :src="img" :alt="imgAlt"> 
    <div class="p-2">
      <div>
        <div class="text-xs text-gray-600 uppercase font-bold"><slot name="eyebrow"></slot></div>
        <div class="font-bold text-gray-700 leading-snug">
          <a :href="url" class="hover:underline"><slot name="title"></slot></a>
        </div>
        <div class="mt-2 text-sm text-gray-600"><slot name="pricing"></slot></div>
      </div>
    </div>
  </div>`
})

app.mount('#app')
<div id="app">
  <vacation-card
    url="/vacations/cancun"
    img="https://images.unsplash.com/photo-1452784444945-3f422708fe5e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=512&q=80"
    imgAlt="Beach in Cancun"
  >
    <template v-slot:eyebrow>
      <p>Private Villa</p>
    </template>
    <template v-slot:title>
      <p>Relaxing All-Inclusive Resort in Cancun</p>
    </template>
    <template v-slot:pricing>
      <p><mark>$299 USD per night</mark></p>
    </template>
  </vacation-card>
</div>

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

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

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

Итог: для достижения наилучших результатов комбинируйте реквизит и прорези! 😀

Изучение других сценариев использования слотов

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

Повторное использование функциональности со слотами

Слоты могут предоставлять не только контент / структуру, но и функциональность. Давайте посмотрим на это в действии:

const app = Vue.createApp({
  data() {
    return {
      counter: 1
    }
  },
  methods: {
    increment() {
      this.counter++
    }
  }
})

app.component('double-counter', {
  props: ['counter'],
  template: `
    <p>
      <slot :double="double"></slot>
    </p>`,
  computed: {
    double() {
      return this.counter * 2
    }
  }
})

app.mount('#app')
<div id="app">
  <double-counter :counter="counter">
    <template v-slot:default="{ double }">
      <h2>{{ counter }} x 2 = {{ double }}</h2>
      <button @click="increment">
        Increment counter by 1
      </button>
    </template>
  </double-counter>
</div>

Здесь мы создаем double-counterкомпонент, который удваивает значение счетчика. Для этого мы создаем вычисляемое свойство, doubleкоторое предоставляем родительскому объекту, привязывая его значение как атрибут слота. Вычисляемый принимает значение counterопоры и удваивает его.

В родительском элементе у нас есть counterсвойство данных и increment()метод, увеличивающий их на единицу. Когда мы используем double-counter, мы привязываем опору counter, а затем предоставляем doubleвычисленное свойство. В выражении мы используем counterи double. Когда мы нажимаем кнопку, counterувеличивается на 1, и вычисляемое свойство пересчитывается с новым удвоенным значением. Например, если для counterсвойства задано значение 3, удвоенное значение будет 6 ( 3×2 = 6).

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

const app = Vue.createApp({
  data() {
    return {
      counter: 1,
      by: 4
    }
  },
  methods: {
    increment() {
      this.counter++
    }
  }
})

app.component('multiply-counter', {
  props: ['counter', 'by'],
  template: `
    <p>
      <slot :multiply="multiply"></slot>
    </p>`,
  computed: {
    multiply() {
      return this.counter * this.by
    }
  }
})

app.mount('#app')
<div id="app">
  <multiply-counter :by="by" :counter="counter">
    <template v-slot:default="{ multiply }">
      <h2>{{ counter }} x {{ by }} = {{ multiply }}</h2>
      <button @click="increment">
        Increment counter by 1
      </button>
    </template>
  </multiply-counter>
</div>

Здесь мы добавляем опору, byкоторая устанавливает число умножения, и меняем doubleвычисляемое свойство на multiply, которое умножает counterна заданное число.

В родительском элементе мы добавляем byсвойство данных и привязываем его к byопоре. Итак, теперь, если мы установим для byсвойства data значение 3 и значение counter4, результат будет 4×3 = 12.

Использование слотов в компонентах без рендеринга

Другой эффективный способ использования слотов — поместить их в компонент без рендеринга. Компонент без рендеринга не имеет templateэлемента. Он имеет функцию рендеринга, которая предоставляет единственный слот с ограниченным объемом. Давайте создадим версию нашего многоцелевого списка без рендеринга:

const app = Vue.createApp({
  data() {
    return {
      products: [
        {name: 'Tomatoes', quantity: '4'},
        {name: 'Cucumbers', quantity: '2'},
        {name: 'Red onion', quantity: '1'},
      ]
    }
  }
})

app.component('renderless-list', {
  props: ['items', 'name'], 
  render() {
    return this.$slots.default({
      items: this.items,
      name: this.name 
    });
  }
})

app.mount('#app')
<div id="app">
  <renderless-list name="My shopping list:" :items="products">
    <template v-slot:default="{name, items: products}">
      <h3>{{ name }}</h3>
      <ul>
        <li v-for="product in products" :key="product.name">
          {{ product.quantity }} {{ product.name }}
        </li>
      </ul>
    </template>
  </renderless-list>
</div>

Здесь мы создаем renderless-listкомпонент, который принимает nameи itemsподдерживает. Он также предоставляет слот с одной областью видимости в функции рендеринга.
Затем в родительском элементе мы используем его аналогично нашему многоцелевому компоненту списка, за исключением того, что на этот раз структура содержимого определена в родительском элементе, что дает нам больше гибкости, как мы увидим в следующем примере.

Примечание: в Vue 3 this.$scopedSlotsудален и this.$slotsиспользуется вместо него. Кроме того, this.$slotsпредоставляет слоты как функции. См. Раздел » Объединение слотов» для получения дополнительной информации.

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

<div id="app">
  <renderless-list name="My shopping list:" :items="products">
    <template v-slot:default="{name, items: products}">
      <h3>{{ name }}</h3>
      <table>
        <tr>
          <th>Quantity</th>
          <th>Product</th>
        </tr>
        <tr v-for="product in products" :key="product.name">
          <td>{{ product.quantity }}</td>
          <td>{{ product.name }}</td>
        </tr>
      </table>
    </template>
  </renderless-list>
</div>
table, th, td {
  border: 1px solid black;
  border-collapse: collapse;
  padding: 5px;
}

Здесь мы используем те же функции, но определяем другую структуру контента.

Заключение

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

Оцените статью
bestprogrammer.ru
Добавить комментарий