Smart client-side routing with navigation guards, dynamic params, and programmatic control.
- Quick Start
- Core Concepts
- Route Directives
- Programmatic Navigation
- Route Guards
- Lifecycle Events
- Advanced Usage
<!DOCTYPE html>
<html>
<head>
<title>My SPA</title>
</head>
<body>
<!-- Navigation Links -->
<nav>
<a t-link="'home'">Home</a>
<a t-link="'about'">About</a>
<a t-link="'user/42'">User Profile</a>
</nav>
<!-- Route Views -->
<div t-route="'home'">
<h1>Welcome Home!</h1>
</div>
<div t-route="'about'">
<h1>About Us</h1>
</div>
<div t-route="'user/:id'">
<div t-data="{ userId: $route.params.id }">
<h1 t-text="'User Profile: ' + userId"></h1>
</div>
</div>
<!-- 404 Fallback -->
<div t-route="*">
<h1>404 - Page Not Found</h1>
</div>
<script src="https://unpkg.com/tinypine@1.5.0/dist/tinypine.min.js"></script>
<script>
TinyPine.router({
default: "home",
});
TinyPine.init();
</script>
</body>
</html>TinyPine uses hash-based routing (#/path) for client-side navigation without page reloads.
URL Structure:
http://example.com/#/user/42?tab=posts
│ └─────┘ └────────┘
│ │ │
│ path query params
hash prefix
| Pattern | Example URL | Matches | Params |
|---|---|---|---|
home |
#/home |
Exact match | - |
user/:id |
#/user/42 |
Dynamic param | { id: '42' } |
post/:postId/comment/:id |
#/post/10/comment/5 |
Multiple params | { postId: '10', id: '5' } |
* |
#/anything |
Wildcard (404) | - |
Shows element only when the route matches.
Basic Usage:
<div t-route="'home'">
<h1>Home Page</h1>
</div>With Dynamic Parameters:
<div t-route="'user/:id'">
<div t-data="{}">
<p t-text="$route.params.id"></p>
</div>
</div>Nested Routes:
<div t-route="'dashboard'">
<h1>Dashboard</h1>
<nav>
<a t-link="'dashboard/overview'">Overview</a>
<a t-link="'dashboard/settings'">Settings</a>
</nav>
<div t-route="'dashboard/overview'">
<p>Overview content...</p>
</div>
<div t-route="'dashboard/settings'">
<p>Settings content...</p>
</div>
</div>404 Fallback:
<div t-route="*">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
</div>Smart router link that automatically manages href, click events, and active state.
Basic Usage:
<a t-link="'home'">Home</a> <a t-link="'about'">About</a>With Dynamic Params:
<div t-data="{ userId: 42 }">
<a t-link="'user/' + userId">View Profile</a>
</div>Active State:
/* Links automatically get .active class when route matches */
a.active {
color: blue;
font-weight: bold;
}Available in all t-data contexts and globally on TinyPine.router.
Navigate to a new route.
// Simple navigation
$router.push("/about");
// With parameters
$router.push("/user/:id", { params: { id: "42" } });
// With query parameters
$router.push("/search", { query: { q: "test", page: "1" } });
// Combined
$router.push("/user/:id", {
params: { id: "42" },
query: { tab: "posts" },
});Navigate without adding to history.
$router.replace("/login");Go back one step in history.
$router.back();Go forward one step in history.
$router.forward();Get current route information.
const route = $router.current();
console.log(route.path); // 'user/42'
console.log(route.params); // { id: '42' }
console.log(route.query); // { tab: 'posts' }Available in all t-data contexts.
{
path: 'user/42',
params: { id: '42' },
query: { tab: 'posts' },
pattern: 'user/:id',
hash: '#/user/42?tab=posts'
}Example:
<div t-route="'user/:id'">
<div t-data="{}">
<h1 t-text="'User ID: ' + $route.params.id"></h1>
<p t-text="'Current tab: ' + $route.query.tab"></p>
</div>
</div>Control navigation across all routes.
beforeEnter:
TinyPine.router({
beforeEnter(to, from) {
// Check authentication
const isLoggedIn = TinyPine.store("auth").loggedIn;
if (!isLoggedIn && to.path !== "login") {
return "/login"; // Redirect to login
}
return true; // Allow navigation
},
});beforeLeave:
TinyPine.router({
beforeLeave(to, from) {
// Confirm leaving unsaved changes
if (hasUnsavedChanges) {
const confirmed = confirm(
"You have unsaved changes. Leave anyway?"
);
return confirmed; // true = allow, false = cancel
}
return true;
},
});Apply guards to individual routes.
TinyPine.router({
routes: {
admin: {
beforeEnter(to, from) {
const isAdmin = TinyPine.store("user").role === "admin";
return isAdmin ? true : "/";
},
},
"edit/:id": {
beforeLeave(to, from) {
return confirm("Discard changes?");
},
},
},
});Guards can be async for API calls.
TinyPine.router({
async beforeEnter(to, from) {
try {
const response = await fetch("/api/verify-token");
const { valid } = await response.json();
return valid ? true : "/login";
} catch (error) {
console.error("Auth check failed:", error);
return "/login";
}
},
});| Return Value | Effect |
|---|---|
true |
Allow navigation |
false |
Cancel navigation |
'/path' |
Redirect to path |
Promise |
Wait for async result |
Listen to route changes across your application.
// Route is about to change (before guards)
TinyPine.router.on("route:before-enter", (to, from) => {
console.log("Navigating to:", to.path);
});
// Route has changed (after guards)
TinyPine.router.on("route:change", (to, from) => {
console.log("Route changed from", from.path, "to", to.path);
});
// Entering new route
TinyPine.router.on("route:enter", (to, from) => {
console.log("Entered:", to.path);
});
// Leaving current route
TinyPine.router.on("route:leave", (from, to) => {
console.log("Left:", from.path);
});
// Navigation error
TinyPine.router.on("route:error", (error) => {
console.error("Navigation error:", error);
});Page Analytics:
TinyPine.router.on("route:change", (to, from) => {
// Track page views
if (typeof gtag !== "undefined") {
gtag("config", "GA_MEASUREMENT_ID", {
page_path: to.path,
});
}
});Loading Indicators:
TinyPine.router.on("route:before-enter", () => {
document.body.classList.add("loading");
});
TinyPine.router.on("route:enter", () => {
document.body.classList.remove("loading");
});Scroll Management:
TinyPine.router.on("route:enter", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});<!-- In template -->
<div t-data="{}">
<button t-click="$router.push('/search', { query: { q: searchQuery } })">
Search
</button>
</div>
<!-- Access in route -->
<div t-route="'search'">
<div t-data="{}">
<p t-text="'Searching for: ' + $route.query.q"></p>
</div>
</div>Configure scroll behavior on route change.
TinyPine.router({
scrollBehavior: "smooth", // 'auto', 'smooth', or 'none'
});TinyPine supports a single global router instance, but you can manage different route sets:
TinyPine.router({
routes: {
// Public routes
home: {},
about: {},
contact: {},
// Auth routes
login: {},
register: {},
// Protected routes
dashboard: {
beforeEnter: requireAuth,
},
profile: {
beforeEnter: requireAuth,
},
},
});// Setup router with store integration
TinyPine.store("router", {
currentPath: "home",
previousPath: null,
});
TinyPine.router.on("route:change", (to, from) => {
TinyPine.store("router").currentPath = to.path;
TinyPine.store("router").previousPath = from.path;
});
// Use in templates
<div t-data="{}">
<p t-text="$store.router.currentPath"></p>
</div>;TinyPine.router({
beforeEnter(to, from) {
const publicRoutes = ["home", "about", "login"];
const isPublic = publicRoutes.includes(to.path);
const isLoggedIn = TinyPine.store("auth").loggedIn;
if (!isPublic && !isLoggedIn) {
return "/login";
}
return true;
},
});const routes = {
home: { title: "Home" },
about: { title: "About" },
"user/:id": {
title: "User Profile",
beforeEnter(to) {
// Validate user ID
return /^\d+$/.test(to.params.id);
},
},
};
TinyPine.router({ routes, default: "home" });<div t-route="*">
<h1>Oops! Page not found</h1>
<p>The page you're looking for doesn't exist.</p>
<a t-link="'home'">Go back home</a>
</div>✅ Good:
/user/42
/post/10/edit
/dashboard/settings
❌ Bad:
/u/42
/p10e
/dash-set
- t-route Directive Reference
- t-link Directive Reference
- Router Guards Guide
- Programmatic Navigation
- 404 Handling
TinyPine Router v1.5.0 Navigate like a pro — with guards, params, and full control.