Now.js Framework Documentation

Now.js Framework Documentation

สร้าง Custom Elements

TH 15 Dec 2025 08:53

สร้าง Custom Elements

คู่มือสำหรับการสร้าง Element Factory classes กำหนดเองเพื่อขยายความสามารถของ Now.js form handling

📋 สารบัญ

  1. ภาพรวม
  2. เมื่อไหร่ควรสร้าง Custom Elements
  3. คู่มือแบบทีละขั้นตอน
  4. Configuration
  5. Property Handlers
  6. Validation
  7. Registration
  8. ตัวอย่างเต็ม
  9. แนวทางปฏิบัติที่ดี

ภาพรวม

Form elements ทั้งหมดใน Now.js ขยายจาก ElementFactory base class การสร้าง custom element ประกอบด้วย:

  1. ขยาย ElementFactory (หรือ factory เฉพาะทาง เช่น TextElementFactory)
  2. กำหนด configuration defaults
  3. Implement setupElement() สำหรับ custom behavior
  4. เพิ่ม property handlers และ validators (optional)
  5. ลงทะเบียนกับ ElementManager

Class Hierarchy

ElementFactory (base)
├── TextElementFactory    # ขยายสำหรับ text-based inputs
├── NumberElementFactory  # ขยายสำหรับ numeric inputs
├── SelectElementFactory  # ขยายสำหรับ dropdown inputs
└── YourCustomFactory     # Custom element ของคุณ

เมื่อไหร่ควรสร้าง Custom Elements

✅ สร้าง Custom Element เมื่อ:

  • ต้องการ reusable specialized input behavior
  • Standard elements ไม่รองรับ data format ของคุณ
  • ต้องการ custom validation logic
  • ต้องการ UI/UX ที่สม่ำเสมอสำหรับ input types เฉพาะ

❌ อย่าสร้าง Custom Element เมื่อ:

  • Standard validation rules เพียงพอ (ใช้ data-validate)
  • ต้องการ customization แบบครั้งเดียว (ใช้ JavaScript)
  • Behavior สามารถทำได้ด้วย configuration

คู่มือแบบทีละขั้นตอน

ขั้นตอน 1: สร้าง Factory Class

class RatingElementFactory extends ElementFactory {
  // Configuration จะใส่ที่นี่
}

ขั้นตอน 2: กำหนด Configuration

class RatingElementFactory extends ElementFactory {
  static config = {
    ...ElementFactory.config,  // สืบทอด base config
    type: 'rating',
    min: 1,
    max: 5,
    step: 1,
    iconFilled: '★',
    iconEmpty: '☆',
    validationMessages: {
      required: 'กรุณาเลือกคะแนน',
      min: 'คะแนนต้องอย่างน้อย {min}',
      max: 'คะแนนต้องไม่เกิน {max}'
    }
  };
}

ขั้นตอน 3: Implement setupElement

class RatingElementFactory extends ElementFactory {
  // ... code ก่อนหน้า

  static setupElement(instance) {
    const { element, config } = instance;

    // ซ่อน input ต้นฉบับ
    element.type = 'hidden';

    // สร้าง rating UI
    const wrapper = document.createElement('div');
    wrapper.className = 'rating-wrapper';
    element.parentNode.insertBefore(wrapper, element);

    // สร้างดาว
    for (let i = config.min; i <= config.max; i += config.step) {
      const star = document.createElement('span');
      star.className = 'rating-star';
      star.dataset.value = i;
      star.textContent = config.iconEmpty;
      star.addEventListener('click', () => {
        this.setValue(instance, i);
      });
      wrapper.appendChild(star);
    }

    instance.wrapper = wrapper;
    instance.stars = wrapper.querySelectorAll('.rating-star');

    // Initialize display
    this.updateDisplay(instance);

    return instance;
  }

  static updateDisplay(instance) {
    const { element, config, stars } = instance;
    const value = parseFloat(element.value) || 0;

    stars.forEach(star => {
      const starValue = parseFloat(star.dataset.value);
      star.textContent = starValue <= value
        ? config.iconFilled
        : config.iconEmpty;
    });
  }

  static setValue(instance, value) {
    instance.element.value = value;
    this.updateDisplay(instance);
    instance.element.dispatchEvent(new Event('change', { bubbles: true }));
  }
}

ขั้นตอน 4: ลงทะเบียนกับ ElementManager

// ลงทะเบียน element type
ElementManager.registerElement('rating', RatingElementFactory);

// Expose globally (optional)
window.RatingElementFactory = RatingElementFactory;

การตั้งค่า

Static Config Object

static config = {
  // สืบทอดจาก parent
  ...ElementFactory.config,

  // Element type
  type: 'your-type',

  // Custom properties
  myOption: true,
  anotherOption: 'default',

  // Validation messages (i18n keys)
  validationMessages: {
    required: 'This field is required',
    custom: 'Custom validation failed'
  }
};

อ่าน Configuration จาก HTML

ใช้ extractCustomConfig() เพื่ออ่าน data attributes:

static extractCustomConfig(element, def, dataset) {
  return {
    // Boolean: เปรียบเทียบกับ 'true'
    myBoolean: dataset.myBoolean === 'true',

    // Number: ใช้ parseNumeric helper
    myNumber: this.parseNumeric('myNumber', element, def, dataset),

    // String: ใช้ dataset หรือ default
    myString: dataset.myString || def.myString
  };
}

ลำดับความสำคัญของ Configuration

  1. JavaScript options (สูงสุด)
  2. *data- attributes**
  3. HTML attributes (min, max, required)
  4. Static config (ต่ำสุด)

Property Handlers

Property handlers ให้ getter/setter สำหรับ element properties:

static propertyHandlers = {
  propertyName: {
    get(element) {
      // คืนค่าปัจจุบัน
      return element.value;
    },
    set(instance, newValue) {
      // ใช้ค่าใหม่
      instance.element.value = newValue;
      // อัพเดท UI ถ้าต้องการ
      MyFactory.updateDisplay(instance);
    }
  }
};

Validation

validateSpecific Method

เพิ่ม custom validation ใน setupElement:

instance.validateSpecific = function(value) {
  // ตรวจสอบกฎกำหนดเอง
  if (!this.isValidFormat(value)) {
    return 'รูปแบบไม่ถูกต้อง';
  }

  // คืนค่า null ถ้าถูกต้อง
  return null;
};

Registration

กับ ElementManager

// ลงทะเบียน element type
ElementManager.registerElement('my-element', MyElementFactory);

Auto-Enhancement

เมื่อลงทะเบียนแล้ว elements จะถูก auto-enhanced:

<input data-element="my-element" name="field">

ตัวอย่างเต็ม

PhoneElementFactory

/**
 * PhoneElementFactory - Thai phone number input พร้อม formatting
 */
class PhoneElementFactory extends TextElementFactory {
  static config = {
    ...TextElementFactory.config,
    type: 'tel',
    pattern: '###-###-####',
    placeholder: '081-234-5678',
    validationMessages: {
      required: 'กรุณากรอกหมายเลขโทรศัพท์',
      pattern: 'กรุณากรอกหมายเลขโทรศัพท์ที่ถูกต้อง'
    }
  };

  static setupElement(instance) {
    super.setupElement?.(instance);

    const { element, config } = instance;

    // ตั้งค่า input attributes
    element.type = 'tel';
    element.inputMode = 'numeric';
    element.placeholder = config.placeholder;

    // เพิ่ม formatting on input
    instance.formatValue = (value) => {
      const digits = value.replace(/\D/g, '').substring(0, 10);
      if (digits.length <= 3) return digits;
      if (digits.length <= 6) return `${digits.slice(0,3)}-${digits.slice(3)}`;
      return `${digits.slice(0,3)}-${digits.slice(3,6)}-${digits.slice(6)}`;
    };

    // Custom validation
    instance.validateSpecific = function(value) {
      if (!value && this.element.required) {
        return Now.translate(config.validationMessages.required);
      }
      if (value && !/^\d{3}-\d{3}-\d{4}$/.test(value)) {
        return Now.translate(config.validationMessages.pattern);
      }
      return null;
    };

    // เพิ่ม input handler
    EventSystemManager.addHandler(element, 'input', (e) => {
      const formatted = instance.formatValue(e.target.value);
      if (formatted !== e.target.value) {
        e.target.value = formatted;
      }
    });

    return instance;
  }
}

// ลงทะเบียน
ElementManager.registerElement('phone', PhoneElementFactory);
window.PhoneElementFactory = PhoneElementFactory;

การใช้งาน

<input data-element="phone"
       name="phone"
       required>
<!-- Auto-formats เป็น: 081-234-5678 -->

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

1. ✅ ขยาย Base Class ที่เหมาะสม

// สำหรับ text-based inputs
class MyElement extends TextElementFactory { }

// สำหรับ numeric inputs
class MyElement extends NumberElementFactory { }

// สำหรับ basic functionality
class MyElement extends ElementFactory { }

2. ✅ เรียก Super Methods เสมอ

static setupElement(instance) {
  super.setupElement?.(instance);  // เรียก parent ก่อน
  // แล้วเพิ่ม custom behavior
}

3. ✅ ใช้ EventSystemManager

// ✅ ดี: Events ที่ติดตามได้
EventSystemManager.addHandler(element, 'click', handler);

// ❌ ไม่ดี: Native events ไม่ถูกติดตาม
element.addEventListener('click', handler);

4. ✅ Implement Cleanup

static cleanup(instance) {
  // ลบ elements ที่สร้างขึ้น
  if (instance.wrapper?.parentNode) {
    instance.wrapper.parentNode.removeChild(instance.wrapper);
  }

  // เรียก parent cleanup
  super.cleanup?.(instance);
}

5. ✅ รองรับ i18n

// ใช้ Now.translate สำหรับข้อความที่ผู้ใช้เห็น
const message = Now.translate(config.validationMessages.required);

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