# Register navigation items

By allowing consumers to register dynamic navigation items, Squide enables developers to build scalable modular applications with well-defined boundaries. Each module contributes its own navigation items, which the host assembles into a unified application.

Below are the most common use cases. For more details, refer to the reference documentation.

# Register a basic item

Typically, simple navigation items using only the $id, $label and to options will be defined:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "page-1",
        $label: "Page 1",
        to: "/page-1"
    });
};

# Register a nested item

Similarly to nested routes, a navigation item can be nested under an existing section be specifying a sectionId option that matches the section's $id option:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "link",
        $label: "Link",
        to: "/link"
    }, {
        // The following example takes for granted that a section with the "some-section" $id is already registered or will be registered.
        sectionId: "some-section"
    });
};

Navigation items can also be nested by registering multipe items in a single registration block:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    // Register the following menu hierarchy:
    //
    //  Section
    //  --- Nested Section
    //  ------- Nested Nested Link
    //  --- Nested Link
    runtime.registerNavigationItem({
        $id: "section",
        $label: "Section",
        children: [
            {
                $id: "nested-section",
                $label: "Nested Section",
                children: [
                    {
                        $id: "nested-nested-link",
                        $label: "Nested Nested Link",
                        to: "#"
                    }
                ]
            },
            {
                $id: "nested-link",
                $label: "Nested Link",
                to: "#"
            }
        ]
    });
};

# Register an item with a sorting priority

A $priority option can be defined for a navigation item to affect it's position in the menu. The sorting algorithm is as follow:

  • By default a navigation item have a priority of 0.
  • If no navigation item have a priority, the items are positioned according to their registration order.
  • If an item have a priority > 0, the item will be positioned before any other items with a lower priority (or without an explicit priority value).
  • If an item have a priority < 0, the item will be positioned after any other items with a higher priority (or without an explicit priority value).
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "about",
        $label: "About",
        $priority: 10,
        to: "/about"
    });

    runtime.registerNavigationItem({
        $id: "home",
        $label: "Home",
        // Because the "Home" navigation item has an higher priority, it will be rendered
        // before the "About" navigation item.
        $priority: 100,
        to: "/home"
    });
};

# Use dynamic segments for an item link

Sometimes a route's path depends on dynamic contextual values. To support those routes, navigation items can be defined with dynamic segments:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "user-profile",
        $label: "User profile",
        to: "/user-profile/:userId"
    });
};

# Use a React element in an item label

Navigation item labels are not limited to plain text. For greater flexibility, a label can contain any React component:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { QuestionMarkIcon } from "@sample/icons";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "about",
        $label: (
            <QuestionMarkIcon />
            <span>About</span>
        ),
        to: "/about"
    });
};

# Style an item

Sometimes a module knows best how its navigation items should be styled. To enforce a specific style on a navigation item, define the style option:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "about",
        $label: "About",
        style: {
            backgroundColor: "#000"
        },
        to: "/about"
    });
};

# Open an item link in a new tab

To open a navigation item in a new tab, set the target option to _blank:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "about",
        $label: "About",
        target: "_blank",
        to: "/about"
    });
};

# Conditionally render an item

The $canRender native property can be used to indicate whether a navigation item should be rendered by the layout:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "about",
        $label: "About",
        $canRender: (index: number) => {
            return index % 2 == 0;
        },
        to: "/about"
    });
};

It's the responsibility of the code rendering the menu to execute the navigation items $canRender function and conditionally render the items based on the return value.

import { Suspense } from "react";
import { Link, Outlet } from "react-router";
import { 
    useNavigationItems,
    useRenderedNavigationItems,
    isNavigationLink,
    type RenderItemFunction,
    type RenderSectionFunction
} from "@squide/firefly";

const renderItem: RenderItemFunction = (item, key) => {
    if (!item.canRender()) {    
        return null;
    }

    if (!isNavigationLink(item)) {
        return null;
    }

    const { label, linkProps, additionalProps } = item;

    return (
        <li key={key}>
            <Link {...linkProps} {...additionalProps}>
                {label}
            </Link>
        </li>
    );
};

const renderSection: RenderSectionFunction = (elements, key) => {
    return (
        <ul key={key}>
            {elements}
        </ul>
    );
};

export function RootLayout() {
    // Retrieve the navigation items registered by the modules.
    const navigationItems = useNavigationItems();

    // Transform the navigation items into React elements.
    const navigationElements = useRenderedNavigationItems(navigationItems, renderItem, renderSection);

    return (
        <>
            <nav>{navigationElements}</nav>
            <Suspense fallback={<div>Loading...</div>}>
                <Outlet />
            </Suspense>
        </>
    );
}

Register a conditional navigation item with a deferred registration
../register-deferred-nav-items/

# Render additional props on an item

Any properties defined in the $additionalProps option will be forwarded to the layout:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "about",
        $label: "About",
        $additionalProps: {
            highlight: true
        },
        to: "/about"
    });
};

It's the responsibility of the code rendering the menu to handle the additional properties.

import { Suspense } from "react";
import { Link, Outlet } from "react-router";
import { 
    useNavigationItems,
    useRenderedNavigationItems,
    isNavigationLink,
    type RenderItemFunction,
    type RenderSectionFunction
} from "@squide/firefly";

const renderItem: RenderItemFunction = (item, key) => {
    if (!item.canRender()) {    
        return null;
    }

    if (!isNavigationLink(item)) {
        return null;
    }

    const { label, linkProps, additionalProps: { highlight, ...additionalProps } } = item;

    return (
        <li key={key} style={{ fontWeight: highlight ? "bold" : "normal" }}>
            <Link {...linkProps} {...additionalProps}>
                {label}
            </Link>
        </li>
    );
};

const renderSection: RenderSectionFunction = (elements, key) => {
    return (
        <ul key={key}>
            {elements}
        </ul>
    );
};

export function RootLayout() {
    // Retrieve the navigation items registered by the modules.
    const navigationItems = useNavigationItems();

    // Transform the navigation items into React elements.
    const navigationElements = useRenderedNavigationItems(navigationItems, renderItem, renderSection);

    return (
        <>
            <nav>{navigationElements}</nav>
            <Suspense fallback={<div>Loading...</div>}>
                <Outlet />
            </Suspense>
        </>
    );
}

# Navigation menu

By default, every navigation item registered through registerNavigationItem is added to the root navigation menu. The navigation items API also supports defining additional menus.

# Define a menu

To define an additional menu in a layout or page, retrieve the navigation items associated with that menu by passing the menu's identifier to the useNavigationItems hook using the menuId option:

import { useNavigationItems } from "@squide/firefly";

const items = useNavigationItems({ menuId: "page-menu" });

Then, render the additional menu items using the useRenderedNavigationItems hook:

import { Link, Outlet } from "react-router";
import { 
    useNavigationItems,
    useRenderedNavigationItems,
    isNavigationLink,
    type RenderItemFunction,
    type RenderSectionFunction
} from "@squide/firefly";

const renderItem: RenderItemFunction = (item, key) => {
    if (!isNavigationLink(item)) {
        return null;
    }

    const { label, linkProps, additionalProps } = item;

    return (
        <li key={key}>
            <Link {...linkProps} {...additionalProps}>
                {label}
            </Link>
        </li>
    );
};

const renderSection: RenderSectionFunction = (elements, key) => {
    return (
        <ul key={key}>
            {elements}
        </ul>
    );
};

export function Page() {
    // Retrieve the navigation items for the additional menu.
    const navigationItems = useNavigationItems({ menuId: "page-menu" });

    // Transform the navigation items into React elements.
    const navigationElements = useRenderedNavigationItems(navigationItems, renderItem, renderSection);

    return (
        <h2>Page</h2>
        {navigationElements}
    );
}

# Register an item in a menu

Finally, register navigation items specifically for this additional menu using the menuId option when registering the items:

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerNavigationItem({
        $id: "page-1",
        $label: "Page 1",
        to: "/layout/page-1"
    }, { 
        menuId: "page-menu" 
    });
};

# Localize an item label

Refer to the localize resources essential page.