Vue. Динамическое переключение компонентов

Чтобы не повторять себя (принцип DRY) при разработке Vue-приложения приходится прибегать к различным приемам. Одним из таких приемов (не так сильно известным, кстати) является применение атрибута is для динамического переключения между компонентами. В этой статье будет показано и рассказано как можно использовать такой подход.

Содержание

Абстрактный пример

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

const settings = {
  component_1: { component: FirstComponent, data: {} },
  component_2: {
    component: SecondComponent,
    data: { param1: 'Some value', param2: 5.99 } },
  component2: {
    component: OneAnotherComponent,
    data: { param3: 'Another value', param4: 'Something else' }
  }
}

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

Во Vue.js для этих целей есть следующая конструкция:

<component :is="currentComponent"></component>

Добавим немного конкретики для наглядности.

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

Вот основной файл приложения.

<script setup>
import { ref } from 'vue'
import TextComponent from '@/components/TextComponent.vue'
import ImgComponent from '@/components/ImgComponent.vue'
import FormComponent from '@/components/FormComponent.vue'

const settings = {
  text: { component: TextComponent, data: {} },
  img: { component: ImgComponent, data: { width: '380px', title: 'Sample picture' } },
  form: {
    component: FormComponent,
    data: { title: 'Some name', description: 'Some very long text' }
  }
}

const selected = ref('text')
</script>

<template>
  <header>
    <h1>Динамические компоненты</h1>
  </header>

  <main>
    <p>Select a component</p>

    <select v-model="selected">
      <option disabled value="">Please select one</option>
      <option>text</option>
      <option>img</option>
      <option>form</option>
    </select>

    <h2>Result</h2>

    <!-- Здесь будет отображаться выбранный компонент -->
    <component :is="settings[selected].component" v-bind="settings[selected].data"></component>
  </main>
</template>

Далее код отдельных компонентов:

<template>
  <p class="text">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ultrices enim egestas condimentum
    dictum. Cras eu tristique elit, eget aliquet quam. Vivamus efficitur libero in turpis interdum,
    id placerat felis lacinia.
  </p>
</template>
<script setup>
defineProps({
  width: String,
  title: String
})
</script>

<template>
  <img src="@/assets/logo.svg" :title="title" :alt="title" :width="width" />
</template>
<script setup>
import { reactive } from 'vue'

const props = defineProps({
  title: String,
  description: String
})

const data = reactive({ ...props })

function handleSubmit() {
  alert('Form data: ' + JSON.stringify(data))
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <div>
      <label for="title"> Title: </label>
      <input type="text" id="title" v-model="data.title" />
    </div>
    <div>
      <label for="description"> Description: </label>
      <textarea id="description" v-model="data.description"></textarea>
    </div>
    <button type="submit">Submit</button>
  </form>
</template>

Как можно видеть, здесь в файле App.vue есть настройки в константе settings и реактивная переменная selected. Значение последней меняется через выпадающий список. А блок componentс атрибутом is как раз выполняет переключение. В сам атрибут is мы передаем выбранный компонент, который находим по ключу из выпадающего списка settings[selected].component. Также передаем туда свойства через конструкцию v-bind="settings[selected].data".

Выглядит в работе это так:

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

Код примера есть в репозитории.

Практический пример

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

Действительно, модальное окно само по себе может быть представлено компонентом, а также оно может содержать различные формы. Формы редактирования и добавления записи, подтверждения действия с записью. Причем записи могут относиться к различным сущностям. Вы можете редактировать учетную запись клиента или данные о товаре. В зависимости от этого в модальном окне будет отображаться своя форма со своими параметрами.

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

Модальное окно выглядит как полноценное диалоговое окно на широком экране:

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

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

Посмотрим как это выглядит в коде. Будут использоваться элементы библиотеки Vuetify.

<template>
  <v-app>
    <header>
      <h1>Пример для v-dlg и v-navigation-drawer</h1>
    </header>

    <main>
      <p>Используйте кнопки ниже для вызова форм</p>
      <br />
      <v-btn @click="showModal">Редактирование</v-btn>
      <v-btn @click="handleDelete">Удаление</v-btn>
    </main>

    <!-- В слот модального окна встраиваем нужный компонент динамически -->
    <app-modal v-model="modal.visible">
      <component
        :is="modal.form"
        v-bind="modal.props"
        :title="modal.title"
        @done="modal.close()"
      ></component>
    </app-modal>
  </v-app>
</template>

<script setup>
import AppModal from '@/components/AppModal.vue'
import { useModalStore } from '@/stores/modal.js'
import EditForm from '@/components/forms/EditForm.vue'
import DeleteForm from '@/components/forms/DeleteForm.vue'

const modal = useModalStore()

function showModal() {
  modal.open('Редактировать запись', EditForm, {
    id: 'JH219zA',
    name: 'Большой каньон',
    description: 'Гранд-Каньон в Аризоне – это уникальное природное образование.'
  })
}

function handleDelete() {
  modal.open('Удалить запись', DeleteForm, { id: 'oAuu378', name: 'Крупная рыба' })
}
</script>

В основном файле примера (App.vue) можно увидеть как используется компонент app-modal. Он вызывается в шаблоне один раз во всем примере. И в более крупном приложении при таком подходе он будет оставаться также в единственном экземпляре. В слот к нему передается известная нам конструкция, данные для которой берутся из хранилища Pinia. С помощью хранилища мы управляем показом модального кона с нужной формой и данными. О Pinia ранее рассказывалось в статье про передачу данных на много уровней в дереве компонентов.

Вот как выглядит хранилище:

import { ref, shallowRef } from 'vue'
import { defineStore } from 'pinia'

export const useModalStore = defineStore('modal', () => {
  const visible = ref(false)
  const title = ref('')
  const form = shallowRef()
  const props = ref()

  function open(modalTitle, content, data) {
    visible.value = true
    title.value = modalTitle
    form.value = content
    props.value = data
  }

  function close() {
    visible.value = false
  }

  return { visible, form, title, props, open, close }
})

Так выглядит компонент app-modal:

<template>
  <!-- Вызываем мобильную или обычную версию -->
  <component :is="component">
    <slot></slot>
  </component>
</template>

<script setup>
import DlgModal from '@/components/modals/DlgModal.vue'
import DrawerModal from '@/components/modals/DrawerModal.vue'
import { computed } from 'vue'
import { useDisplay } from 'vuetify'

const { mobile } = useDisplay()
const component = computed(() => (mobile.value ? DrawerModal : DlgModal))

defineEmits(['close'])
</script>

В нем через знакомую нам конструкцию одключается нужная версия модального окна.

Обычная (для больших экранов):

<template>
  <v-dialog max-width="900">
    <slot></slot>
  </v-dialog>
</template>

И для мобильных экранов (в виде нижней шторки):

<template>
  <v-navigation-drawer temporary location="bottom" mobile>
    <slot></slot>
  </v-navigation-drawer>
</template>

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

<template>
  <v-card :title="title">
    <v-card-text class="form-layout__content">
      <slot></slot>
    </v-card-text>
    <v-card-actions>
      <slot name="actions"></slot>
    </v-card-actions>
  </v-card>
</template>

<script setup>
defineProps({
  title: { type: String }
})
</script>

<style>
.form-layout__content {
  overflow-y: auto;
  padding-top: 12px !important;
}
</style>

И наконец, код самих форм.

<template>
  <v-form @submit.prevent="handleSubmit">
    <form-layout :title="props.title">
      <template #default>
        <v-text-field v-model="data.name" variant="outlined" label="Имя"></v-text-field>

        <v-textarea v-model="data.description" label="Описание" variant="outlined"></v-textarea>
      </template>
      <template #actions>
        <v-btn
          :loading="loading"
          color="primary"
          class="mt-2"
          text="Сохранить изменения"
          type="submit"
          block
        ></v-btn>
      </template>
    </form-layout>
  </v-form>
</template>

<script setup>
import { reactive, ref, toRefs } from 'vue'
import FormLayout from '@/components/forms/FormLayout.vue'

const props = defineProps({
  id: { type: String },
  name: { type: String },
  description: { type: String },
  title: { type: String }
})

const emit = defineEmits(['done'])

const loading = ref(false)
const data = reactive(toRefs(props))

function handleSubmit() {
  loading.value = true
  console.log(`Updating ${props.id}...`)
  loading.value = false
  emit('done')
}
</script>
<template>
  <form-layout :title="props.title">
    <template #default>
      <v-alert type="warning"
        >Вы действительно хотите удалить запись &ldquo;{{ props.name }}&rdquo;?</v-alert
      >
    </template>
    <template #actions>
      <v-btn :loading="loading" color="warning" block @click="handleClick" class="mt-2"
        >Удалить</v-btn
      >
    </template>
  </form-layout>
</template>

<script setup>
import { ref } from 'vue'
import FormLayout from '@/components/forms/FormLayout.vue'

const props = defineProps({
  id: String,
  name: { type: String },
  title: { type: String }
})

const emit = defineEmits(['done'])

const loading = ref(false)

function handleClick() {
  loading.value = true
  console.log(`Deleting ${props.id}...`)
  loading.value = false
  emit('done')
}
</script>

Полный код примера – в репозитории.

Конечно в более сложном приложении может понадобится вызвать еще одно модальное окно для уже вызванного. Например, для подтверждения какой-либо операции. Это можно реализовать различными способами. Например, можно в хранилище завести место под целый стек форм. Так получится вызывать новые окна и возвращаться после закрытия к предыдущим.

Итоги

Как можно видеть, конструкция <component :is ...> может сильно нам помочь не повторять себя. Часто разработчики начинают дублировать вызов компонента v-dlg для каждой формы. В проекте появляется множество дублей с одинаковыми параметрами и поведением. При необходимости поменять что-либо придется сделать много однообразной работы.

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

Код примеров – в репозитории с примерами.

Ссылки

5 1 голос
Рейтинг статьи
Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии