Тема
Единый регистр модальных окон
Потребность: вызов модальных окон по идентификатору из любого места приложения через единый 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)из любого места.
Что нужно предусмотреть
- Регистрация: при инициализации (или лениво) регистрируются пары (id, компонент); при необходимости — default props, размер, доступность с клавиатуры (Esc), блокировка фона.
- Открытие/закрытие:
open(id, props?)кладёт в стейт id и props, контейнер монтирует компонент и передаёт props + callbackonClose/resolve;close()или закрытие пользователем очищает стейт. - Результат: для модалов с выбором (confirm/cancel, выбор варианта) удобно возвращать Promise:
const result = await modals.open('confirm-delete', { title, message })— Promise резолвится при нажатии «Да»/«Нет» с переданным значением. Реализация через хранение resolve-функции в стейте и вызов при закрытии. - Один или несколько модалов: обычно достаточно одного активного модала (стек не обязателен); при открытии второго можно закрыть первый или показывать стек — решается политикой в реализации.
- Интеграция с Vue: стейт реактивный (ref/reactive), контейнер — компонент в корне приложения (например, в App.vue или в EndgeShell), который подписан на стейт и рендерит текущий модал через
<component :is="currentModalComponent" v-bind="currentProps" @close="handleClose" />. - Типизация: для 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 для простых подтверждений, а регистр модалов — для кастомных модалов с формой/контентом.
Практические шаги
- Ввести слой «регистр модалов»: хранилище (id - component), стейт (currentId, currentProps, resolveRef для Promise), методы register(id, component), open(id, props?), close(), и при необходимости API
open(...).then(...). - Реализовать контейнер (один компонент в корне), который по currentId рендерит соответствующий модал и передаёт props + onClose.
- Зарегистрировать существующие модалы (create-document, create-version и т.д.) при старте приложения или в соответствующих фичах; заменить текущее управление через v-if в родителях на вызовы
modals.open(...). - При необходимости типизировать id и props по модалам для типобезопасности вызовов.
- Решить вопрос «один модал или стек» и при стеке — порядок отображения и закрытия (например, закрытие только верхнего).
Риски и ограничения
- Стек из нескольких модалов усложняет управление фокусом и доступность (a11y); при введении стека нужно соблюдать паттерн «фокус в верхнем модале», см. Accessibility_A11y. Связь с Notifications: тосты — для кратких сообщений без ответа; модалы — для форм и выбора с возвратом результата.
Вывод
- Единый регистр модальных окон по аналогии с тостами упрощает вызов из любого места и снижает связность компонентов с деревом.
- Реализация: реактивный стейт + контейнер в корне + API open/close (и при необходимости Promise для результата); регистрация id - компонент в приложении или фичах.
- Размещение в @endge/ui-vue или в пакете @endge/modals с Vue-адаптером; простые confirm-диалоги могут быть либо поверх этого регистра, либо отдельным тонким API.