Feb 02, 2021

Route based modals with Laravel, Inertia.js and Vue.js

Modals can be a pain in the ass, but there's no way around them. I'm currently working on an application where we need them. Whilst working with Inertia.js and Vue.js we have many options available, let's have a look.

Illustration with routes and slide over

Route based modals with Laravel, Inertia.js and Vue.js

Available options

Working with Vue.js gives us more flexibility. We can easily take advantage of existing libraries such as vue-js-modal or creating our own client side one. However, this comes with some downsides. We now rely on the client side state which can look like:

<script>
// Our View.vue

export default {
  data() {
    return {
      createModalIsOpen: false
    }
  },

  mounted() {
    if (location.hash === 'createModal') {
      this.createModalIsOpen = true
    }
  }
}
</script>

We already see some problems arise. We're relying on a location hash example.app/resource#createModal which can't be read from the back-end. Outsmarting the system with a query parameter such as example.app/resource?modal=createModal solves that. But we still have a problem. How are we going to pass along modal specific data?

My approach

Okay, let's take a few steps back. What we actually want is example.app/resource/create and none of that query or hash nonsense. So, how do we achieve that?

// CompanyUserController.php

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);
    }
}

Looking good, but... Where did we get the modal() method from? And how do we handle this in the front-end? Slow down a bit. First, let's have a look at this method.

In the AppServiceProvider boot() method I've created a macro for the Inertia response factory:

// AppServiceProvider.php boot()

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

This simply passes the modal's path as prop to the front-end which we can take care off. Now we're going to see how we catch this on the front-end. Because this project is based on Vue 2, we're going to use a mixin:

// UseModal.js

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

export { useModal }

What this mixin basically does is checking if there's a modal component set. If there's one we're going to dynamically import the modal's Vue component, otherwise we return false and don't render anything. The @ symbol is an alias for ./resources/js you can achieve that easily using Laravel Mix.

The mixin does nothing on its own. We have to use it in our global Vue instance which will look like this:

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)

Alright, cool. We're almost set. There's only a few steps left. How are we going to render the actual component? Because we have the mixin we can easily obtain the component and render it inside our app layout so we can make use of the modal everywhere:

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

This is a dynamic Vue component, we can tell it what to render by passing a component name or path to the :is="<component>" attribute. Also, notice how we check if there's a modal and how we pass the data. The modal has access to the page props, just like a regular Inertia.js view.

You forgot something?

On the first glance, everything looks fine. However, if you were to build an edit modal you're now probably wondering: What to do if my create method has additional data such as roles, users or companies I can select from?

No problem, this we can easily solve by allowing the index method to have an additional parameter:

// CompanyUserController.php

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' => ['...', '...'],
        ]);
    }
}

Awesome, we now have a route based modal with Inertia.js and Vue.js. I hope this will be useful for your projects (for now). I say for now because Jonathan and the Inertia team is working on modals as well. So until that ships, feel free to use this implementation.

Don't want to miss out?