Now.js Framework Documentation

Now.js Framework Documentation

สร้าง Custom Elements

TH 04 Sep 2026 01:40

สร้าง Custom Elements

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

📋 สารบัญ

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

ภาพรวม

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,
    allowHalf: false,
    showValue: true,
    iconFilled: '★',
    iconEmpty: '☆',
    validationMessages: {
      required: 'กรุณาเลือกคะแนน',
      min: 'คะแนนต้องอย่างน้อย {min}',
      max: 'คะแนนต้องไม่เกิน {max}'
    }
  };
}

ขั้นตอน 3: ดึง Configuration ที่กำหนดเอง

class RatingElementFactory extends ElementFactory {
  // ... config ด้านบน

  static extractCustomConfig(element, def, dataset) {
    return {
      min: this.parseNumeric('min', element, def, dataset) ?? def.min,
      max: this.parseNumeric('max', element, def, dataset) ?? def.max,
      step: this.parseNumeric('step', element, def, dataset) ?? def.step,
      allowHalf: dataset.allowHalf === 'true',
      showValue: dataset.showValue !== 'false',
      iconFilled: dataset.iconFilled || def.iconFilled,
      iconEmpty: dataset.iconEmpty || def.iconEmpty
    };
  }
}

ขั้นตอน 4: 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);
    }

    // ส่วนแสดงค่า
    if (config.showValue) {
      instance.valueDisplay = document.createElement('span');
      instance.valueDisplay.className = 'rating-value';
      wrapper.appendChild(instance.valueDisplay);
    }

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

    // Initialize display
    this.updateDisplay(instance);

    return instance;
  }

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

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

    if (valueDisplay) {
      valueDisplay.textContent = value ? `${value}/${config.max}` : '';
    }
  }

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

ขั้นตอน 5: เพิ่ม Property Handlers

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

  static propertyHandlers = {
    value: {
      get(element) {
        return parseFloat(element.value) || 0;
      },
      set(instance, newValue) {
        RatingElementFactory.setValue(instance, newValue);
      }
    },

    max: {
      get(element) {
        return parseFloat(element.dataset.max) || 5;
      },
      set(instance, newValue) {
        instance.config.max = newValue;
        // สร้างดาวใหม่
        RatingElementFactory.rebuildStars(instance);
      }
    }
  };
}

ขั้นตอน 6: เพิ่ม Validation

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

  static setupElement(instance) {
    // ... setup code ก่อนหน้า

    instance.validateSpecific = function(value) {
      if (this.element.required && !value) {
        return Now.translate(this.config.validationMessages.required);
      }

      const numValue = parseFloat(value);
      if (numValue < this.config.min) {
        return Now.translate(this.config.validationMessages.min, {
          min: this.config.min
        });
      }
      if (numValue > this.config.max) {
        return Now.translate(this.config.validationMessages.max, {
          max: this.config.max
        });
      }

      return null; // ไม่มีข้อผิดพลาด
    };

    return instance;
  }
}

ขั้นตอน 7: ลงทะเบียนกับ 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,

    // อ้างอิงถึงฟังก์ชัน
    myCallback: dataset.myCallback
      ? window[dataset.myCallback]
      : def.myCallback
  };
}

ลำดับความสำคัญของ 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);
    }
  }
};

Properties ที่ใช้บ่อย

Property จุดประสงค์
value ค่าหลักของ element
min, max ข้อจำกัดของช่วงค่า
disabled สถานะเปิด/ปิดใช้งาน
required การตรวจสอบว่าต้องกรอก
options สำหรับ element ประเภท select

Validation

validateSpecific Method

เพิ่ม custom validation ใน setupElement:

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

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

Custom Validators

เพิ่มเข้าไปใน validators ของ factory:

static validators = {
  customRule: (value) => {
    // คืนค่า true ถ้าถูกต้อง
    return /pattern/.test(value);
  }
};

ใช้ใน HTML:

<input data-element="my-element" data-validate="customRule">

การจัดการเหตุการณ์

setupEventListeners

Override เพื่อเพิ่ม event handlers กำหนดเอง:

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

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

  // เพิ่ม handlers กำหนดเอง
  EventSystemManager.addHandler(element, 'focus', () => {
    // จัดการ focus
  });

  EventSystemManager.addHandler(element, 'blur', () => {
    // จัดการ blur
  });
}

ส่งเหตุการณ์กำหนดเอง

element.dispatchEvent(new CustomEvent('rating:change', {
  bubbles: true,
  detail: {
    value: newValue,
    oldValue: oldValue
  }
}));

Registration

กับ ElementManager

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

Auto-Enhancement

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

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

สร้างด้วยตนเอง

// สร้างขึ้นใหม่ทั้งหมด
MyElementFactory.create({
  name: 'field',
  value: 'initial'
});

// ปรับปรุง element ที่มีอยู่แล้ว
MyElementFactory.enhance(existingElement, {
  option: 'value'
});

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

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 extractCustomConfig(element, def, dataset) {
    const parentConfig = super.extractCustomConfig?.(element, def, dataset) || {};
    return {
      ...parentConfig,
      pattern: dataset.pattern || def.pattern
    };
  }

  static setupElement(instance) {
    // เรียก parent setup
    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);

// รองรับ data-i18n สำหรับข้อความคงที่
placeholder.dataset.i18n = 'Select option';

6. ✅ เขียนเอกสารประกอบ Element ของคุณ

ใส่ไว้ในไฟล์ element ของคุณ:

  • JSDoc comments
  • ตัวเลือกการตั้งค่า
  • ตัวอย่างการใช้งาน
  • เหตุการณ์ที่ส่งออก

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