1. Posts/

Backend rendered menu for Inertia.js

·3 mins

Recently when building applications using inertia.js I had to build a backend controlled menu.

Why?

Pre-rendering menu data directly in backend allows you to execute route authorization, this way deciding what menu entries are visible for users based on its role, access to a specific resource or you name it. It also keeps frontend templates light, by just providing computed data and leaving complex decisions to the backend code.

How?

  1. Pass menu to each response

Rendering inertia page accepts parameters to be passed to the so-called API response. You can pass that freshly built menu tree at each inertia render call, but it is not very convenient, and it will require a lot of refactoring if you choose to change something. Below is an example of how it could look like.

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Gate;
use Inertia\Inertia;

class DashboardController extends Controller
{
    public function __invoke()
    {
        return Inertia::render('Dashboard', [
            // additional data passed to view
            'menu' => function () {
                $menu = [];

                $menu[] = [
                    'title' => 'Dashboard',
                    'uri' => route('dashboard'),
                    'active' => request()->routeIs('dashboard'),
                ];

                if (Gate::allows('projects')) {
                    $menu[] = [
                        'title' => 'Projects',
                        'uri' => route('projects'),
                        'active' => request()->routeIs('projects*'),
                    ];
                }

                return $menu;
            },
        ]);
    }
}

And navigation part for vue.

<nav class="nav-main-sidebar space-y-1">
    <inertia-link
        v-for="menuItem in $page.menu.items"
        :key="menuItem.title"
        :href="menuItem.uri"
        class="group nav-main-sidebar-item"
        :class="{'is-active': menuItem.is_active}"
        @click="sidebarOpen = false"
    >
        {{ menuItem.title }}
    </inertia-link>
</nav>
  1. Use Inertia::share

Another approach would be to share menu entries using inertia’s share function, as it returns shared data on every request. This is my go-to solution. Below you can find code example how it can be implemented.

use Illuminate\Support\Facades\Gate;
use Inertia\Inertia;

Inertia::share([
    'menu' => function () {
        $menu = [];

        $menu[] = [
            'title' => 'Dashboard',
            'uri' => route('dashboard'),
            'active' => request()->routeIs('dashboard'),
        ];

        if (Gate::allows('projects')) {
            $menu[] = [
                'title' => 'Projects',
                'uri' => route('projects'),
                'active' => request()->routeIs('projects*'),
            ];
        }

        return $menu;
    },
]);

Place this code to ApplicationServiceProvider::boot.

Let`s sprinkle some extra flavor to our implementation and provide endpoint on which any part of the application can listen for events and append new menu entries dynamically. It gives us the ability to modify our menu entries in any part of the application, but it takes readability from us. For this case I built a small package to handle the menu with objects, you can find it at https://github.com/mbvienasbaitas/menu.

namespace App\Events;

use VienasBaitas\Menu\Menu;

class BuildMenuEvent
{
    public Menu $menu;

    public function __construct(Menu $menu)
    {
        $this->menu = $menu;
    }
}
Inertia::share('menu', function () {
    $menu = new \VienasBaitas\Menu\Menu();

    event(new \App\Events\BuildMenuEvent($menu));

    return (new (\VienasBaitas\Menu\Renderers\ArrayRenderer()))->render($menu);
});

Additional use cases

Tab navigation

Tab navigation is basically the same as menu navigation, it is just used in another context, mostly for a specific resource.