Skip to content

Реализация модалки через шину событий

1. composable useModal.ts

typescript
// src/composables/useModal.ts

import { reactive, readonly, InjectionKey, inject, provide } from 'vue';

interface ModalState {
  isOpen: boolean;
  component: any;
  props: Record<string, any>;
}

interface ModalMethods {
  openModal: (component: any, props?: Record<string, any>) => void;
  closeModal: () => void;
}

// Создаем уникальный ключ для provide/inject
const modalKey: InjectionKey<ReturnType<typeof createModal>> = Symbol('ModalKey');

// Функция для создания состояния и методов модальных окон
function createModal() {
  const modalState = reactive<ModalState>({
    isOpen: false,
    component: null,
    props: {},
  });

  const openModal = (component: any, props: Record<string, any> = {}) => {
    modalState.isOpen = true;
    modalState.component = component;
    modalState.props = props;
  };

  const closeModal = () => {
    modalState.isOpen = false;
    modalState.component = null;
    modalState.props = {};
  };

  return {
    modalState: readonly(modalState),
    openModal,
    closeModal,
  };
}

// Функция для предоставления модального состояния и методов
export function provideModal() {
  const modal = createModal();
  provide(modalKey, modal);
}

// Функция для использования модального состояния и методов в компонентах
export function useModal() {
  const modal = inject(modalKey);
  if (!modal) {
    throw new Error('useModal() called without provider.');
  }
  return modal;
}

Пояснение:

  • Мы создали функцию createModal(), которая инициализирует состояние и методы модальных окон.
  • В provideModal() мы создаем экземпляр модального состояния и предоставляем его через provide.
  • useModal() используется для получения доступа к модальному состоянию и методам внутри компонентов.

2. App.vue

vue
<!-- src/App.vue -->

<template>
  <div id="app">
    <!-- Ваше приложение -->
    <ModalManager />
  </div>
</template>

<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import { provideModal } from './composables/useModal';
import ModalManager from './components/ModalManager.vue';

export default defineComponent({
  name: 'App',
  components: {
    ModalManager,
  },
  setup() {
    provideModal(); // Предоставляем модальное состояние и методы
  },
});
</script>

Пояснение:

  • Используем setup-функцию для вызова provideModal().

3. ModalManager.vue

vue
<!-- src/components/ModalManager.vue -->

<template>
  <component
    v-if="modalState.isOpen"
    :is="modalState.component"
    v-bind="modalState.props"
    @close="closeModal"
  />
</template>

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

export default defineComponent({
  name: 'ModalManager',
  setup() {
    const { modalState, closeModal } = useModal();

    return {
      modalState,
      closeModal,
    };
  },
});
</script>

Пояснение:

  • Используем setup для получения модального состояния и метода closeModal через useModal().

4. Модальный компонент MyModal.vue

vue
<!-- src/components/MyModal.vue -->

<template>
  <div class="modal-overlay" @click.self="close">
    <div class="modal-content">
      <h2>{{ title }}</h2>
      <p>{{ message }}</p>
      <button @click="close">Закрыть</button>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';

export default defineComponent({
  name: 'MyModal',
  props: {
    title: {
      type: String as PropType<string>,
      required: true,
    },
    message: {
      type: String as PropType<string>,
      required: true,
    },
  },
  emits: ['close'],
  setup(props, { emit }) {
    const close = () => {
      emit('close');
    };

    return {
      ...props,
      close,
    };
  },
});
</script>

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

.modal-content {
  /* Ваши стили для содержимого модалки */
}
</style>

Пояснение:

  • Используем setup для определения методов и доступа к пропсам.

5. Компонент, открывающий модалку

Теперь обновим компонент, который открывает модальное окно, чтобы можно было просто вызвать openMyModal() без необходимости импортировать useModal в каждом компоненте.

Чтобы этого достичь, мы можем экспортировать функцию openMyModal() из composable и сделать ее доступной глобально.

Обновление useModal.ts

Добавим экспорт функции openMyModal:

typescript
// Добавьте в конец файла useModal.ts

// Предоставляем глобальную функцию для открытия конкретной модалки
export function openMyModal() {
  const modal = inject(modalKey);
  if (!modal) {
    throw new Error('openMyModal() called without provider.');
  }
  modal.openModal(MyModalComponent, {
    title: 'Привет',
    message: 'Это сообщение в модальном окне',
  });
}

Однако, поскольку мы не можем использовать inject вне setup, нам нужно немного изменить подход.

Использование глобального экземпляра для доступа к модальному состоянию

Мы можем изменить useModal.ts следующим образом:

typescript
// src/composables/useModal.ts

import { reactive, readonly } from 'vue';

interface ModalState {
  isOpen: boolean;
  component: any;
  props: Record<string, any>;
}

const modalState = reactive<ModalState>({
  isOpen: false,
  component: null,
  props: {},
});

const openModal = (component: any, props: Record<string, any> = {}) => {
  modalState.isOpen = true;
  modalState.component = component;
  modalState.props = props;
};

const closeModal = () => {
  modalState.isOpen = false;
  modalState.component = null;
  modalState.props = {};
};

export function useModal() {
  return {
    modalState: readonly(modalState),
    openModal,
    closeModal,
  };
}

Теперь modalState, openModal, и closeModal доступны напрямую из useModal(), без необходимости provide/inject.

Обновим App.vue:

vue
<!-- src/App.vue -->

<template>
  <div id="app">
    <!-- Ваше приложение -->
    <ModalManager />
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import ModalManager from './components/ModalManager.vue';

export default defineComponent({
  name: 'App',
  components: {
    ModalManager,
  },
  setup() {
    // Больше не нужно provideModal()
  },
});
</script>

Обновим ModalManager.vue:

vue
<!-- src/components/ModalManager.vue -->

<template>
  <component
    v-if="modalState.isOpen"
    :is="modalState.component"
    v-bind="modalState.props"
    @close="closeModal"
  />
</template>

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

export default defineComponent({
  name: 'ModalManager',
  setup() {
    const { modalState, closeModal } = useModal();

    return {
      modalState,
      closeModal,
    };
  },
});
</script>

Теперь мы можем создать функцию openMyModal и экспортировать ее из useModal.ts:

typescript
// src/composables/useModal.ts

import MyModal from '../components/MyModal.vue';

// ... предыдущий код

export function openMyModal() {
  openModal(MyModal, {
    title: 'Привет',
    message: 'Это сообщение в модальном окне',
  });
}

Теперь в любом компоненте мы можем импортировать openMyModal и использовать ее без необходимости вызова useModal().

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

vue
<!-- src/components/SomeComponent.vue -->

<template>
  <button @click="openMyModal">Открыть модалку</button>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { openMyModal } from '../composables/useModal';

export default defineComponent({
  name: 'SomeComponent',
  setup() {
    return {
      openMyModal,
    };
  },
});
</script>

Пояснение:

  • Мы экспортируем функцию openMyModal из useModal.ts.
  • В компонентах, где мы хотим открыть модальное окно, мы просто импортируем openMyModal и вызываем ее в setup.

Преимущества данного подхода

  • Простота использования: В компонентах не нужно каждый раз использовать useModal; достаточно импортировать openMyModal.
  • Чистая архитектура: Использование Composition API (setup) во всех компонентах и файлах обеспечивает современный и рекомендуемый подход в Vue 3.
  • Инкапсуляция: Управление модальными окнами скрыто внутри useModal, обеспечивая чистый код и легкую поддержку.
  • Поддержка TypeScript: Все компоненты и функции типизированы, что повышает надежность и удобство разработки.