Now.js Framework Documentation

Now.js Framework Documentation

TemplateManager

TH 09 May 2026 02:50

TemplateManager

ภาพรวม

TemplateManager คือระบบ template และ data binding ใน Now.js Framework ประกอบด้วย:

  • Template Loading - โหลด HTML template แบบ dynamic ผ่าน AJAX
  • Data Binding - ผูกข้อมูลกับ DOM elements แบบ reactive
  • JavaScript Initialization - เริ่มต้น JavaScript อัตโนมัติเมื่อ template โหลด
  • Component Lifecycle - จัดการ cleanup เมื่อ template ถูกลบ

ใช้เมื่อ:

  • ต้องการแยก HTML templates ออกจากหน้าหลัก
  • ต้องการ reactive data binding ใน templates
  • ต้องการ initialize JavaScript เมื่อ template โหลด
  • ต้องการโครงสร้างโค้ดที่สะอาดและดูแลง่าย

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

  • ✅ ชุด data directives ที่ครอบคลุมทั้ง binding, lifecycle hooks และ declarative animations
  • ✅ โหลด templates แบบ dynamic ผ่าน AJAX
  • ✅ Auto-initialize JavaScript functions
  • ✅ รองรับ cleanup เมื่อ templates ถูกลบ
  • ✅ มีความปลอดภัยในตัว (sanitization, validation)

Template Directives

TemplateManager รองรับ directive มากกว่าชุด core เดิม 12 ตัว นอกจาก text, attribute, loop และ event bindings แล้วยังมี option binding, complex data rebinding, การแสดงไฟล์เดิม, lifecycle hooks และ declarative animations ด้วย

Directive คำอธิบาย เอกสาร
data-text ผูกเนื้อหาข้อความพร้อม formatters data-text.md
data-html ผูก HTML content พร้อม sanitization data-html.md
data-attr ผูก attributes (href, src ฯลฯ) data-attr.md
data-class ผูก CSS class (3 โหมด) data-class.md
data-style ผูก inline style data-style.md
data-if แสดงผลแบบมีเงื่อนไข data-if.md
data-for แสดงผลรายการ data-for.md
data-on จัดการ event (20+ modifiers) data-on.md
data-model Two-way binding ฟอร์ม data-model.md
data-checked ผูก checkbox/radio data-checked.md
data-options-key เติม options ให้ select/tag/autocomplete จาก context.options data-options-key.md
data-bind ผูกข้อมูลเชิงโครงสร้างให้ TableManager / LineItemsManager data-bind.md
data-files แสดงไฟล์เดิมบน file input data-files.md
data-container โหลด component แบบ dynamic data-container.md
data-script เริ่มต้น JavaScript data-script.md
data-on-load รัน logic หลัง block ถูก render พร้อมแล้ว data-on-load.md
data-show, data-enter, data-leave, data-transition animation directives แบบ declarative ที่ขับเคลื่อนด้วย AnimationManager data-animation.md

ดู ดัชนี Template Directives สำหรับการอ้างอิงด่วน

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

Template กับ Data Binding

<!-- profile.html -->
<main class="content" data-script="initProfile">
  <h1 data-text="user.name"></h1>
  <p data-text="user.email"></p>

  <ul data-for="skill in user.skills">
    <template>
      <li data-text="skill"></li>
    </template>
  </ul>

  <form data-form="editprofile">
    <input type="text" name="name" data-model="user.name">
    <button type="submit">บันทึก</button>
  </form>
</main>

Initialization Script

// main.js - Global function
function initProfile(element, data) {
  console.log('โหลด Profile แล้ว!', element);
  console.log('ข้อมูลจาก API:', data);

  // Custom initialization
  const input = element.querySelector('input[name="name"]');
  input.addEventListener('focus', () => {
    console.log('ฟิลด์ชื่อถูก focus');
  });

  // Return cleanup function (ไม่บังคับ)
  return () => {
    console.log('Cleanup Profile');
  };
}

การทำงาน:

  1. RouterManager โหลด profile.html
  2. TemplateManager ประมวลผล data directives ทั้งหมด
  3. ตรวจพบ data-script="initProfile"
  4. เรียก window.initProfile(element, data)
  5. เมื่อ navigate ออก เรียก cleanup function

ฟีเจอร์หลัก

1. Reactive Data Binding

การเปลี่ยนแปลงข้อมูลอัพเดท DOM อัตโนมัติ:

<span data-text="count"></span>
<button data-on="click:increment">+</button>
let count = 0;
function increment() {
  count++;
  // DOM อัพเดทอัตโนมัติ
}

2. Template Expressions

รองรับ expressions ใน bindings:

<!-- Text กับ formatters -->
<span data-text="price | currency:THB"></span>

<!-- Computed class -->
<div data-class="count > 0 ? 'has-items' : 'empty'"></div>

<!-- Conditional rendering -->
<div data-if="user.isAdmin && permissions.edit">
  ส่วนควบคุมแอดมิน
</div>

arguments ของ formatter ใช้รูปแบบ string ที่คั่นด้วย : ตรงๆ เช่นควรใช้ currency:THB:th-TH:4 แทนรูปแบบที่มี quote ครอบ

3. Event Handling

Event binding แบบ declarative พร้อม modifiers:

<!-- Basic click -->
<button data-on="click:save">บันทึก</button>

<!-- กับ modifiers -->
<form data-on="submit.prevent:handleSubmit">
<input data-on="keydown.enter:search">
<div data-on="click.self:close">

4. Auto-Initialization

Templates ที่มี data-script จะถูก initialize อัตโนมัติ:

<div class="dashboard" data-script="initDashboard">
  <div id="chart"></div>
</div>
function initDashboard(element, data) {
  const chart = new Chart(element.querySelector('#chart'), {
    type: 'bar',
    data: data.chartData
  });

  return () => chart.destroy();
}

5. Cleanup Functions

Return function เพื่อ cleanup เมื่อ template ถูกลบ:

function initTimer(element, data) {
  const timer = setInterval(() => {
    element.querySelector('#time').textContent = new Date().toLocaleTimeString();
  }, 1000);

  // ถูกเรียกเมื่อ navigate ออก
  return () => clearInterval(timer);
}

การใช้งานขั้นสูง

รับข้อมูลจาก API

เมื่อใช้กับ RouterManager ที่มี data-load-api:

// Router configuration
routes: {
  '/dashboard': {
    template: 'dashboard.html',
    title: 'Dashboard',
    'data-load-api': 'api/dashboard/stats'
  }
}
function initDashboard(element, data) {
  // data มี API response
  console.log(data); // { totalUsers: 150, activeUsers: 89, ... }
  renderStats(data);
}

หลาย Scripts

Templates สามารถมีหลาย data-script attributes:

<div class="page">
  <main data-script="initMainContent">...</main>
  <aside data-script="initSidebar">...</aside>
  <footer data-script="initFooter">...</footer>
</div>

Functions ถูกเรียกตามลำดับ DOM (บนลงล่าง)

Nested Templates

Templates สามารถมี templates อื่นๆ ได้:

<div data-container="'components/' + currentComponent + '.html'"></div>

ดู data-container.md สำหรับรายละเอียด

ข้อควรระวัง

⚠️ 1. Function ต้องเป็น Global

// ❌ ไม่ทำงาน - Function อยู่ใน module scope
document.addEventListener('DOMContentLoaded', () => {
  function initProfile(element, data) { }
});

// ✅ ถูก - Global function
function initProfile(element, data) { }

// ✅ ถูก - กำหนด global ชัดเจน
window.initProfile = function(element, data) { };

⚠️ 2. Memory Leaks

// ❌ ไม่ดี - ไม่มี cleanup
function initTimer(element, data) {
  setInterval(() => { /* ทำงานตลอดไป */ }, 1000);
}

// ✅ ดี - มี cleanup
function initTimer(element, data) {
  const timer = setInterval(() => { /* ... */ }, 1000);
  return () => clearInterval(timer);
}

⚠️ 3. ตรวจสอบ Element ก่อน

// ❌ อาจ error
function initForm(element, data) {
  element.querySelector('#input').value = data.value;
}

// ✅ ตรวจสอบก่อน
function initForm(element, data) {
  const input = element.querySelector('#input');
  if (input) input.value = data.value || '';
}

⚠️ 4. Template path ที่อยู่นอก paths.templates — ใช้ ../ ไม่ได้

Now.resolvePath() ตัด ../ ทั้งหมดออก เพื่อความปลอดภัย
ไม่สามารถออกจาก paths.templates ด้วย relative traversal ได้

// ❌ ../  ถูกตัดออก → resolve ไปเส้นทางผิด
template: `${currentDir}../Widgets/facebook/index.html`
// ผลลัพธ์: /nowjs-gcms/admin/templates/Widgets/facebook/index.html  ← ผิด

ให้ใช้ http:// URL แบบเต็ม แทน — TemplateManager จะตรวจพบและข้าม resolvePath ทั้งหมด:

// ✅ คำนวณ widgetsDir ครั้งเดียวใน main.js
const widgetsDir =
  window.location.origin +
  currentDir.replace(/[^/]+\/$/, '') +  // ตัด "admin/" → project root
  'Widgets/';
// widgetsDir = 'http://localhost/nowjs-gcms/Widgets/'

// จากนั้นใช้ใน route definition
routes: {
  '/widgets/:module': {
    template: `${widgetsDir}:module/index.html`,
    // fetch: http://localhost/nowjs-gcms/Widgets/facebook/index.html ✅
  }
}

ความปลอดภัยยังคงอยู่: validateRequest() ตรวจสอบ allowedOrigins (default = window.location.origin)
ดังนั้น URL template ข้าม domain จะถูกบล็อกเสมอ

⚠️ 5. isValidPath ปฏิเสธ absolute URL (พฤติกรรมเดิม)

ก่อนการแก้ไข isValidPath จะคืนค่า false สำหรับ path ที่ไม่ขึ้นต้นด้วย /
ดังนั้น http:// template URL จะ throw "Invalid template path format" แม้ว่าจะแก้ bug else if prepend แล้วก็ตาม

ปัจจุบัน isValidPath รองรับ absolute URL ผ่าน branch เฉพาะที่เรียก isValidUrl() และตรวจสอบ extension ของไฟล์:

// ภายใน (TemplateManager.isValidPath):
if (/^https?:\/\//i.test(path)) {
  return this.isValidUrl(path) &&
    this.config.security.allowedExtensions.some(ext => path.toLowerCase().endsWith(ext));
}

ไม่ต้องทำอะไรเพิ่มเติมจาก application code — จัดการให้อัตโนมัติแล้ว

แนวปฏิบัติที่ดี

1. ชื่อ Function ที่ชัดเจน

// ✅ ดี
function initUserProfileForm(element, data) { }
function initSalesChart(element, data) { }
function initProductGallery(element, data) { }

2. แยก Helper Functions

function initDashboard(element, data) {
  const cleanup1 = initStats(element, data.stats);
  const cleanup2 = initChart(element, data.chart);

  return () => {
    cleanup1?.();
    cleanup2?.();
  };
}

3. Event Delegation

function initList(element, data) {
  const list = element.querySelector('#list');

  const handleClick = (e) => {
    if (e.target.matches('.btn-delete')) {
      handleDelete(e.target.dataset.id);
    }
  };

  list.addEventListener('click', handleClick);
  return () => list.removeEventListener('click', handleClick);
}

4. Error Handling

function initDataTable(element, data) {
  try {
    const table = element.querySelector('#table');
    if (!table) throw new Error('ไม่พบ Table');

    // Initialize...
  } catch (error) {
    console.error('Init ล้มเหลว:', error);
    element.innerHTML = '<p class="error">โหลดล้มเหลว</p>';
  }
}

การผสานระบบ

กับ FormManager

<form data-form="editprofile" data-script="initProfileForm" data-load-api="api/users/get">
  <!-- FormManager จัดการฟอร์ม, data-script เพิ่ม logic เอง -->
</form>

กับ ComponentManager

<div data-component="sidebar" data-script="initSidebar"></div>

กับ ApiComponent

<div data-api-component="userList" data-script="initUserList" data-api-url="api/users/list">
</div>

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

เอกสาร Directive

ระบบอื่นๆ