Vue.js scoped slots и renderless component

Продолжая рассматривать способы повторного использования кода в vue.js можно встретить упоминания некоего приема с использованием не отображаемых компонентов и слотов с ограниченной областью видимости (scoped slots).

Для начала вспомним что такое слоты в vue.js и как ими пользоваться. Они ведь и сами по себе позволяют использовать один и тот же компонент, но с различным наполнением и даже функционалом.

Пример из документации. Если у нас есть компонент с таким шаблоном:

<template>
     <a v-bind:href="url" class="nav-link" >
        <slot></slot>
     </a>
</template>

То можно использовать его так (заключив между открывающим и закрывающим тегом дополнительное содержимое):

<navigation-link url="/profile">
    <!-- Добавляем иконку из набора Font Awesome -->
    <span class="fa fa-user"></span>
    Ваш профиль
</navigation-link>

И компонент navigation-link подставит указанный дополнительный контент вместо блока <slot></slot> при рендеринге.

Дополнительный контент может содержать не только обычные html теги, но и другие компоненты. Также слотов может быть более чем один. Слоту без имени фактически будет присвоено имя default. А другим слотам в компоненте нужно дать другие имена через атрибут name в каждом слоте.

Когда мы используем какие-либо параметры (переменные) в содержимом для слота, то используется область видимости родительского компонента (а не компонента, в котором объявлен блок <slot></slot>. Используется, в общем-то то же правило, что и веде во vue:

Всё в родительском шаблоне компилируется в области видимости родительского компонента; всё в дочернем шаблоне компилируется в области видимости дочернего компонента.

Но есть возможность из компонента, содержащего слот передавать данные контенту. Это слоты с ограниченной областью видимости (scoped slots).

Немного модифицированный компонент из документации. Допустим есть компонент

<template>
  <span>
    <slot v-bind:person="user">
      {{ user.lastName }}
    </slot>
  </span>
</template>
<script>
export default {
  data() {
    return {
      user: {
        firstName: 'John',
        lastName: 'Hall'
      },
    }
  },
  methods: {

  }
}
</script>

И тогда можно использовать в родительском шаблоне это так:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.person.firstName }} {{ slotProps.person.lastName }}
  </template>
</current-user>

Таким образом через переменную slotProps (имя можно использовать любое) можно обратиться к данным дочернего компонента из родительского.

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

<current-user>
  <template v-slot:default="{ person }">
    {{ person.firstName }} {{ person.lastName }}
  </template>
</current-user>

И еще можно избавиться от тега template раз уж мы используем слот по-умолчанию:

<current-user v-slot="{ person }">
  {{ person.firstName }} {{ person.lastName }}
</current-user>

Как уже было упомянуто выше, слоты, уже сами по себе, являются неплохим инструментом повторного использования кода. Однако посмотрим, что же мы можем получить при помощи renderless component (компонента без отображения).

На самом деле название не совсем верное. Скорее речь о компоненте, у которого нет полноценного шаблона, а есть только слот и функционал:

export default {
  data: () => ({
    count: 0
  }),
  methods: {
    increment() {
      this.count++;
    }
  },
  render(createElement) {
    return createElement('div',
      this.$scopedSlots.default({
        count: this.count,
        increment: this.increment,
      })
    );
  }
}

Для объявления слота и передачи параметров используется this.$scopedSlots.

И теперь этот функционал и параметр можно использовать в другом компоненте:

<template>
  <div class="component-border pl-1 mb-1 ms-1">
    <h1>
      Scoped Slot Example
    </h1>
    <counter-renderless v-slot="{ count, increment }">
      <p>Count: {{ count }}</p>
      <p>
        <button @click="increment">Increment</button>
      </p>
    </counter-renderless>
  </div>
</template>
<script>
import CounterRenderless from "./CounterRenderless";
export default {
  components: {
    CounterRenderless
  }
}
</script>

И получаем результат:

Это работает, но похоже, что взять и реализовать в отдельном renderless component таким же образом decrement (как в одной из предыдущих статей) уже не получится без ухищрений. Ведь компоненты decrement и increment будут работать каждый со своим свойством count.

Такой подход сможет найти применение в весьма узком числе случаев. И фаворитом в вопросе повторного использования кода для меня остается Composition API.

Буду раз вашим замечания, ссылкам и примерам по этой теме.


Код примеров — в моем репозитории

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

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *