← Recipe: add typed navigation

3. Replace src/main.luau

luastra/navigation · checked parameters · about 15 minutes
complete runnable entry module

Route definitions validate parameters and queries before a mutation can enter the stack.

When to use it

Replace the entire generated src/main.luau file; the compact layout intentionally omits host History so the router contract is visible first.

Focused example

--!strict

local Navigation = require("luastra/navigation")
local UI = require("luastra/ui")

local routes = Navigation.compile {
    { name = "home", path = "/" },
    {
        name = "workspace",
        path = "/workspaces/:workspace_id",
        params = { workspace_id = { type = "integer", minimum = 1, maximum = 999 } },
    },
    {
        name = "document",
        parent = "workspace",
        path = "documents/:document_slug",
        params = { document_slug = { type = "string", maximumLength = 32 } },
        query = { mode = { type = "enum", values = { "read", "edit" }, required = true } },
    },
}

local router = Navigation.createRouter {
    compiler = routes,
    initial = { name = "home", params = {}, query = {} },
}

local App = require("luastra/app")
local Root = {}

function Root.render(): UI.Node
    local current = router.current()
    return UI.Screen {
        id = "routing-lab",
        UI.Text { id = "routing/title", text = "Routing lab", variant = "title" },
        UI.Text { id = "routing/name", text = "Route: " .. current.name },
        UI.Text { id = "routing/location", text = router.currentLocation(), role = "status" },
        UI.Actions {
            id = "routing/actions",
            UI.Button {
                id = "routing/workspace",
                text = "Open workspace",
                onTap = "open-workspace",
            },
            UI.Button {
                id = "routing/document",
                text = "Open document",
                onTap = "open-document",
            },
            UI.Button {
                id = "routing/back",
                text = "Back",
                onTap = "back",
                disabled = not router.canBack(),
            },
        },
    }
end

function Root.handle(action: string, target: string, _value: string)
    if action == "open-workspace" and target == "routing/workspace" then
        router.push { name = "workspace", params = { workspace_id = 7 }, query = {} }
    elseif action == "open-document" and target == "routing/document" then
        router.push {
            name = "document",
            params = { workspace_id = 7, document_slug = "release-notes" },
            query = { mode = "edit" },
        }
    elseif action == "back" and target == "routing/back" then
        router.back()
    end
end

function Root.snapshot()
    return {
        name = router.current().name,
        location = router.currentLocation(),
    }
end

return App.compose {
    render = function(_outputs) return Root.render() end,
    handle = Root.handle,
    snapshot = Root.snapshot,
}

Complete inventory

View all pages in Recipe: add typed navigation
← Previous: 2. Replace luastra.jsonNext: 4. Replace tests/smoke.luau →