Модальные окна в веб-приложениях на Laravel, Inertia.js, Vue и Tailwind CSS
В чём заключается проблема?
Когда вы разрабатываете веб-приложения на Laravel и Inertia.js, вы не пишете отдельный API на Laravel, а разрабатываете SPA, как разрабатывали бы классический MPA на Laravel, но из контроллеров возвращаете не стандартные ответы через вспомогательные функции и фасады Laravel, а декорированные ответы — через фасад Inertia. В этих ответах вы передаёте данные, которые формируют глобальное состояние (state) для страницы, которую рендерит Inertia, и эти данные становятся при объявлении доступными всем вложенным компонентам страницы как свойства (props).
Проблема в том, что это глобальное состояние обновляется и сбрасывается при переходах на другие маршруты внутри приложения, поэтому реализовать единую глобальную шину событий или состояния становится не так просто и прямолинейно, как это было бы в случае реализации SPA на Vue/Nuxt в паре с Pinia. Inertia со своей стороны, манипулируя HTTP-заголовками, предлагает принципиально другую парадигму и выступает в роли промежуточного слоя для бесшовной трансформации запросов и ответов между фронтендом и бэкендом.
Как же решить проблему модальных окон в Inertia.js?
Вы можете попробовать наплодить десятки компонентов на фронтенде, — под каждое модальное окно, которое будет в вашем приложении, — и понаписать “костылей” для обхода ограничений глобального состояния Inertia. Но возвращаться к такому коду потом откровенно не захочется, и кроме вас его вряд ли кто-то разберёт (да и вы — с трудом уже через пару дней), поэтому для командной работы такой вариант тоже не подойдёт. Почему я с такой уверенностью это заявляю? Потому что я уже обжигался и за время работы с Inertia с версии 0.7.0 испробовал разные подходы.
Решение проблемы намного проще: нужно следовать парадигме Inertia и оставлять архитектурные решения за бэкендом. Ваш фронтенд останется “тупым” и “красивым”, работая с тем, что вы сформируете для него на бэке и вернёте с ответом — в тех ограничениях, которые вы установите.
Что это значит в контексте модальных окон? Что на фронтенде у них будет только базовая “обёртка”, которая будет динамически наполняться данными с бэкенда, а вызываться модальные окна будут через XHR-запросы (через Axios) к точкам доступа маршрутизатора Laravel. То есть: делаем GET-запрос на URL внутри приложения, получаем данные модального окна в JSON, рендерим его динамически на фронтенде. Как итог — чистый рабочий код, разделение ответственности, и мы не пытаемся делать из Inertia то, чем она не является.
Модальные окна в Inertia.js на фронтенде
При переходах между страницами внутри приложения на Inertia.js, компонент основного шаблона (корневой layout) не рендерится полностью заново, как при полной загрузке страницы, а обновляется (updated в жизненном цикле Vue). Поэтому, чтобы решить проблему глобального управления модальным окном из разных частей приложения, нам нужно выполнить две задачи:
- Добавить в глобальный layout компонент-обёртку модального окна, внутри которого все модальные окна будут рендериться динамически. Этот компонент будет содержать публичный API для взаимодействия с модальным окном и Vue transition для анимации.
- Добавить собственный composable и импортировать его в главный обёрточный компонент модальных окон. Этот composable будет содержать своё “реактивное” состояние и публичный API для управления им, что позволит вызывать нужные методы для взаимодействия с модальным окном отовсюду из вашего UI — при условии импорта того же composable.
Вызов модальных окон с данными с бэкенда
Итак, у нас есть “обёртка”, описывающая условия анимации и рендеринга модальных окон разного типа, и у нас есть глобальный state, касающийся только модального окна. Теперь дело за малым: нам нужно как-то вызывать сами модальные окна и получать их данные в JSON для отображения на странице.
Для этого мы добавим компонент-триггер, который сможем стилизовать извне, передавать ему URL, по которому будет вызываться модальное окно, и использовать его использовать его в любой части пользовательского интерфейса. Внутри этот компонент будет содержать кнопку, реагировать на клики по ней и отправлять GET-запросы через Axios на наш бэкенд — по переданному в компонент URL.
Ответом на такие запросы будет приходить JSON-объект, который Axios при успешном выполнении запроса будет передавать в наш composable, где он будет фиксироваться в state модального окна, запуская процесс его отображения на странице. Таким образом, пока мы не сбросим этот state, у нас есть полностью интерактивное модальное окно, готовое к использованию вспомогательных composable из пакета Inertia.js типа useForm — со всеми их преимуществами (например, валидация форм).
Модальные окна в Inertia.js на бэкенде
Так как основные архетипы модальных окон бывают двух видов: для подтверждения действия и для формы ввода, — наша главная задача — выстроить в коде абстракцию, позволяющую отдавать с бэкенда модальные окна нужного типа, не привязываясь к конкретной имплементации. Для этого мы используем принцип полиморфизма и строгой типизации данных через DTO.
Подробное описание логики вы сможете найти прямо в коде моего репозитория на GitHub, но я объясню логику нашей системы поэтапно:
- Запрос с фронтенда прилетает в маршрутизатор Laravel и, если он содержит данные, то они проходят валидацию через FormRequest, и затем запрос попадет в нужный контроллер.
- В контроллере мы “отвязываемся” от объекта запроса и делегируем дальнейшие действия с данными сервису и/или репозиторию, переводя данные в DTO со строгой типизацией каждого свойства в этом DTO, передавая его в сервис или репозиторий.
- При обращении к сервису мы получаем готовый DTO модального окна, который можем вернуть из контроллера в ответе в формате JSON, а при обращении к репозиторию мы выполняем действия с данными в хранилище и можем вернуть ответ с flash-уведомлением по итогам (чтобы на фронтенде закрыть модальную форму и показать статус).
Полиморфизм в этой схеме мы применяем в том, как мы конструируем и используем DTO для любых типов наших модальных окон, а также при использовании объектов FormRequest, которые мы подчиняем общему контракту для модальных окон. Это позволяет масштабировать один подход и одну логику на любое количество модальных окон в любом количестве доменов и суб-доменов в архитектуре вашего проекта.
В моём демо-проекте это модальные окна для работы с учётными записями пользователей, но в вашем проекте это могут быть и заказы, и заявки, и т.п., и прелесть в том, что имплементацию под каждый из таких доменов вы можете изолировать от других, не меняя общую логику работы с модальными окнами, что отлично подойдёт к domain-driven дизайну.
Применяйте и адаптируйте этот подход под свои нужды
Это не урок по DDD или паттернам проектирования и тем более не готовый программный пакет. Я описываю подход, который использую в своей профессиональной практике на клиентских проектах, он полностью готов к применению в продакшене, но вам нужно будет самостоятельно делать свои архитектурные решения под задачи ваших проектов.
Поэтому я приглашаю вас навестить мой репозиторий на GitHub, где вы найдёте демо-проект на Laravel, который вы сможете клонировать к себе на машину и изучить комментарии и рекомендации в описании репозитория и в самом коде, а также попробовать подход в действии.
Если эта статья или мой код будут вам полезны, не забывайте делиться ими в соцсетях или поставить мне звезду на GitHub. Это мелочь, но всегда приятно видеть, что твои труды приносят пользу, и я надеюсь, что мои материалы будут полезными для многих!
Перейти в GitHub-репозиторий