Now.js Framework Documentation

Now.js Framework Documentation

PageNavigator

TH 06 Nov 2026 02:55

PageNavigator

ภาพรวม

PageNavigator โหลดหน้าที่ server เรนเดอร์ (เช่นหน้าเว็บของ CMS) เข้า element หลัก (Now.config.mainSelector, ปกติคือ #main) โดยไม่โหลดทั้งหน้าใหม่ และเปลี่ยน URL ใน address bar ตามปกติ

  • คลิกลิงก์ภายในเว็บ → fetch URL เดิมพร้อม header X-Partial-Page: 1 → server ตอบ JSON เฉพาะส่วนที่เปลี่ยน → สลับเนื้อหาใน #main, เปลี่ยน title และ meta ของหน้า, pushState URL
  • เปิด URL ตรง ๆ (หรือ bot ของ search engine) ได้หน้าเต็มจาก server ตามปกติ — SEO ไม่เปลี่ยน
  • อะไรที่ผิดคาด (network error, ไม่ใช่ JSON, ไม่มี header ตอบกลับ, reload: true, leave guard ไม่ยอม) → โหลดหน้าเต็มแทน ลิงก์จึงไม่มีทาง "กดแล้วไม่เกิดอะไร"

ใช้เมื่อ: หน้าเว็บสร้างจาก server (PHP) และอยากได้ความเร็วแบบ SPA
ไม่ใช้เมื่อ: แอปที่ใช้ template ฝั่ง client — ใช้ RouterManager (ห้ามเปิดทั้งสองตัวพร้อมกัน เพราะดักลิงก์ชุดเดียวกัน)

การเปิดใช้

await Now.init({
  router: {enabled: false},
  navigator: {
    enabled: true,
    // path (นับจาก Now.basePath) ที่ให้โหลดแบบปกติ — string = ขึ้นต้นด้วย, หรือ RegExp
    exclude: ['admin/', 'api/']
  }
});
ตัวเลือก ค่าเริ่มต้น ความหมาย
enabled false เปิดการดักลิงก์
header 'X-Partial-Page' header ที่ส่งไปขอ partial และต้องได้กลับมาเป็น 1
exclude [] path ที่ไม่ดัก (string prefix หรือ RegExp)
scrollToTop true หน้าใหม่เริ่มที่บนสุด (ถ้า URL มี #id จะเลื่อนไปที่ element นั้น)
focusContent true ย้าย focus ไปที่ #main หลังโหลด (screen reader อ่านต่อจากเนื้อหาใหม่)
progress true แถบโหลดด้านบนของหน้าต่าง (ดูหัวข้อแถบโหลด)
transition false cross-fade ระหว่างหน้าด้วย View Transitions API (เฉพาะเบราว์เซอร์ที่รองรับ และไม่ทำเมื่อผู้ใช้ตั้ง reduced motion)

สัญญากับ server

request มี header X-Partial-Page: 1 — server ต้องตอบ JSON พร้อม header X-Partial-Page: 1 (และควรใส่ Vary: X-Partial-Page, Cache-Control: no-store เพราะ URL เดียวกันตอบได้สองแบบ)

{
  "content": "<div>…innerHTML ของ #main…</div>",
  "title": "ชื่อหน้า",
  "head": "<meta name=\"description\" content=\"…\" data-page-meta><link rel=\"canonical\" href=\"…\" data-page-meta>",
  "styles": ["https://fonts.googleapis.com/css2?family=Kanit"],
  "reload": false
}
key ความหมาย
content HTML ที่แทน innerHTML ของ #main (จำเป็น)
title document.title
head tag ใน <head> ที่ขึ้นกับหน้า — ทุก tag ต้องมี data-page-meta ตัวเก่าที่มี data-page-meta ถูกลบแล้วใส่ชุดใหม่ (หน้าเต็มก็ต้องใส่ data-page-meta ให้ tag ชุดเดียวกัน)
styles stylesheet ที่หน้านี้ต้องใช้ เพิ่มครั้งเดียว ไม่ลบออก
reload true = ตอบแบบ partial ไม่ได้ (เช่น หน้า maintenance, เปลี่ยนภาษา) → โหลดหน้าเต็ม
อื่น ๆ ส่งต่อใน event page:loaded (detail.data) เช่นเมนูที่เลือก

redirect ที่ server ทำ (เช่นไปหน้า login) fetch ตามให้ และ address bar แสดง URL ปลายทาง

ฟอร์ม

ฟอร์ม method="get" (เช่นช่องค้นหา) โหลดหน้าผลลัพธ์แบบเดียวกับลิงก์ — query ของ action ถูกแทนด้วยค่าในฟอร์ม (รวมปุ่มที่กด) ตามกติกาของ GET
ฟอร์ม POST, ฟอร์มที่ FormManager จัดการ (data-form), ฟอร์มที่มี target และฟอร์มใต้ data-navigate="false" ไม่ถูกแตะ

แถบโหลด

ระหว่างโหลดมีแถบเส้นบางที่ขอบบนของหน้าต่าง ทำงานสองจังหวะ

  1. เริ่มโหลด → วิ่งเร็วไปถึง 80% แล้วรอ
  2. หน้าใหม่เข้ามาแล้ว → เติมจากตำแหน่งปัจจุบันไป 100% แล้วจางหาย

หน้าส่วนใหญ่มาเร็ว แถบจึงวิ่งแค่ช่วงสั้น ๆ แล้วเต็มทันที ปรับได้ด้วยตัวแปร CSS (ใส่ใน CSS ของธีม):

:root {
  --page-progress-color: var(--color-primary); /* สี */
  --page-progress-height: 3px;                 /* ความหนา */
  --page-progress-at: 0.8;                     /* จุดรอของจังหวะแรก (0–1) */
  --page-progress-start: 0.5s;                 /* เวลาของจังหวะแรก */
  --page-progress-end: 0.2s;                   /* เวลาเติมเต็มและเวลาจางหาย */
}

ถ้าสีของแถบกลืนกับพื้นที่ขอบบนของหน้าต่าง (เช่น header เป็นสีหลักเดียวกัน — contrast ต่ำกว่า 2) แถบจะเปลี่ยนเป็นสีขาวหรือสีเข้มเองตามพื้น

element คือ <div class="page-progress"> (class is-loading = จังหวะแรก, is-done = จังหวะที่สอง) ถ้าลิงก์กลายเป็นการโหลดหน้าเต็ม แถบค้างที่ 80% จนหน้าเปลี่ยน — ถ้าหน้าไม่เปลี่ยน (เป็นไฟล์ดาวน์โหลด) สถานะโหลดจบเองภายใน 3 วินาที ปิดแถบได้ด้วย progress: false

ลิงก์ที่ไม่ถูกดัก

  • มี data-navigate="false" ที่ลิงก์หรือ element แม่
  • มี target (ที่ไม่ใช่ _self), download, rel="external"
  • กด Ctrl/Cmd/Shift/Alt หรือคลิกปุ่มอื่นที่ไม่ใช่ปุ่มซ้าย
  • คนละ origin, อยู่นอก Now.basePath, mailto:/tel:
  • ไฟล์ (นามสกุลอื่นนอกจาก .php, .html, .htm) เช่น .pdf, .zip
  • ลิงก์ #id ในหน้าเดียวกัน
  • path ที่ตรงกับ exclude
PageNavigator.isNavigable('/news/123');      // true
PageNavigator.isNavigable('/files/a.pdf');   // false

script ในเนื้อหาใหม่

innerHTML ไม่รัน <script> — PageNavigator รันให้ตามลำดับหลังสลับเนื้อหา

  • inline script รันทุกครั้ง
  • script ที่มี src ซึ่งโหลดอยู่แล้วในหน้า ไม่โหลดซ้ำ
  • listener ของ DOMContentLoaded / load ที่ script ใหม่ลงทะเบียน ถูกเรียกหลัง script ทั้งหมดรันเสร็จ (event สองตัวนี้จะไม่เกิดอีกในเอกสารเดิม) — template เก่าที่เขียน document.addEventListener('DOMContentLoaded', …) จึงทำงานได้โดยไม่ต้องแก้

component, element และ form ใน #main ถูกทำลาย/สร้างใหม่ด้วยขั้นตอนเดียวกับ RouterManager.render()

ค่าตั้งของไซต์ที่ AppConfigManager ผูกกับหน้าตอนโหลดเต็มหน้า (data-text="data.web_title", data-attr="data-username:data.google_client_id" ของปุ่มเข้าระบบด้วยบัญชีโซเชียล, data-if …) ถูกผูกกับเนื้อหาใหม่ใน #main ด้วย ผ่าน AppConfigManager.applyBindings() ส่วน markup นอก #main คงเดิม

Event

event ที่มา ข้อมูล
route:changed EventManager {path, params: {}, query, hash, route: null, previous, isNotFoundPage, scroll: false} — event เดียวกับ router, component ที่รีเฟรชตาม router (MenuManager, EventCalendar …) ทำงานต่อได้เลย
page:loaded EventManager และ document (CustomEvent) {url, previous, status, data} — data คือ JSON ทั้งก้อน
content:rendered EventManager จาก RouterManager.render()
// script ธรรมดาที่ไม่ได้ใช้ EventManager
document.addEventListener('page:loaded', e => {
  console.log('now showing', e.detail.url);
});

Leave guard

const off = PageNavigator.beforeLeave(url => {
  // คืน false = ไม่สลับเนื้อหา ให้โหลดหน้าเต็มแทน → beforeunload ของหน้าทำงานตามปกติ
  return !editor.hasUnsavedChanges();
});
off(); // ยกเลิก

ใช้กับทั้งคลิกลิงก์และปุ่ม Back/Forward

Back / Forward และตำแหน่ง scroll

  • ตำแหน่ง scroll ถูกเก็บใน history.state.scrollY และคืนให้เมื่อกด Back/Forward
  • history.scrollRestoration ถูกตั้งเป็น manual (เนื้อหามาหลัง fetch) — reload หน้าก็คืนตำแหน่งให้
  • PageNavigator จัดตำแหน่งเอง: route:changed ที่ส่งออกมี scroll: false เพื่อให้ ScrollManager ไม่เลื่อนไปที่ #main ซ้ำ
  • ระหว่างโหลด body มี class loading และ #main มี aria-busy="true" (หน้าตาให้ธีมกำหนดเอง)

API

method คำอธิบาย
init(options) เริ่มทำงาน (Now.init เรียกให้จาก navigator config)
navigate(url, {fromHistory, scrollY}) โหลดหน้าแบบ partial — คืน true เมื่อสลับเนื้อหาสำเร็จ
isNavigable(href) URL นี้โหลดแบบ partial ได้หรือไม่
beforeLeave(guard) ลงทะเบียน leave guard คืนฟังก์ชันยกเลิก