Skip to content

Модалки

1. Простой компонент модального окна

Описание:

Создайте компонент модального окна (Modal.vue) и используйте его в местах, где нужна модалка. Управление видимостью осуществляется с помощью локального состояния и директивы v-if.

Код:

Modal.vue

vue
<template>
  <div class="modal-overlay" @click.self="close">
    <div class="modal-content">
      <slot></slot>
      <button @click="close">Закрыть</button>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    close() {
      this.$emit('close');
    }
  }
}
</script>

<style scoped>
/* Стили модального окна */
.modal-overlay {
  /* стили для затемнения фона */
}
.modal-content {
  /* стили для контента модалки */
}
</style>

Родительский компонент:

vue
<template>
  <div>
    <button @click="isModalVisible = true">Открыть модалку</button>
    <Modal v-if="isModalVisible" @close="isModalVisible = false">
      <p>Содержимое модального окна</p>
    </Modal>
  </div>
</template>

<script>
import Modal from './Modal.vue';

export default {
  components: { Modal },
  data() {
    return {
      isModalVisible: false
    };
  }
}
</script>

Плюсы:

  • Простота реализации: легко понять и настроить.
  • Локальное управление: состояние модалки управляется внутри компонента.

Минусы:

  • Ограниченность использования: сложно использовать одну и ту же модалку в разных местах без дублирования кода.
  • Глубокая вложенность: при глубокой иерархии компонентов передача событий может стать сложной.

2. Использование Teleport (доступно в Vue 3)

Описание:

Teleport позволяет рендерить компонент вне текущей иерархии DOM. Это удобно для модалок, которые должны быть размещены непосредственно внутри <body>.

Код:

Modal.vue

vue
<template>
  <Teleport to="body">
    <div class="modal-overlay" v-if="visible" @click.self="close">
      <div class="modal-content">
        <slot></slot>
        <button @click="close">Закрыть</button>
      </div>
    </div>
  </Teleport>
</template>

<script>
export default {
  props: {
    visible: {
      type: Boolean,
      required: true
    }
  },
  methods: {
    close() {
      this.$emit('close');
    }
  }
}
</script>

Родительский компонент:

vue
<template>
  <div>
    <button @click="isModalVisible = true">Открыть модалку</button>
    <Modal :visible="isModalVisible" @close="isModalVisible = false">
      <p>Содержимое модального окна</p>
    </Modal>
  </div>
</template>

<script>
import Modal from './Modal.vue';

export default {
  components: { Modal },
  data() {
    return {
      isModalVisible: false
    };
  }
}
</script>

Плюсы:

  • Отсутствие проблем с вложенностью: модалка рендерится непосредственно внутри <body>.
  • Глобальный доступ к стилям: стили применяются корректно вне зависимости от местоположения компонента.

Минусы:

  • Требуется Vue 3: недоступно в более ранних версиях.
  • Потенциальные сложности с SSR: может потребоваться дополнительная настройка для серверного рендеринга.

3. Композиционный подход с provide/inject

typescript
// composables/useModal.ts
import { provide, inject, ref } from 'vue';

const ModalSymbol = Symbol();

export function provideModal() {
  const isOpen = ref(false);
  const modalComponent = ref(null);
  const modalProps = ref({});

  const openModal = (component: any, props = {}) => {
    modalComponent.value = component;
    modalProps.value = props;
    isOpen.value = true;
  };

  const closeModal = () => {
    isOpen.value = false;
    modalComponent.value = null;
    modalProps.value = {};
  };

  provide(ModalSymbol, {
    isOpen,
    modalComponent,
    modalProps,
    openModal,
    closeModal
  });

  return {
    openModal,
    closeModal
  };
}

export function useModal() {
  const modal = inject(ModalSymbol);
  if (!modal) throw new Error('Modal context not found!');
  return modal;
}
vue
<!-- ModalProvider.vue -->
<template>
  <div class="modal-provider">
    <slot></slot>

    <Teleport to="body">
      <Transition name="fade">
        <div v-if="isOpen" class="modal-overlay">
          <component
            :is="modalComponent"
            v-bind="modalProps"
            @close="closeModal"
          />
        </div>
      </Transition>
    </Teleport>
  </div>
</template>

<script setup lang="ts">
import { provideModal } from './composables/useModal';

const { isOpen, modalComponent, modalProps, closeModal } = provideModal();
</script>

Плюсы:

  • Чистая композиционная логика.
  • Гибкое управление состоянием.
  • Хорошая инкапсуляция.

Минусы:

  • Может быть сложным для понимания новичками
  • Требует правильной структуры приложения
  • Необходимость оборачивать приложение в провайдер

4. Использование глобального события (Event Bus)

Описание:

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

Подробная реализация

Реализация:

  1. Создайте Event Bus

    Создайте файл eventBus.js:

    javascript
    import { reactive } from 'vue';
    
    const eventBus = reactive({});
    
    export default eventBus;
  2. Реализуйте менеджер модальных окон

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

    ModalManager.vue

    vue
    <template>
      <component
        v-if="modalComponent"
        :is="modalComponent"
        v-bind="modalProps"
        @close="closeModal"
      ></component>
    </template>
    
    <script>
    import { reactive, onMounted } from 'vue';
    import eventBus from '../eventBus';
    
    export default {
      name: 'ModalManager',
      setup() {
        const state = reactive({
          modalComponent: null,
          modalProps: {},
        });
    
        const openModal = (component, props = {}) => {
          state.modalComponent = component;
          state.modalProps = props;
        };
    
        const closeModal = () => {
          state.modalComponent = null;
          state.modalProps = {};
        };
    
        onMounted(() => {
          eventBus.openModal = openModal;
          eventBus.closeModal = closeModal;
        });
    
        return {
          modalComponent: state.modalComponent,
          modalProps: state.modalProps,
          closeModal,
        };
      },
    };
    </script>
  3. Добавьте ModalManager в ваше приложение

    Включите ModalManager в App.vue или другой корневой компонент.

    App.vue

    vue
    <template>
      <div id="app">
        <!-- Ваше приложение -->
        <ModalManager />
      </div>
    </template>
    
    <script>
    import ModalManager from './components/ModalManager.vue';
    
    export default {
      components: {
        ModalManager,
      },
    };
    </script>
  4. Создайте модальные компоненты

    MyModal.vue

    vue
    <template>
      <div class="modal-overlay" @click.self="$emit('close')">
        <div class="modal-content">
          <h2>{{ title }}</h2>
          <p>{{ message }}</p>
          <button @click="$emit('close')">Закрыть</button>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      props: {
        title: String,
        message: String,
      },
    };
    </script>
  5. Открывайте модалки из любого компонента

    vue
    <template>
      <button @click="openMyModal">Открыть модалку</button>
    </template>
    
    <script>
    import eventBus from '../eventBus';
    import MyModal from './MyModal.vue';
    
    export default {
      methods: {
        openMyModal() {
          eventBus.openModal(MyModal, {
            title: 'Привет',
            message: 'Это сообщение в модальном окне',
          });
        },
      },
    };
    </script>

Плюсы:

  • Централизованное управление: Все модальные окна управляются из одного места.
  • Простота доступа: Модалки можно открывать и закрывать из любого компонента.
  • Без использования store: Нет необходимости в дополнительных зависимостях.

Минусы:

  • Потенциальный беспорядок в глобальном пространстве: Глобальное использование eventBus может привести к трудностям в отладке.
  • Отсутствие явных зависимостей: Труднее отслеживать, какие компоненты зависят от модального менеджера.

5. Использование Provide/Inject

Описание:

Используйте механизм provide и inject в Vue для передачи методов открытия и закрытия модалок вниз по дереву компонентов без необходимости пропс-дерриллинга.

Реализация:

  1. Предоставьте методы модального управления

    В корневом компоненте (например, App.vue):

    vue
    <template>
      <div id="app">
        <!-- Ваше приложение -->
        <ModalManager />
      </div>
    </template>
    
    <script>
    import { provide, reactive } from 'vue';
    import ModalManager from './components/ModalManager.vue';
    
    export default {
      components: { ModalManager },
      setup() {
        const state = reactive({
          modalComponent: null,
          modalProps: {},
        });
    
        const openModal = (component, props = {}) => {
          state.modalComponent = component;
          state.modalProps = props;
        };
    
        const closeModal = () => {
          state.modalComponent = null;
          state.modalProps = {};
        };
    
        provide('modalState', state);
        provide('openModal', openModal);
        provide('closeModal', closeModal);
      },
    };
    </script>
  2. Модальный менеджер использует Inject

    ModalManager.vue

    vue
    <template>
      <component
        v-if="modalState.modalComponent"
        :is="modalState.modalComponent"
        v-bind="modalState.modalProps"
        @close="closeModal"
      ></component>
    </template>
    
    <script>
    import { inject } from 'vue';
    
    export default {
      name: 'ModalManager',
      setup() {
        const modalState = inject('modalState');
        const closeModal = inject('closeModal');
    
        return {
          modalState,
          closeModal,
        };
      },
    };
    </script>
  3. Компоненты используют Inject для открытия модалок

    vue
    <template>
      <button @click="openMyModal">Открыть модалку</button>
    </template>
    
    <script>
    import { inject } from 'vue';
    import MyModal from './MyModal.vue';
    
    export default {
      setup() {
        const openModal = inject('openModal');
    
        const openMyModal = () => {
          openModal(MyModal, {
            title: 'Привет',
            message: 'Это сообщение в модальном окне',
          });
        };
    
        return {
          openMyModal,
        };
      },
    };
    </script>

Плюсы:

  • Избегает глобального пространства: Нет необходимости в глобальных переменных.
  • Явные зависимости: Компоненты явно указывают на свои зависимости через inject.

Минусы:

  • Ограничения по глубине вложенности: Может быть сложно при очень глубокой или динамической иерархии компонентов.
  • Может быть менее понятным для новичков: Требует понимания механизма provide/inject.

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

Описание:

Создайте реактивный объект состояния модальных окон и экспортируйте его. Компоненты могут импортировать функции для управления модалками и сам объект состояния для отображения.

Реализация:

  1. Создайте глобальное состояние модалок

    modalState.js

    javascript
    import { reactive } from 'vue';
    
    export const modalState = reactive({
      component: null,
      props: {},
    });
    
    export const openModal = (component, props = {}) => {
      modalState.component = component;
      modalState.props = props;
    };
    
    export const closeModal = () => {
      modalState.component = null;
      modalState.props = {};
    };
  2. Модальный менеджер использует состояние

    ModalManager.vue

    vue
    <template>
      <component
        v-if="modalState.component"
        :is="modalState.component"
        v-bind="modalState.props"
        @close="closeModal"
      ></component>
    </template>
    
    <script>
    import { modalState, closeModal } from '../modalState';
    
    export default {
      setup() {
        return {
          modalState,
          closeModal,
        };
      },
    };
    </script>
  3. Компоненты управляют модалками через импортированные функции

    vue
    <template>
      <button @click="openMyModal">Открыть модалку</button>
    </template>
    
    <script>
    import { openModal } from '../modalState';
    import MyModal from './MyModal.vue';
    
    export default {
      methods: {
        openMyModal() {
          openModal(MyModal, {
            title: 'Привет',
            message: 'Это сообщение в модальном окне',
          });
        },
      },
    };
    </script>

Плюсы:

  • Простота: Нет необходимости в сложных настройках или плагинах.
  • Явные зависимости через импорт: Легко увидеть, откуда приходят функции.

Минусы:

  • Глобальное состояние: Может привести к трудностям в поддержке и отладке.
  • Риск дублирования состояния: Если неаккуратно импортировать, можно создать несколько экземпляров состояния.

Общий вывод

  • Простой компонент подходит для небольших проектов с ограниченным использованием модалок.
  • Teleport идеален, когда требуется избежать проблем с вложенностью и обеспечить корректное отображение модалки на уровне <body>.

Выбор метода зависит от:

  • Размеров и сложности вашего проекта
  • Требований к функциональности модального окна
  • Необходимости в кастомизации и контроле
  • Предпочтений по управлению состоянием приложения