Now.js Framework Documentation

Now.js Framework Documentation

ComponentManager

TH 15 Dec 2025 08:52

ComponentManager

ภาพรวม

ComponentManager คือระบบจัดการ Component ใน Now.js Framework ที่ช่วยให้คุณสร้าง reusable components พร้อม lifecycle management, state management และ event handling

ใช้เมื่อ:

  • ต้องการสร้าง reusable UI components
  • ต้องการ component lifecycle (created, mounted, destroyed)
  • ต้องการ state management ภายใน component
  • ต้องการ event handling แบบ declarative

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

  • ✅ Component lifecycle hooks (8 hooks)
  • ✅ Reactive state สำหรับ auto-update
  • ✅ Template processing พร้อม data binding
  • ✅ Event delegation และ custom events
  • ✅ Props extraction จาก attributes
  • ✅ Virtual DOM patching

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

การสร้าง Component

// js/components/counter.js
ComponentManager.define('counter', {
  // Template HTML
  template: `
    <div class="counter">
      <span class="count">{{ count }}</span>
      <button data-ref="increment">+</button>
      <button data-ref="decrement">-</button>
    </div>
  `,

  // Initial state
  state: {
    count: 0
  },

  // Methods
  methods: {
    increment() {
      this.state.count++;
      this.render();
    },
    decrement() {
      this.state.count--;
      this.render();
    }
  },

  // Event handlers
  events: {
    'click [data-ref="increment"]': function(e) {
      this.methods.increment();
    },
    'click [data-ref="decrement"]': function(e) {
      this.methods.decrement();
    }
  },

  // Lifecycle hooks
  mounted() {
    console.log('Counter mounted!', this.element);
  }
});

การใช้งานใน HTML

<!-- Basic usage -->
<div data-component="counter"></div>

<!-- With props -->
<div data-component="counter" data-count="10"></div>

<!-- With custom template -->
<div data-component="counter">
  <div class="custom-counter">
    <span>{{ count }}</span>
    <button data-ref="increment">เพิ่ม</button>
  </div>
</div>

Lifecycle Hooks

ComponentManager มี 8 lifecycle hooks ตามลำดับการทำงาน:

graph TD
    A[define] --> B[beforeCreate]
    B --> C[created]
    C --> D[beforeMount]
    D --> E[processTemplate]
    E --> F[mounted]
    F --> G[State Change]
    G --> H[beforeUpdate]
    H --> I[updated]
    G --> J[Component Removal]
    J --> K[beforeDestroy]
    K --> L[destroyed]

1. beforeCreate

ถูกเรียกก่อนสร้าง instance (ยังไม่มี state)

ComponentManager.define('example', {
  beforeCreate() {
    console.log('Before create - this.state:', this.state); // {}
  }
});

2. created

ถูกเรียกหลัง instance ถูกสร้าง (มี state แล้ว)

ComponentManager.define('example', {
  state: { count: 0 },
  created() {
    console.log('Created - count:', this.state.count); // 0
    // ดี: fetch initial data
    this.fetchData();
  },
  methods: {
    async fetchData() {
      const data = await fetch('/api/data').then(r => r.json());
      this.state.data = data;
    }
  }
});

3. beforeMount

ถูกเรียกก่อน template ถูก render ลง DOM

ComponentManager.define('example', {
  beforeMount() {
    // ปรับ state ก่อน render
    this.state.timestamp = Date.now();
  }
});

4. mounted

ถูกเรียกหลัง component ใส่ลง DOM แล้ว

ComponentManager.define('chart', {
  template: '<canvas id="chart"></canvas>',
  mounted() {
    // ดี: ทำงานกับ DOM elements
    const canvas = this.element.querySelector('#chart');
    this.chart = new Chart(canvas, this.state.chartConfig);
  }
});

5. beforeUpdate

ถูกเรียกก่อน re-render

ComponentManager.define('example', {
  beforeUpdate() {
    console.log('กำลังจะ update...');
    this.state.lastUpdate = Date.now();
  }
});

6. updated

ถูกเรียกหลัง re-render เสร็จ

ComponentManager.define('example', {
  updated() {
    console.log('Update เสร็จแล้ว');
    // ดี: scroll to new content
    this.element.scrollIntoView({ behavior: 'smooth' });
  }
});

7. beforeDestroy

ถูกเรียกก่อน component ถูกลบ

ComponentManager.define('timer', {
  state: { timer: null },
  mounted() {
    this.state.timer = setInterval(() => {
      this.state.count++;
      this.render();
    }, 1000);
  },
  beforeDestroy() {
    // ดี: cleanup resources
    clearInterval(this.state.timer);
  }
});

8. destroyed

ถูกเรียกหลัง component ถูกลบออกจาก DOM

ComponentManager.define('example', {
  destroyed() {
    console.log('Component destroyed');
    // Notify parent or analytics
    EventManager.emit('component:destroyed', this.id);
  }
});

Component Definition Options

ตัวเลือกทั้งหมด

ComponentManager.define('complete-example', {
  // Template
  template: '<div class="example">{{ title }}</div>',

  // Initial state
  state: {
    title: 'Hello',
    items: []
  },

  // Props validation (optional)
  props: {
    title: { type: String, default: 'Default Title' },
    count: { type: Number, default: 0 }
  },

  // Methods
  methods: {
    doSomething() {
      this.state.title = 'Changed';
      this.render();
    }
  },

  // Computed properties
  computed: {
    fullTitle() {
      return `${this.state.title} - App`;
    }
  },

  // Watch state changes
  watch: {
    'state.count': function(newVal, oldVal) {
      console.log(`count changed: ${oldVal} -> ${newVal}`);
    }
  },

  // Reactive mode (auto re-render on state change)
  reactive: true,

  // Render strategy: 'auto', 'manual', 'batch'
  renderStrategy: 'auto',

  // Event handlers
  events: {
    'click button': function(e) { },
    'submit form': function(e) { },
    'custom:event': function(data) { }
  },

  // ARIA attributes
  aria: {
    role: 'region',
    label: 'Example component'
  },

  // Custom element validation
  validElement: (element) => element.tagName === 'DIV',

  // Custom element setup
  setupElement: (element, state) => {
    element.classList.add('initialized');
  },

  // Error boundary
  errorBoundary: true,
  errorCaptured: (error) => {
    console.error('Component error:', error);
    return false; // Don't propagate
  },

  // Lifecycle hooks
  beforeCreate() { },
  created() { },
  beforeMount() { },
  mounted() { },
  beforeUpdate() { },
  updated() { },
  beforeDestroy() { },
  destroyed() { }
});

State และ Reactivity

Basic State

ComponentManager.define('user-card', {
  state: {
    name: 'Guest',
    email: '',
    isActive: false
  },
  methods: {
    updateName(newName) {
      this.state.name = newName;
      this.render(); // Manual re-render
    }
  }
});

Reactive State

เมื่อเปิด reactive: true state จะ auto-trigger re-render:

ComponentManager.define('reactive-counter', {
  reactive: true, // Enable reactivity
  state: {
    count: 0
  },
  events: {
    'click [data-action="increment"]': function() {
      this.state.count++; // Auto re-renders!
    }
  }
});

Watch

ติดตามการเปลี่ยนแปลง state:

ComponentManager.define('watcher-example', {
  reactive: true,
  state: {
    query: '',
    results: []
  },
  watch: {
    'state.query': async function(newVal, oldVal) {
      if (newVal.length >= 3) {
        this.state.results = await this.methods.search(newVal);
      }
    }
  },
  methods: {
    async search(query) {
      const response = await fetch(`/api/search?q=${query}`);
      return response.json();
    }
  }
});

Event Handling

Event Syntax

events: {
  // Basic: 'eventType selector'
  'click button': function(e) { },

  // Multiple events
  'mouseenter .card': function(e) { },
  'mouseleave .card': function(e) { },

  // Form events
  'submit form': function(e) {
    e.preventDefault();
  },
  'input [name="search"]': function(e) {
    this.state.query = e.target.value;
  },

  // Custom events (via EventManager)
  'user:login': function(userData) { },
  'cart:updated': function(items) { }
}

Event Delegation

Event ถูกผูกกับ root element และใช้ delegation:

ComponentManager.define('todo-list', {
  template: `
    <ul class="todos">
      <li data-for="item in items">
        <template>
          <span data-text="item.title"></span>
          <button data-action="delete" data-id="{{ item.id }}">ลบ</button>
        </template>
      </li>
    </ul>
  `,
  events: {
    // Handles click on any [data-action="delete"] inside component
    'click [data-action="delete"]': function(e) {
      const id = e.target.dataset.id;
      this.methods.deleteItem(id);
    }
  }
});

Props

การรับ Props

Props ถูก extract จาก data-* attributes:

<div data-component="user-card"
     data-user-id="123"
     data-name="John Doe"
     data-count="5"
     data-active="true">
</div>
ComponentManager.define('user-card', {
  created() {
    console.log(this.props);
    // { userId: "123", name: "John Doe", count: 5, active: true }
  }
});

Auto Type Conversion

  • Numbers: "123"123
  • Booleans: "true"true, "false"false
  • Arrays: "[1,2,3]"[1, 2, 3]

Refs

เข้าถึง DOM elements ผ่าน refs:

<div data-component="form-example">
  <input data-ref="nameInput" type="text">
  <button data-ref="submitBtn">Submit</button>
</div>
ComponentManager.define('form-example', {
  mounted() {
    // Access via refs proxy
    this.refs.nameInput.focus();
    this.refs.submitBtn.disabled = true;
  },
  methods: {
    validate() {
      const value = this.refs.nameInput.value;
      this.refs.submitBtn.disabled = value.length < 3;
    }
  }
});

API อ้างอิง

ComponentManager.define(name, definition)

ลงทะเบียน component definition

Parameter Type Description
name string ชื่อ component (ต้องไม่ซ้ำ)
definition object Component definition object

Returns: Object - Processed definition

const definition = ComponentManager.define('my-component', {
  template: '<div>Hello</div>',
  state: { count: 0 }
});

ComponentManager.mount(element, name, props)

Mount component ลง element

Parameter Type Description
element HTMLElement Target element
name string ชื่อ component
props object Initial props

Returns: Promise<Object> - Component instance

const element = document.querySelector('#container');
const instance = await ComponentManager.mount(element, 'counter', { count: 10 });

ComponentManager.destroy(element)

ลบ component ออกจาก element

Parameter Type Description
element HTMLElement Element ที่มี component
await ComponentManager.destroy(document.querySelector('#counter'));

ComponentManager.has(name)

ตรวจสอบว่ามี component ลงทะเบียนไว้หรือไม่

Parameter Type Description
name string ชื่อ component

Returns: boolean

if (ComponentManager.has('counter')) {
  console.log('Counter component is registered');
}

ComponentManager.get(name)

รับ component definition

Parameter Type Description
name string ชื่อ component

Returns: Object|undefined - Component definition

const definition = ComponentManager.get('counter');
console.log(definition.state); // { count: 0 }

ComponentManager.cleanup(container)

ลบ components ทั้งหมดใน container

Parameter Type Description
container HTMLElement Container element
await ComponentManager.cleanup(document.querySelector('#app'));

ComponentManager.forceUpdate(instance)

บังคับ re-render component

Parameter Type Description
instance Object Component instance
ComponentManager.forceUpdate(instance);

ตัวอย่างขั้นสูง

Component ที่โหลดข้อมูลจาก API

ComponentManager.define('user-list', {
  template: `
    <div class="user-list">
      <div data-if="loading" class="loading">กำลังโหลด...</div>
      <div data-if="error" class="error" data-text="error"></div>
      <ul data-if="!loading && !error" data-for="user in users">
        <template>
          <li>
            <img data-attr="src: user.avatar" alt="">
            <span data-text="user.name"></span>
          </li>
        </template>
      </ul>
    </div>
  `,
  state: {
    users: [],
    loading: false,
    error: null
  },
  async created() {
    await this.methods.fetchUsers();
  },
  methods: {
    async fetchUsers() {
      this.state.loading = true;
      this.state.error = null;

      try {
        const response = await fetch('/api/users');
        if (!response.ok) throw new Error('Failed to fetch');
        this.state.users = await response.json();
      } catch (error) {
        this.state.error = error.message;
      } finally {
        this.state.loading = false;
        this.render();
      }
    }
  }
});

Nested Components

<div data-component="dashboard">
  <div data-component="stats-card" data-title="Users" data-value="150"></div>
  <div data-component="stats-card" data-title="Sales" data-value="$12,500"></div>
  <div data-component="chart" data-type="line"></div>
</div>
ComponentManager.define('stats-card', {
  template: `
    <div class="stats-card">
      <h3 data-text="title"></h3>
      <div class="value" data-text="value"></div>
    </div>
  `,
  created() {
    this.state.title = this.props.title;
    this.state.value = this.props.value;
  }
});

ข้อควรระวัง

⚠️ 1. ชื่อ Component ต้องไม่ซ้ำ

// ❌ จะ warning และ overwrite
ComponentManager.define('counter', { /* ... */ }); // file1.js
ComponentManager.define('counter', { /* ... */ }); // file2.js

// ✅ ใช้ชื่อที่ unique
ComponentManager.define('page-counter', { /* ... */ });
ComponentManager.define('cart-counter', { /* ... */ });

⚠️ 2. อย่าลืม cleanup resources

// ❌ Memory leak
ComponentManager.define('timer', {
  mounted() {
    setInterval(() => { /* ... */ }, 1000);
  }
});

// ✅ Cleanup properly
ComponentManager.define('timer', {
  mounted() {
    this.state.timer = setInterval(() => { /* ... */ }, 1000);
  },
  beforeDestroy() {
    clearInterval(this.state.timer);
  }
});

⚠️ 3. ใช้ this.render() สำหรับ manual update

// ❌ ไม่มีผล - DOM ไม่ update
ComponentManager.define('counter', {
  methods: {
    increment() {
      this.state.count++;
      // missing render()
    }
  }
});

// ✅ เรียก render() หลังเปลี่ยน state
ComponentManager.define('counter', {
  methods: {
    increment() {
      this.state.count++;
      this.render(); // Update DOM
    }
  }
});

// ✅ หรือใช้ reactive: true
ComponentManager.define('counter', {
  reactive: true, // Auto renders on state change
  methods: {
    increment() {
      this.state.count++; // Auto renders!
    }
  }
});

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