Now.js Framework Documentation
สร้าง Custom Elements
สร้าง Custom Elements
คู่มือสำหรับการสร้าง Element Factory classes กำหนดเองเพื่อขยายความสามารถของ Now.js form handling
📋 สารบัญ
- ภาพรวม
- เมื่อไหร่ควรสร้าง Custom Elements
- คู่มือแบบทีละขั้นตอน
- Configuration
- Property Handlers
- Validation
- การจัดการเหตุการณ์
- Registration
- ตัวอย่างเต็ม
- แนวทางปฏิบัติที่ดี
ภาพรวม
Form elements ทั้งหมดใน Now.js ขยายจาก ElementFactory base class การสร้าง custom element ประกอบด้วย:
- ขยาย
ElementFactory(หรือ factory เฉพาะทาง เช่นTextElementFactory) - กำหนด configuration defaults
- Implement
setupElement()สำหรับ custom behavior - เพิ่ม property handlers และ validators (optional)
- ลงทะเบียนกับ
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
- JavaScript options (สูงสุด)
- *data- attributes**
- HTML attributes (min, max, required)
- 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
- ตัวเลือกการตั้งค่า
- ตัวอย่างการใช้งาน
- เหตุการณ์ที่ส่งออก
เอกสารที่เกี่ยวข้อง
- Form Elements ภาพรวม
- ElementFactory - Base class reference
- ElementManager - Registration system
- FormManager - Form integration