Sidebar Navigation

The sidebar of a Nexus application is configured in resources/nexus/vue/LayoutSidebar.vue. This gives the application developer full control over the menu structure.

Nexus includes two helper components for creating standard menu layouts. It supports up to 3 levels of navigation (links), optionally broken up into sections.

A typical link is added like this:

<LayoutSidebarLink
    active="nexus.home"
    :href="$route('nexus.home')"
    text="Dashboard"
/>

The href must be a link to an internal Inertia.js page (external links are not supported, as this would be confusing for users).

The text prop is the text to be displayed on the link.

To include icons or other HTML you can use the #text slot instead:

<LayoutSidebarLink
    active="nexus.home"
    :href="$route('nexus.home')"
>
    <template #text>
        <FontAwesome :icon="faHome" text="Dashboard" />
    </template>
</LayoutSidebarLink>

The active parameter determines when the link is styled as the active (current) link. Only one link at each level should be active at once. In the simplest case, it is the name of the route you are linking to, as above. If it is a section with several sub-pages, use a wildcard:

<LayoutSidebarLink
    active="examples.*"
    :href="$route('examples.index')"
    text="Examples"
/>

If there are several pages with different prefixes, use an array of strings:

<LayoutSidebarLink
    :active="['examples.*', 'more-examples.*']"
    :href="$route('examples.index')"
    text="Examples"
/>

For more advanced scenarious, set active to a boolean:

<LayoutSidebarLink
    :active="$routeIs('examples.index') && $page.props.request.deleted === '1'"
    :href="$route('examples.index', { deleted: true })"
    text="Deleted"
/>

Note: Request parameters are always strings. You must return a boolean, not a string, so simplifying this to ... && $page.props.request.deleted would not work (it would return the string '1').

Nested Menus

Each link can have children, up to 3 levels deep:

<LayoutSidebarLink
    active="examples.*"
    :href="$route('examples.index')"
    text="Examples"
>
    <LayoutSidebarLink
        active="examples.one"
        :href="$route('examples.one')"
        text="Example 1"
    />
    <LayoutSidebarLink
        active="examples.two"
        :href="$route('examples.two')"
        text="Example 2"
    >
        ...
    </LayoutSidebarLink>
</LayoutSidebarLink>

[Demo with 3 levels - expand the "Demo Package" section to see it.]

The children are only displayed when the link is active.

Generally the first child link should be a self-link - i.e. a link to the same page as the parent link - to make it easy for the user to get back to that page:

<LayoutSidebarLink
    active="examples.*"
    :href="$route('examples.index')"
    text="Examples"
>
    <template #self>
        <LayoutSidebarLink
            active="examples.index"
            :href="$route('examples.index')"
            text="Examples"
        />
    </template>
    ...
</LayoutSidebarLink>

This is similar to the WordPress admin area.

If the self-link is the only child, it will be hidden. This allows you to conditionally hide the other children (see below) and the menu will adapt automatically if there is only the one link left.

Section Navigation

As an alternative to nested menus, you may want to consider using section nav bars (tabs) for some links.

Sections (Subheadings)

After the first batch of links with no heading, there may be any number of sections with headings. Sections may not be nested, but each section may contain up to three levels of links. For example:

<LayoutSidebarSection title="Administration">

    <LayoutSidebarLink
        active="settings.*"
        :href="$route('settings.index')"
        text="Settings"
    >
        ...
    </LayoutSidebarLink>

    ...

</LayoutSidebarSection>

Collapsible Sections

You can optionally make the section collapsible. This is useful for hiding less important sections such as Administration that aren't regularly needed.

<LayoutSidebarSection title="Administration" collapsible>
    ...
</LayoutSidebarSection>

Collapsible sections should generally be at the end of the sidebar, after any non-collapsible sections/links. It is less confusing for users if you make all sections either collapsible or not, rather than mixing the two - although there may be cases where that is necessary.

By default, collapsible sections are collapsed when the page loads. You can choose to have them open by default:

<LayoutSidebarSection title="Administration" collapsible default-open>
    ...
</LayoutSidebarSection>

When the user expands/collapses a section, it is remembered using localStorage - i.e. per browser, not per user. The key name is sidebar-expanded- + the title in lowercase - so if two subheadings have the same title they will conflict, and if you change a title the state will be forgotten. You can override this by providing the id parameter, e.g. id="administration".

Note: Because the state is remembered, you may not see any difference when adding/removing the default-open prop. To test it, clear your local storage using web developer tools, then refresh the page.

HTML in Titles

Similar to links, you can use the #title slot instead of the title prop if you want to use any HTML:

<LayoutSidebarSection>
    <template #title>
        <span class="mr-1">{{ school.school_name }}</span>
        <span v-if="school.is_partner" class="badge badge-info">Partner</span>
    </template>
    ...
</LayoutSidebarSection>

However, if you use collapsible with the #title slot, you must also provide the id parameter since it can't be generated automatically:

<LayoutSidebarSection collapsible id="demo package">
    <template #title>
        <em>Demo</em> Package
    </template>
    ...
</LayoutSidebarSection>

(If you forget, you will get an error message in the console.)

To conditionally show/hide a link, use v-if on the link itself.

If you need data from the server (e.g. user permissions), add it to nav array in App\Providers\InertiaServiceProvider:

Inertia::share('nav', static function () {
    //...
    return [
        //...
        'userCan' => user_can([
            Permission::ViewExamples,
        ]),
    ];
});

Then use v-if="$page.props.nav.<name>":

<LayoutSidebarLink
    v-if="$page.props.nav.userCan.ViewExamples"
    active="examples.*"
    :href="$route('examples.index')"
    text="Examples"
/>

Warning: The nav array is included in the response on every Inertia.js page load - so don't load a large amount of data.

Some Nexus packages provide components so you can easily add links to their pages, e.g.

  • <LayoutSidebarLinkUsers /> (Nexus Users - admin links)
  • <LayoutSidebarLinkSwitchBack /> (Nexus Users - masquerade function)
  • <LayoutSidebarLinkMediaLibrary /> (Nexus Media Library)

If you need more control, you can manually add the links instead.

Notes for Package Developers

  • These components should be functional components - otherwise Vue thinks there is always a child link even when they are all hidden, so the link/section collapsing doesn't work correctly.
  • Data can be added to every page using Inertia::share() in the service provider. It should be namespaced using the package name - e.g. nexusUsers_userCan.

Component Reference

<LayoutSidebar>

Props: None.

Slots: None.

Events: None.

Props:

  • href (String - required) - The URL to link to.
  • text (String - required unless the #text slot is used) - The link text.
  • active - (Boolean, String or Array) - Either a boolean to say whether the link is active or not, or a string/array of route name patterns that should be treated as active.
  • link-class - (String, Array or Object) - Classes to add to the <a> tag.

Slots:

  • #text - An alternative to the text prop, in case you want to format it with HTML.
  • #self - A link to the default page in the section. If this is the only child link visible, it is hidden.
  • default - Optional child links.

Events: None.

<LayoutSidebarSection>

Props:

  • title (String, required unless the #title slot is used) - The section title.
  • collapsible (Boolean) - Allow the user to open/close the section.
  • default-open (Boolean) - Default a collapsible section to open instead of closed.
  • id (String) - The ID to use to record the collapse state. Defaults to the title prop in lowercase. Required if collapsible is true and the #title slot is used rather than the title prop.

Slots:

  • default - Child links. If there are no child links visible, the section is hidden.
  • #title - An alternative to the title prop, in case you want to format it with HTML.

Events: None.