Where do you want to go? The Router
The true power of the Lego router isn't just changing the URL; it's the targeted DOM injection that allows you to swap any part of the page with any block, without writing a single line of fetch or innerHTML logic.
The "Surgical" Philosophy
Most SPAs use a Replacer Strategy: URL Change -> Match Route -> Destroy App -> Rebuild App with new Page
LegoDOM uses a Surgical Strategy: URL Change -> Match Route -> Find Targets (#sidebar, #main) -> Modify ONLY those nodes
Location in the Codebase
The router lives in src/features/router.js:
// src/features/router.js
import { snap } from '../core/lifecycle.js';
import { registry } from '../core/registry.js';
import { globals } from '../core/globals.js';
import { defineLegoFile } from '../core/parser.js';
export const routes = [];
export const _go = (href, ...targets) => {
return {
get: (shouldPush = true) => {
const [pathname, search] = href.split('?');
const url = pathname + (search ? `?${search}` : '');
// Update history
if (shouldPush) {
history.pushState({ legoTargets: targets }, '', url);
}
// Update $route global
globals.$route.url = url;
globals.$route.method = 'GET';
// Find matching route
const match = routes.find(r => r.regex.test(pathname));
if (!match) {
console.warn(`[Lego Router] No route found for: ${pathname}`);
return;
}
// Extract params
const values = pathname.match(match.regex).slice(1);
const params = Object.fromEntries(
match.paramNames.map((n, i) => [n, values[i]])
);
const query = Object.fromEntries(new URLSearchParams(search || ''));
globals.$route.route = match.path;
globals.$route.params = params;
globals.$route.query = query;
// Determine targets
const resolvedTargets = targets.length > 0
? targets
: ['lego-router'];
// Swap content in each target
resolvedTargets.forEach(selector => {
const container = document.querySelector(selector);
if (!container) {
console.warn(`[Lego Router] Target not found: ${selector}`);
return;
}
// Clear existing content
container.innerHTML = '';
// Create new block element
const el = document.createElement(match.tagName);
container.appendChild(el);
// Snap the new block
snap(el);
});
},
post: (body) => {
globals.$route.method = 'POST';
globals.$route.body = body;
return this.get(true);
}
};
};
export const _matchRoute = (targets) => {
const path = window.location.pathname;
const search = window.location.search;
_go(path + search, ...targets).get(false);
};Route Registration
Routes are registered via Lego.route() in src/index.js:
// src/index.js
const Lego = {
route: (path, tagName, options = {}) => {
// Convert path to regex with param extraction
const paramNames = [];
const pattern = path.replace(/:(\w+)/g, (_, name) => {
paramNames.push(name);
return '([^/]+)';
});
routes.push({
path,
tagName,
regex: new RegExp(`^${pattern}$`),
paramNames,
guards: options.guards || []
});
}
};Usage:
Lego.route('/users/:id', 'user-profile');
Lego.route('/dashboard', 'dashboard-page');
Lego.route('/posts/:slug/edit', 'post-editor');Surgical Swapping
The key innovation is the targets parameter:
// In your block or HTML:
<a href="/profile" @click.prevent="$go('/profile', '#main', '#sidebar').get()">
Go to Profile
</a>This will:
- Navigate to
/profile - Update only
#mainand#sidebar - Leave everything else untouched
Why this matters
This architecture enables Persistent Shells. You can have:
- A sidebar that plays music or holds chat state
- A main content area that navigates freely
- A header that shows notifications
Traditional routers usually require complex "Layout Blocks" to achieve this. LegoDOM does it by simply not touching the sidebar.
Intelligent Defaults
If no targets are specified, LegoDOM looks for <lego-router>:
const resolvedTargets = targets.length > 0
? targets
: ['lego-router'];This hybrid approach gives you the best of both worlds:
- Rapid prototyping: Just use
<lego-router>and$go('/page') - App-like fidelity: Use surgical targets when needed
The $route Global
The router updates globals.$route (from src/core/globals.js):
// src/core/globals.js
export const globals = {
$route: {
url: '',
route: '',
params: {},
query: {},
method: 'GET',
body: null
},
$go: null, // Set by index.js during init
$db: null // Set by index.js during init
};You can access route info in any block:
<template>
<div>
<h1>User ID: [[ $route.params.id ]]</h1>
<p>Search: [[ $route.query.search ]]</p>
</div>
</template>Link Interception
During init(), LegoDOM sets up automatic link interception:
// src/index.js (in Lego.init)
document.addEventListener('click', e => {
const path = e.composedPath();
const link = path.find(el =>
el.tagName === 'A' &&
(el.hasAttribute('b-target') || el.hasAttribute('b-link'))
);
if (link) {
e.preventDefault();
const href = link.getAttribute('href');
const targetAttr = link.getAttribute('b-target');
const targets = targetAttr ? targetAttr.split(/\s+/).filter(Boolean) : [];
globals.$go(href, ...targets).get();
}
});This allows declarative routing:
<a href="/profile" b-link>My Profile</a>
<a href="/settings" b-target="#main #sidebar">Settings</a>History Management
The router integrates with the browser's History API:
// Popstate for back/forward buttons
window.addEventListener('popstate', (event) => {
const targets = event.state?.legoTargets || [];
_matchRoute(targets);
});When you click the back button, LegoDOM re-applies the same surgical targets that were used for that navigation.
Summary: The router in src/features/router.js uses surgical DOM updates to enable persistent UI shells. It supports parameterized routes, query strings, multiple targets, and integrates seamlessly with the browser's History API. The $route global makes route data available everywhere, while $go provides programmatic navigation with POST support.