Now.js Framework Documentation
RouterManager
RouterManager
Overview
RouterManager is the client-side routing system in Now.js Framework for SPA (Single Page Application).
When to use:
- Need SPA routing
- Need page navigation without reload
- Need route guards
- Need dynamic routes
Why use it:
- ✅ Hash and History mode
- ✅ Route parameters
- ✅ Navigation guards
- ✅ Lazy loading
- ✅ Auto template loading
- ✅ Scroll restoration
Basic Usage
Initialization
await RouterManager.init({
mode: 'history', // or 'hash'
routes: [
{ path: '/', template: '/pages/home.html' },
{ path: '/about', template: '/pages/about.html' },
{ path: '/users/:id', template: '/pages/user.html' }
]
});HTML Links
<!-- Regular links work automatically -->
<a href="/about">About</a>
<a href="/users/123">User Profile</a>
<!-- With query string in href -->
<a href="/search?q=hello&page=1">Search</a>
<!-- Programmatic navigation -->
<button onclick="RouterManager.navigate('/contact')">Contact</button>Passing Query Parameters via Links
RouterManager supports data-params and data-param-* attributes to pass query parameters through <a> links without writing JavaScript expressions.
data-params — Forward params from current URL
Reads query parameters from the current URL (window.location.search) and forwards them to the destination.
<!-- Forward only "parent" param from current URL -->
<!-- Current URL: /menus?parent=0_MAINMENU -->
<!-- Navigates to: /menu?parent=0_MAINMENU -->
<a href="/menu" data-params="parent">Add Menu</a>
<!-- Forward multiple params -->
<!-- Current URL: /menus?parent=0_MAINMENU&lang=en -->
<!-- Navigates to: /menu?parent=0_MAINMENU&lang=en -->
<a href="/menu" data-params="parent,lang">Add Menu</a>
<!-- Forward ALL params from current URL -->
<a href="/menu" data-params="*">Add Menu</a>data-param-* — Add fixed params
Appends fixed query parameters regardless of the current URL.
<!-- Navigates to: /menus?parent=0_MAINMENU -->
<a href="/menus" data-param-parent="0_MAINMENU">Main Menu</a>
<!-- Multiple fixed params -->
<a href="/menus" data-param-parent="0_MAINMENU" data-param-lang="en">Main Menu</a>Combining both
data-params (from URL) and data-param-* (fixed) can be combined. Fixed values take precedence.
<!-- Forward "lang" from URL, but always set type=sub -->
<a href="/menu" data-params="lang" data-param-type="sub">Add Sub-menu</a>Configuration
RouterManager.init({
mode: 'history',
routes: [
{ path: '/', template: '/pages/home.html' },
{ path: '/products', template: '/pages/products.html' },
{ path: '/products/:id', template: '/pages/product.html' }
],
// Container for page content
container: '#app',
// Default route
defaultRoute: '/',
// 404 template
notFoundTemplate: '/pages/404.html',
// Scroll behavior
scrollBehavior: 'top', // 'top', 'restore', 'none'
// Base path
basePath: '',
// API integration
api: {
enabled: true,
format: '/api{path}'
}
});Route Definition
{
// URL path
path: '/users/:id',
// Template file
template: '/pages/user.html',
// API endpoint
api: '/api/users/:id',
// Route name
name: 'user-profile',
// Metadata
meta: {
requiresAuth: true,
title: 'User Profile'
},
// Before enter guard
beforeEnter: (to, from) => {
if (!isAuthenticated()) {
return '/login'; // Redirect
}
return true; // Allow
}
}API Reference
RouterManager.navigate(path, options?)
Navigate to path
| Parameter | Type | Description |
|---|---|---|
path |
string | Target path |
options.replace |
boolean | Replace history |
options.params |
object | Query parameters |
RouterManager.navigate('/users/123');
RouterManager.navigate('/search', { params: { q: 'john' } });
RouterManager.navigate('/login', { replace: true });RouterManager.go(delta)
Navigate history
RouterManager.go(-1); // Back
RouterManager.go(1); // ForwardRouterManager.back()
Go back
RouterManager.back();RouterManager.forward()
Go forward
RouterManager.forward();RouterManager.getCurrentRoute()
Get current route info
Returns: Object
const route = RouterManager.getCurrentRoute();
// { path: '/users/123', params: { id: '123' }, query: {}, meta: {} }RouterManager.getParams()
Get route parameters
Returns: Object
// URL: /users/123
const params = RouterManager.getParams();
// { id: '123' }RouterManager.getQuery()
Get query parameters
Returns: Object
// URL: /search?q=john&page=2
const query = RouterManager.getQuery();
// { q: 'john', page: '2' }RouterManager.beforeEach(guard)
Global navigation guard
| Parameter | Type | Description |
|---|---|---|
guard |
function | Guard function |
RouterManager.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login');
} else {
next();
}
});RouterManager.afterEach(hook)
After navigation hook
RouterManager.afterEach((to, from) => {
document.title = to.meta.title || 'My App';
trackPageView(to.path);
});Events
| Event | When Triggered | Detail |
|---|---|---|
route:changed |
Route changed | {path, params, query} |
route:before |
Before navigate | {to, from} |
route:after |
After navigate | {to, from} |
route:error |
Error | {error, path} |
EventManager.on('route:changed', (data) => {
console.log('Navigated to:', data.path);
});Real-World Examples
Basic SPA Setup
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<main id="app">
<!-- Page content loads here -->
</main>
<script src="/js/now.js"></script>
<script>
Now.init({
router: {
mode: 'history',
container: '#app',
routes: [
{ path: '/', template: '/pages/home.html' },
{ path: '/about', template: '/pages/about.html' },
{ path: '/contact', template: '/pages/contact.html' }
]
}
});
</script>
</body>
</html>Protected Routes
RouterManager.init({
routes: [
{ path: '/', template: '/pages/home.html' },
{ path: '/login', template: '/pages/login.html', meta: { guest: true } },
{
path: '/dashboard',
template: '/pages/dashboard.html',
meta: { requiresAuth: true }
},
{
path: '/admin',
template: '/pages/admin.html',
meta: { requiresAuth: true, role: 'admin' }
}
]
});
RouterManager.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !AuthManager.isAuthenticated()) {
AuthManager.saveIntendedUrl(to.path);
return next('/login');
}
if (to.meta.guest && AuthManager.isAuthenticated()) {
return next('/dashboard');
}
if (to.meta.role && !AuthManager.hasRole(to.meta.role)) {
return next('/403');
}
next();
});Dynamic Page Title
RouterManager.afterEach((to) => {
const titles = {
'/': 'Home',
'/about': 'About Us',
'/contact': 'Contact'
};
document.title = `${titles[to.path] || 'Page'} | My App`;
});Route Parameters
// Route: /users/:id
// URL: /users/123
const route = RouterManager.getCurrentRoute();
console.log(route.params.id); // '123'
// In template
// <h1>User {{params.id}}</h1>SPA in a Subfolder
When the admin panel is served from a subdirectory (e.g. /nowjs-gcms/admin/) rather than the domain root, two problems arise with history-mode routing:
- Relative resource paths break when the browser is at a deep URL like
/admin/widgets/facebookand reloads —js/main.jsresolves relative to the current URL, not the admin root. currentDirdetection viawindow.location.pathnamereturns the wrong directory on a deep route refresh.
Solution: PHP entry point + <base href> + currentDir from script src
admin/index.php — generate <base href> dynamically so it never needs to be hardcoded:
<?php
// Works at any install path, no hardcoding needed.
// $_SERVER['SCRIPT_NAME'] always points to the executed PHP file
// regardless of URL rewriting, so dirname() gives the correct base.
$base = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'])), '/') . '/';
?>
<!DOCTYPE html>
<html>
<head>
<base href="<?= htmlspecialchars($base, ENT_QUOTES) ?>">
...admin/.htaccess — rewrite all non-file requests to index.php:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L,QSA]Root .htaccess — add an admin SPA rule before the public CMS catch-all, otherwise the CMS index.php intercepts deep admin routes first:
# Admin SPA routes must reach admin/index.php, not the public CMS
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^admin/(.*)$ admin/index.php [L,QSA]
# Public CMS catch-all (comes after the admin rule)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L,QSA]admin/js/main.js — detect currentDir from the script's own src attribute, not from window.location.pathname:
// ❌ Breaks on deep route refresh (e.g. /admin/widgets/facebook)
const currentDir = window.location.pathname.substring(
0, window.location.pathname.lastIndexOf('/') + 1
);
// ✅ Always correct — reads the script src attribute directly
const mainScriptEl = document.querySelector('script[src*="js/main.js"]');
let currentDir;
if (mainScriptEl) {
const scriptUrl = new URL(mainScriptEl.src);
currentDir = scriptUrl.pathname.replace(/\/js\/main\.js$/, '') + '/';
} else {
// Fallback for non-SPA or direct-load scenarios
const currentPath = window.location.pathname;
currentDir = currentPath.substring(0, currentPath.lastIndexOf('/') + 1);
}Then pass currentDir to Now.init:
await Now.init({
paths: {
templates: `${currentDir}templates`,
components: `${currentDir}components`,
translations: `${currentDir}../language`
},
router: {
base: currentDir,
...
}
});Dynamic Routes Pointing Outside the Templates Directory
By default, TemplateManager resolves template paths relative to paths.templates.Now.resolvePath() strips all ../ sequences for security, so you cannot use relative traversal to escape the templates directory.
Widget pattern — templates in a sibling directory
If each widget lives in its own directory outside admin/ (e.g. Widgets/facebook/index.html),
build the path as a full http:// URL computed once after currentDir is known.TemplateManager bypasses resolvePath entirely for absolute URLs:
// currentDir = '/nowjs-gcms/admin/'
// Strip the last path segment ("admin/") to get the project root, then append Widgets/
const widgetsDir =
window.location.origin +
currentDir.replace(/[^/]+\/$/, '') + // → '/nowjs-gcms/'
'Widgets/';
// widgetsDir = 'http://localhost/nowjs-gcms/Widgets/'Use :module as a route parameter in the template path:
routes: {
'/widgets/:module': {
template: `${widgetsDir}:module/index.html`,
// resolves to e.g. http://localhost/nowjs-gcms/Widgets/facebook/index.html
title: '{LNG_Widget}',
menuPath: '/widgets',
requireAuth: true,
beforeEnter: requireAdmin
}
}Directory layout:
nowjs-gcms/
├── admin/
│ ├── index.php ← SPA entry point with dynamic <base href>
│ ├── .htaccess ← rewrites all routes to index.php
│ ├── js/main.js
│ └── templates/ ← normal admin templates
└── Widgets/
├── facebook/
│ └── index.html ← loaded via widgetsDir
├── textlinks/
│ └── index.html
└── share/
└── index.htmlSecurity:
TemplateManager.validateRequest()enforces same-origin by default.allowedOriginsis automatically seeded withwindow.location.originon init,
so cross-domain template URLs are rejected even if thehttp://prefix is used.
Common Pitfalls
⚠️ 1. History Mode Requires Server Config
# Apache (.htaccess)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule># Nginx
location / {
try_files $uri $uri/ /index.html;
}⚠️ 2. Guard Must Call next()
// ❌ Forgot to call next()
RouterManager.beforeEach((to, from, next) => {
console.log('Guard');
// Navigation stuck!
});
// ✅ Always call next()
RouterManager.beforeEach((to, from, next) => {
console.log('Guard');
next();
});⚠️ 3. Root .htaccess catch-all intercepts SPA subroutes
If the SPA sits inside a subdirectory and the root .htaccess has a catch-all rule,
it will intercept requests like /admin/widgets/facebook before admin/.htaccess can run.
Always place a specific admin rule before the public catch-all:
# ✅ Admin rule first
RewriteRule ^admin/(.*)$ admin/index.php [L,QSA]
# Public CMS catch-all after
RewriteRule ^(.*)$ index.php [L,QSA]⚠️ 4. ../ in template paths is stripped
Now.resolvePath() removes all ../ sequences as a security measure.
Use an absolute http:// URL (computed from window.location.origin) instead:
// ❌ Stripped by resolvePath — resolves to wrong path
template: `${currentDir}../Widgets/:module/index.html`
// ✅ http:// bypasses resolvePath entirely
const widgetsDir = window.location.origin + currentDir.replace(/[^/]+\/$/, '') + 'Widgets/';
template: `${widgetsDir}:module/index.html`Related Documentation
- TemplateManager - Templates and path resolution
- AuthManager - Authentication