Now.js Framework Documentation

Now.js Framework Documentation

RouterManager

TH 22 Feb 2026 12:16

RouterManager

ภาพรวม

RouterManager คือระบบ client-side routing ใน Now.js Framework สำหรับ SPA (Single Page Application)

ใช้เมื่อ:

  • ต้องการ SPA routing
  • ต้องการ page navigation without reload
  • ต้องการ route guards
  • ต้องการ dynamic routes

ทำไมต้องใช้:

  • ✅ Hash and History mode
  • ✅ Route parameters
  • ✅ Navigation guards
  • ✅ Lazy loading
  • ✅ Auto template loading
  • ✅ Scroll restoration

การใช้งานพื้นฐาน

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' }
  ]
});
<!-- ลิงก์ปกติจะทำงานอัตโนมัติ -->
<a href="/about">About</a>
<a href="/users/123">User Profile</a>

<!-- พร้อม query string ใน href -->
<a href="/search?q=hello&page=1">Search</a>

<!-- Navigation แบบ programmatic -->
<button onclick="RouterManager.navigate('/contact')">Contact</button>

การส่ง Query Parameters ผ่านลิงก์

RouterManager รองรับ attribute data-params และ data-param-* บน <a> เพื่อส่ง query parameter ไปยังหน้าปลายทางโดยไม่ต้องเขียน JavaScript expression

data-params — ส่งต่อ params จาก URL ปัจจุบัน

อ่าน query parameter จาก URL ปัจจุบัน (window.location.search) แล้วส่งต่อไปยังปลายทาง

<!-- ส่งเฉพาะ "parent" จาก URL ปัจจุบัน -->
<!-- URL ปัจจุบัน: /menus?parent=0_MAINMENU -->
<!-- นำทางไป: /menu?parent=0_MAINMENU -->
<a href="/menu" data-params="parent">Add Menu</a>

<!-- ส่งหลาย params -->
<!-- URL ปัจจุบัน: /menus?parent=0_MAINMENU&lang=en -->
<!-- นำทางไป: /menu?parent=0_MAINMENU&lang=en -->
<a href="/menu" data-params="parent,lang">Add Menu</a>

<!-- ส่งทุก params จาก URL ปัจจุบัน -->
<a href="/menu" data-params="*">Add Menu</a>

data-param-* — เพิ่ม params ค่าคงที่

ผนวก query parameter ค่าคงที่โดยไม่ขึ้นกับ URL ปัจจุบัน

<!-- นำทางไป: /menus?parent=0_MAINMENU -->
<a href="/menus" data-param-parent="0_MAINMENU">Main Menu</a>

<!-- หลาย params คงที่ -->
<a href="/menus" data-param-parent="0_MAINMENU" data-param-lang="en">Main Menu</a>

ใช้ร่วมกัน

data-params (จาก URL) และ data-param-* (ค่าคงที่) ใช้ร่วมกันได้ โดยค่าคงที่จะมีความสำคัญกว่า

<!-- ส่ง "lang" จาก URL แต่กำหนด type=sub เสมอ -->
<a href="/menu" data-params="lang" data-param-type="sub">Add Sub-menu</a>

การตั้งค่า

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 อ้างอิง

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);   // Forward

RouterManager.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);
});

เหตุการณ์

Event เมื่อเกิด Detail
route:changed Route เปลี่ยน {path, params, query}
route:before ก่อน navigate {to, from}
route:after หลัง navigate {to, from}
route:error Error {error, path}
EventManager.on('route:changed', (data) => {
  console.log('Navigated to:', data.path);
});

ตัวอย่างการใช้งานจริง

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 ในโฟลเดอร์ย่อย

เมื่อ admin panel ถูก serve จาก subdirectory (เช่น /nowjs-gcms/admin/) แทนที่จะเป็น root ของ domain จะเกิดปัญหา 2 อย่างกับ history-mode routing:

  1. เส้นทาง resource แบบ relative พัง เมื่อ browser อยู่ที่ URL ลึก เช่น /admin/widgets/facebook แล้ว reload — js/main.js จะ resolve เส้นทางจาก URL ปัจจุบัน ไม่ใช่จาก admin root
  2. การตรวจจับ currentDir ด้วย window.location.pathname จะคืนค่า directory ผิดเมื่อ refresh จาก deep route

วิธีแก้: PHP entry point + <base href> + currentDir จาก script src

admin/index.php — สร้าง <base href> แบบ dynamic โดยไม่ต้อง hardcode:

<?php
// ทำงานกับทุก install path ไม่ต้อง hardcode
// $_SERVER['SCRIPT_NAME'] ชี้ไปที่ PHP file ที่รันอยู่เสมอ
// ไม่ขึ้นกับ URL rewriting ดังนั้น dirname() จะให้ base ที่ถูกต้องเสมอ
$base = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'])), '/') . '/';
?>
<!DOCTYPE html>
<html>
<head>
  <base href="<?= htmlspecialchars($base, ENT_QUOTES) ?>">
  ...

admin/.htaccess — rewrite ทุก request ที่ไม่ใช่ไฟล์จริงไปที่ index.php:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L,QSA]

Root .htaccess — เพิ่ม rule ของ admin ก่อน public CMS catch-all ไม่งั้น CMS index.php จะดักจับ deep admin routes ก่อน:

# Admin SPA route ต้องไปถึง admin/index.php ไม่ใช่ public CMS
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^admin/(.*)$ admin/index.php [L,QSA]

# Public CMS catch-all (มาหลัง admin rule)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L,QSA]

admin/js/main.js — ตรวจจับ currentDir จาก attribute src ของ script ไม่ใช่จาก window.location.pathname:

// ❌ พังเมื่อ refresh จาก deep route (เช่น /admin/widgets/facebook)
const currentDir = window.location.pathname.substring(
  0, window.location.pathname.lastIndexOf('/') + 1
);

// ✅ ถูกต้องเสมอ — อ่านจาก src attribute ของ script โดยตรง
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 สำหรับกรณีที่ไม่ใช่ SPA หรือโหลดโดยตรง
  const currentPath = window.location.pathname;
  currentDir = currentPath.substring(0, currentPath.lastIndexOf('/') + 1);
}

จากนั้นส่ง currentDir เข้า Now.init:

await Now.init({
  paths: {
    templates:    `${currentDir}templates`,
    components:   `${currentDir}components`,
    translations: `${currentDir}../language`
  },
  router: {
    base: currentDir,
    ...
  }
});

Dynamic Routes ที่ชี้ไปนอก Templates Directory

โดยปกติ TemplateManager จะ resolve เส้นทาง template ให้อยู่ภายใต้ paths.templates
Now.resolvePath() ตัด ../ ทั้งหมดออก เพื่อความปลอดภัย จึงไม่สามารถใช้ relative traversal หนีออกจาก templates directory ได้

รูปแบบ Widget — templates อยู่ใน sibling directory

ถ้า widget แต่ละตัวอยู่ใน directory ของตัวเองนอก admin/ (เช่น Widgets/facebook/index.html)
ให้สร้าง path เป็น http:// URL แบบเต็ม คำนวณครั้งเดียวหลังรู้ค่า currentDir
TemplateManager จะ bypass resolvePath โดยอัตโนมัติสำหรับ absolute URL:

// currentDir = '/nowjs-gcms/admin/'
// ตัด last path segment ("admin/") เพื่อได้ project root แล้วต่อ Widgets/
const widgetsDir =
  window.location.origin +
  currentDir.replace(/[^/]+\/$/, '') +   // → '/nowjs-gcms/'
  'Widgets/';
// widgetsDir = 'http://localhost/nowjs-gcms/Widgets/'

ใช้ :module เป็น route parameter ใน template path:

routes: {
  '/widgets/:module': {
    template: `${widgetsDir}:module/index.html`,
    // resolve เป็น เช่น http://localhost/nowjs-gcms/Widgets/facebook/index.html
    title: '{LNG_Widget}',
    menuPath: '/widgets',
    requireAuth: true,
    beforeEnter: requireAdmin
  }
}

โครงสร้าง directory:

nowjs-gcms/
├── admin/
│   ├── index.php        ← SPA entry point พร้อม dynamic <base href>
│   ├── .htaccess        ← rewrite ทุก route ไป index.php
│   ├── js/main.js
│   └── templates/       ← admin templates ปกติ
└── Widgets/
    ├── facebook/
    │   └── index.html   ← โหลดผ่าน widgetsDir
    ├── textlinks/
    │   └── index.html
    └── share/
        └── index.html

ความปลอดภัย: TemplateManager.validateRequest() บังคับ same-origin โดยค่าเริ่มต้น
allowedOrigins จะถูก seed ด้วย window.location.origin ตอน init อัตโนมัติ
ดังนั้น URL template ข้าม domain จะถูกปฏิเสธแม้จะใช้ prefix http:// ก็ตาม

ข้อควรระวัง

⚠️ 1. History Mode ต้อง 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 ต้องเรียก next()

// ❌ ลืมเรียก 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 ดักจับ SPA subroute

ถ้า SPA อยู่ใน subdirectory และ root .htaccess มี catch-all rule
มันจะดักจับ request เช่น /admin/widgets/facebook ก่อนที่ admin/.htaccess จะทำงาน

ให้วาง admin rule ก่อน public catch-all เสมอ:

# ✅ Admin rule ก่อน
RewriteRule ^admin/(.*)$ admin/index.php [L,QSA]

# Public CMS catch-all ทีหลัง
RewriteRule ^(.*)$ index.php [L,QSA]

⚠️ 4. ../ ใน template path ถูกตัดออก

Now.resolvePath() ลบ ../ ทั้งหมดออกเพื่อความปลอดภัย
ให้ใช้ absolute http:// URL (คำนวณจาก window.location.origin) แทน:

// ❌ ถูกตัดออกโดย resolvePath — resolve ไปเส้นทางผิด
template: `${currentDir}../Widgets/:module/index.html`

// ✅ http:// bypass resolvePath ได้ทั้งหมด
const widgetsDir = window.location.origin + currentDir.replace(/[^/]+\/$/, '') + 'Widgets/';
template: `${widgetsDir}:module/index.html`

เอกสารที่เกี่ยวข้อง