Enrutamiento dentro de la app
Describe las páginas como un enum, expón la página actual y sus parámetros como dos Signals y obtén atrás / adelante de una pila de historial de capacidad fija.
El modelo de Router
ui.fx.Router(Page) es un router en memoria guiado por un enum, no un router de URL web. Expone dos Signals, current: *Signal(Page) y params: *Signal(RouteParams), y mantiene una pila de historial de hasta 64 entradas.
const std = @import("std");
const ui = @import("ui");
const Page = enum { welcome, editor, settings };
const Router = ui.fx.Router(Page);
fn mountApp(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
// The router's two Signals are created in `scope`, so pass a scope that
// lives as long as the window. The router value itself is large (~290 KB of
// inline history), so keep it in Cx-owned state and pass the pointer around.
const router = try cx.bindState(Router, try Router.init(scope, .welcome));
const shell = try ui.box(cx, .{
.width = .fill(),
.height = .fill(),
.direction = .column,
}, .{});
// ...append a toolbar and the page host (see "Rendering pages")
_ = router;
return shell;
}Navegación y parámetros
var params: ui.fx.router.RouteParams = .{};
try params.set("file", "draft.md"); // copies the bytes; errors on overflow
router.navigate(.editor, params); // page + params
router.navigateTo(.settings); // page, empty params
_ = router.back(); // false when already at the oldest entry
_ = router.forward(); // false when there is no forward branch
if (router.canGoBack()) {
// e.g. enable a Back button
}RouteParams.set copia los bytes de la clave y el valor en sus propios búferes, así que los cambios posteriores en el búfer de quien llama no le afectan; asignar una clave existente sobrescribe su valor. La capacidad es fija y el desbordamiento devuelve un error en lugar de truncar:
max_params = 8error.TooManyParamsmax_key_len = 64error.KeyTooLongmax_value_len = 512error.ValueTooLongnavigate, back y forward escriben en ambos Signals. canGoBack() / canGoForward() son lecturas simples, no Signals. Para controlar el router desde un handler, guarda el puntero en un struct de cx.bindState:
const Nav = struct {
router: *Router,
fn goBack(self: *Nav) void {
_ = self.router.back();
}
};
const nav = try cx.bindState(Nav, .{ .router = router });
const back_button = try ui.widgets.Button(.{
.label = "Back",
.on_click = cx.on(Nav, nav, Nav.goBack),
}).mount(scope, cx);Renderizar páginas
La página actual es un Signal(Page); cambia los subárboles de página según su valor con ui.Match. Match da a cada página un Scope hijo; al cambiar, primero hace dispose del Scope anterior (sus Signals, Effects y recursos se van con él), luego libera los nodos antiguos y monta la nueva página.
const page_host = try ui.box(cx, .{
.width = .fill(),
.height = .fill(),
}, .{});
// Match appends the page to page_host and rebuilds it when router.current
// changes; each page gets a fresh child Scope that is disposed on leave.
_ = try ui.Match(Page, scope, page_host, router.current, cx, struct {
fn build(child_scope: *ui.Scope, c: *ui.Cx, page: Page) anyerror!*ui.Node {
return switch (page) {
.welcome => mountWelcome(c, child_scope),
.editor => mountEditor(c, child_scope),
.settings => mountSettings(c, child_scope),
};
}
}.build);Match solo reconstruye cuando cambia el valor del enum: navegar de .editor a .editor con otros parámetros mantiene la página montada. Una página que depende de los parámetros debe suscribirse ella misma a router.params:
// editor -> editor with other params does not rebuild the page,
// so subscribe to the params Signal inside the page.
try scope.createEffect(.{ .params = router.params }, struct {
fn run(ctx: anytype) void {
// get() returns a copy; keep it in a local so the slices stay valid.
const params = ctx.params.get();
const file = params.get("file") orelse "untitled.md";
std.log.info("editor shows {s}", .{file});
}
}.run);Reglas del historial
- ✓
Una nueva navegación trunca la rama hacia adelante. Navegar después de back descarta las entradas forward anteriores.
- ✓
Cada navigate apila. La misma página con otros parámetros — o incluso un destino idéntico — se convierte en una nueva entrada.
- ✓
Con 64 entradas, se descarta la mitad más antigua. Antes del apilado número 65 se conservan las 32 más recientes y el cursor se desplaza con ellas.
- ✓
Sin URLs. Los deep links y los URL schemes del sistema debe mapearlos la app a
navigate.
Registrar la navegación
El router no registra nada por sí mismo. Para depurar la navegación, escribe las transiciones relevantes en la Console del Cx actual bajo un scope router; la Console de DevTools podrá filtrar por scope y mostrar el nivel y el punto de llamada.
router.navigate(.editor, params);
cx.console().scoped("router").debug("navigate -> {s}", .{@tagName(Page.editor)});Consulta DevTools para la API completa de Console.