Lab / Laravel / InertiaJS

Laravel: Модальные окна с состоянием на основе роутов в связке Inertia + VueJS

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

Сегодня я поделюсь своим опытом работы с модалками в проекте на Laravel + Inertia + VueJS.

🚧 Статья рассчитана на тех, кто уже имеет минимальный опыт работы с Inertia.js и Vue.js

Доступные варианты

Использование Vue.js даёт нам отличные преимущества. Можно скачать огромное количество готовых компонентов типа: vue-js-modal. Или же можно набросать собственный простейший компонент. В любом из случаев прийдётся хранить состояние на стороне клиента, примерно таким образом:

<script>
export default {
  data() {
    return {
      createModalIsOpen: false
    }
  },
  mounted() {
    if (location.hash === 'createModal') {
      this.createModalIsOpen = true
    }
  }
}
</script>

Как видно из примера /resource#createModal, мы полагаемся на хешь в адресной строке, но нам никак не получить это значение на бекенде. Можно конечно использовать url параметры resource?modal=createModal, но вопрос передачи данных в модальное окно по-прежнему остаётся открытым.

Вариант решения

Самым логичным решением, как мне видится — будет создание роута вида: /resource/create, ниже привожу пример моего контроллера:

class CompanyUserController
{
    public function index(Company $company)
    {
        return inertia('Companies/Users/Index', [
            'company' => $company,
            'users' => $company
                ->users()
                ->orderBy('created_at', 'desc')
                ->paginate(),
        ]);
    }

    public function create(Company $company)
    {
        inertia()->modal('Companies/Users/CreateModal');

        return $this->index($company);
    }
}

Внимательный читатель заметит, что у нас нигде нет вызова метода модалки modal() и подумает, что я, что-то упустил. Но ответ гораздо проще. Давайте присмотрим, что у нас тут в сервис провайдере AppServiceProvider, а точнее в его методе boot()

// AppServiceProvider.php boot()

ResponseFactory::macro('modal', function ($modal) { 
    inertia()->share(['modal' => $modal]); 
});

Я создал макрос для фабрики ответов из Inertia. В нём мы просто передаём адрес как пропс на фронтенд. Собственно давайте теперь его обработаем на фронтенде. Для этого я воспользуюсь миксинами из Vue.js

// UseModal.js

const useModal = { 
  computed: { 
    modalComponent() { 
      return this.$page.props.modal 
        ? () => import(`@/Pages/${this.$page.props.modal}`) 
        : false 
    }
  }
}

export { useModal }

Этот миксин проверяет, задан ли модальный компонент и либо динамически импортирует компонент Vue либо возвращает false и ничего не рендерит. Символ @ это алиас для ./resources/js это одна из возможностей Laravel Mix.

Сам по себе наш миксин ничего не делает. Нужно использовать его в глобальном инстансе нашего Vue приложения. Вот пример того, как это может выглядеть:

new Vue({ 
  mixins: [useModal], 
  render: h => h(App, {
    props: {
      initialPage: JSON.parse(el.dataset.page),
      resolveComponent: name => import(`./Pages/${name}`).then(module => module.default), 
    }, 
  }),
}).$mount(el)

Остался последний вопрос, как же мы будем рендерить компонент модалки? Поскольку у нас теперь есть миксин, мы можем вызывать и рендерить компонент из любой части приложения. Пример вызова компонента:

<Component 
  v-bind="$page.props" 
  v-if="$root.modalComponent" 
  :is="$root.modalComponent"
/>

Это динамический Vue компонент, таким образом мы можем передавать данные для отображения, имя и путь в атрибут :is="<component>"

Обратите внимание, как мы проверяем наличие модального окна и как мы передаем данные. Модальное окно имеет доступ к пропсам, как и обычное представление Inertia

Как передать дополнительные параметры в пропсы?

Для того, что бы передать дополнительные данные, например список пользователей, ролей или иную связанную модель, нужно просто передать массив в наш index метод:


class CompanyUserController
{
    public function index(Company $company, array $modalProps = [])
    {
        return inertia('Companies/Users/Index', array_merge([
            'company' => $company,
            'users' => $company
                ->users()
                ->orderBy('created_at', 'desc')
                ->paginate(),
        ], $modalProps));
    }

    public function create(Company $company)
    {
        inertia()->modal('Companies/Users/CreateModal');

        return $this->index($company, [
            'roles' => Role::all(),
            'moreOptions' => ['...', '...'],
        ]);
    }
}