Skip to content

Единый регистр модальных окон

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

Обоснование

В крупных приложениях и Low-Code платформах модалы (диалоги подтверждения, формы создания сущностей) регистрируются централизованно и открываются по id с параметрами; состояние хранится в одном слое, контейнер рендерится в корне. Это снижает связность и упрощает вызов из вложенных компонентов и из сценариев. Риск при отсутствии: рост числа модалов ведёт к раздуванию родительских компонентов и к цепочкам emit/callback для открытия диалога из глубины дерева.

Текущее состояние

  • Реализация: модальные окна — это Vue-компоненты (например, CreateDocumentModal.vue, CreateVersionModal.vue, ConfirmActionDialog.vue), которые вставляются в дерево компонентов — в layout либо в родительской странице или виджете.
  • Вызов: родитель рендерит модал условно (v-if/v-show) и управляет состоянием (open, payload) через свои данные; для открытия нужно знать, где в дереве этот модал находится, и менять состояние именно там. Часто используется компонент типа Drawer/Dialog (vaul-vue, reka-ui и т.д.) с пропсами open и @update:open.
  • Минусы: при росте числа модалов увеличивается связность (родитель должен импортировать модал и держать состояние); открытие модала из глубоко вложенного компонента требует emit/callback или глобального состояния; нет единого места «какие модалы есть в приложении» и «открой модал X с параметрами Y».

Цель

  • Регистр модальных окон: по идентификатору (например, create-document, create-version) приложение регистрирует компонент модала и опционально метаданные (заголовок по умолчанию, размер, тип).
  • Вызов из любого места: один API, например modals.open('create-document', { projectId: '...' }) или modals.open(ModalId.CreateVersion, { ... }). Состояние «какой модал открыт и с какими параметрами» хранится централизованно (например, в слое/сторе модалов); один контейнер в корне приложения (или в layout) рендерит текущий модал по данным из стора.
  • Аналогия: как регистр тостов — один Toaster в корне и вызов toast.success(...) из любого места; так же регистр модалов — один «слот» для текущего модала и вызов modals.open(id, props) из любого места.

Что нужно предусмотреть

  1. Регистрация: при инициализации (или лениво) регистрируются пары (id, компонент); при необходимости — default props, размер, доступность с клавиатуры (Esc), блокировка фона.
  2. Открытие/закрытие: open(id, props?) кладёт в стейт id и props, контейнер монтирует компонент и передаёт props + callback onClose/resolve; close() или закрытие пользователем очищает стейт.
  3. Результат: для модалов с выбором (confirm/cancel, выбор варианта) удобно возвращать Promise: const result = await modals.open('confirm-delete', { title, message }) — Promise резолвится при нажатии «Да»/«Нет» с переданным значением. Реализация через хранение resolve-функции в стейте и вызов при закрытии.
  4. Один или несколько модалов: обычно достаточно одного активного модала (стек не обязателен); при открытии второго можно закрыть первый или показывать стек — решается политикой в реализации.
  5. Интеграция с Vue: стейт реактивный (ref/reactive), контейнер — компонент в корне приложения (например, в App.vue или в EndgeShell), который подписан на стейт и рендерит текущий модал через <component :is="currentModalComponent" v-bind="currentProps" @close="handleClose" />.
  6. Типизация: для TypeScript можно задать мапу id - props (например, ModalRegistry['create-document']), чтобы modals.open('create-document', { ... }) проверял тип второго аргумента.

Где размещать

  • В @endge/ui-vue: если модалы — часть UI-инфраструктуры платформы и всегда в Vue-приложении. Composable useModals() + плагин, регистрирующий контейнер и провайдер стейта.
  • Отдельный пакет @endge/modals: если хочется выделить контракт (регистр, open/close, Promise API) и реализацию для Vue в адаптере. Тогда в приложении используется адаптер из @endge/ui-vue или из приложения.
  • Регистрация конкретных модалов (id - компонент) остаётся в приложении или в фичах; модуль даёт только механику регистра и вызова.

Связь с уведомлениями и вопросами

  • Уведомления (toast): краткие сообщения без обязательного ответа пользователя; не требуют регистра компонентов, только сообщение и опции.
  • Модалы: требуют компонент и опционально возвращают результат (Promise); регистр нужен, чтобы не вставлять модалы в дерево вручную.
  • Questions / confirm dialogs: в приложении уже есть, например, Promise-based диалоги (Questions.vue, AlertDialog). Их можно реализовать поверх того же регистра модалов: зарегистрировать модал «confirm» с пропсами title/message/buttons и вызывать modals.open('confirm', { ... }) с возвратом Promise. Либо оставить отдельный API для простых подтверждений, а регистр модалов — для кастомных модалов с формой/контентом.

Практические шаги

  1. Ввести слой «регистр модалов»: хранилище (id - component), стейт (currentId, currentProps, resolveRef для Promise), методы register(id, component), open(id, props?), close(), и при необходимости API open(...).then(...).
  2. Реализовать контейнер (один компонент в корне), который по currentId рендерит соответствующий модал и передаёт props + onClose.
  3. Зарегистрировать существующие модалы (create-document, create-version и т.д.) при старте приложения или в соответствующих фичах; заменить текущее управление через v-if в родителях на вызовы modals.open(...).
  4. При необходимости типизировать id и props по модалам для типобезопасности вызовов.
  5. Решить вопрос «один модал или стек» и при стеке — порядок отображения и закрытия (например, закрытие только верхнего).

Риски и ограничения

  • Стек из нескольких модалов усложняет управление фокусом и доступность (a11y); при введении стека нужно соблюдать паттерн «фокус в верхнем модале», см. Accessibility_A11y. Связь с Notifications: тосты — для кратких сообщений без ответа; модалы — для форм и выбора с возвратом результата.

Вывод

  • Единый регистр модальных окон по аналогии с тостами упрощает вызов из любого места и снижает связность компонентов с деревом.
  • Реализация: реактивный стейт + контейнер в корне + API open/close (и при необходимости Promise для результата); регистрация id - компонент в приложении или фичах.
  • Размещение в @endge/ui-vue или в пакете @endge/modals с Vue-адаптером; простые confirm-диалоги могут быть либо поверх этого регистра, либо отдельным тонким API.