Модальные окна в веб-приложениях на 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). Поэтому, чтобы решить проблему глобального управления модальным окном из разных частей приложения, нам нужно выполнить две задачи:

  1. Добавить в глобальный layout компонент-обёртку модального окна, внутри которого все модальные окна будут рендериться динамически. Этот компонент будет содержать публичный API для взаимодействия с модальным окном и Vue transition для анимации.
  2. Добавить собственный composable и импортировать его в главный обёрточный компонент модальных окон. Этот composable будет содержать своё “реактивное” состояние и публичный API для управления им, что позволит вызывать нужные методы для взаимодействия с модальным окном отовсюду из вашего UI — при условии импорта того же composable.

Вызов модальных окон с данными с бэкенда

Итак, у нас есть “обёртка”, описывающая условия анимации и рендеринга модальных окон разного типа, и у нас есть глобальный state, касающийся только модального окна. Теперь дело за малым: нам нужно как-то вызывать сами модальные окна и получать их данные в JSON для отображения на странице.

Для этого мы добавим компонент-триггер, который сможем стилизовать извне, передавать ему URL, по которому будет вызываться модальное окно, и использовать его использовать его в любой части пользовательского интерфейса. Внутри этот компонент будет содержать кнопку, реагировать на клики по ней и отправлять GET-запросы через Axios на наш бэкенд — по переданному в компонент URL.

Ответом на такие запросы будет приходить JSON-объект, который Axios при успешном выполнении запроса будет передавать в наш composable, где он будет фиксироваться в state модального окна, запуская процесс его отображения на странице. Таким образом, пока мы не сбросим этот state, у нас есть полностью интерактивное модальное окно, готовое к использованию вспомогательных composable из пакета Inertia.js типа useForm — со всеми их преимуществами (например, валидация форм).

Модальные окна в Inertia.js на бэкенде

Так как основные архетипы модальных окон бывают двух видов: для подтверждения действия и для формы ввода, — наша главная задача — выстроить в коде абстракцию, позволяющую отдавать с бэкенда модальные окна нужного типа, не привязываясь к конкретной имплементации. Для этого мы используем принцип полиморфизма и строгой типизации данных через DTO.

Подробное описание логики вы сможете найти прямо в коде моего репозитория на GitHub, но я объясню логику нашей системы поэтапно:

  1. Запрос с фронтенда прилетает в маршрутизатор Laravel и, если он содержит данные, то они проходят валидацию через FormRequest, и затем запрос попадет в нужный контроллер.
  2. В контроллере мы “отвязываемся” от объекта запроса и делегируем дальнейшие действия с данными сервису и/или репозиторию, переводя данные в DTO со строгой типизацией каждого свойства в этом DTO, передавая его в сервис или репозиторий.
  3. При обращении к сервису мы получаем готовый DTO модального окна, который можем вернуть из контроллера в ответе в формате JSON, а при обращении к репозиторию мы выполняем действия с данными в хранилище и можем вернуть ответ с flash-уведомлением по итогам (чтобы на фронтенде закрыть модальную форму и показать статус).

Полиморфизм в этой схеме мы применяем в том, как мы конструируем и используем DTO для любых типов наших модальных окон, а также при использовании объектов FormRequest, которые мы подчиняем общему контракту для модальных окон. Это позволяет масштабировать один подход и одну логику на любое количество модальных окон в любом количестве доменов и суб-доменов в архитектуре вашего проекта.

В моём демо-проекте это модальные окна для работы с учётными записями пользователей, но в вашем проекте это могут быть и заказы, и заявки, и т.п., и прелесть в том, что имплементацию под каждый из таких доменов вы можете изолировать от других, не меняя общую логику работы с модальными окнами, что отлично подойдёт к domain-driven дизайну.

Применяйте и адаптируйте этот подход под свои нужды

Это не урок по DDD или паттернам проектирования и тем более не готовый программный пакет. Я описываю подход, который использую в своей профессиональной практике на клиентских проектах, он полностью готов к применению в продакшене, но вам нужно будет самостоятельно делать свои архитектурные решения под задачи ваших проектов.

Поэтому я приглашаю вас навестить мой репозиторий на GitHub, где вы найдёте демо-проект на Laravel, который вы сможете клонировать к себе на машину и изучить комментарии и рекомендации в описании репозитория и в самом коде, а также попробовать подход в действии.

Если эта статья или мой код будут вам полезны, не забывайте делиться ими в соцсетях или поставить мне звезду на GitHub. Это мелочь, но всегда приятно видеть, что твои труды приносят пользу, и я надеюсь, что мои материалы будут полезными для многих!

Перейти в GitHub-репозиторий