Now.js Framework Documentation
Creating Custom Elements
Creating Custom Elements
Guide for creating custom Element Factory classes to extend Now.js form handling capabilities.
📋 Table of Contents
- Overview
- When to Create Custom Elements
- Step-by-Step Guide
- Configuration
- Property Handlers
- Validation
- Event Handling
- Registration
- Complete Example
- Best Practices
Overview
All form elements in Now.js extend the ElementFactory base class. Creating a custom element involves:
- Extending
ElementFactory(or a specialized factory likeTextElementFactory) - Defining configuration defaults
- Implementing
setupElement()for custom behavior - Optionally adding property handlers and validators
- Registering with
ElementManager
Class Hierarchy
ElementFactory (base)
├── TextElementFactory # Extend for text-based inputs
├── NumberElementFactory # Extend for numeric inputs
├── SelectElementFactory # Extend for dropdown inputs
└── YourCustomFactory # Your custom elementWhen to Create Custom Elements
✅ Create Custom Element When:
- You need reusable specialized input behavior
- Standard elements don't support your data format
- You need custom validation logic
- You want consistent UI/UX for specific input types
❌ Don't Create Custom Element When:
- Standard validation rules suffice (use
data-validate) - You only need one-off customization (use JavaScript)
- The behavior can be achieved with configuration
Step-by-Step Guide
Step 1: Create Factory Class
class RatingElementFactory extends ElementFactory {
// Configuration will go here
}Step 2: Define Configuration
class RatingElementFactory extends ElementFactory {
static config = {
...ElementFactory.config, // Inherit base config
type: 'rating',
min: 1,
max: 5,
step: 1,
allowHalf: false,
showValue: true,
iconFilled: '★',
iconEmpty: '☆',
validationMessages: {
required: 'Please select a rating',
min: 'Rating must be at least {min}',
max: 'Rating must be at most {max}'
}
};
}Step 3: Extract Custom Configuration
class RatingElementFactory extends ElementFactory {
// ... config above
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
};
}
}Step 4: Implement setupElement
class RatingElementFactory extends ElementFactory {
// ... previous code
static setupElement(instance) {
const { element, config } = instance;
// Hide original input
element.type = 'hidden';
// Create rating UI
const wrapper = document.createElement('div');
wrapper.className = 'rating-wrapper';
element.parentNode.insertBefore(wrapper, element);
// Create stars
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);
}
// Value display
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 }));
}
}Step 5: Add Property Handlers
class RatingElementFactory extends ElementFactory {
// ... previous 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;
// Rebuild stars
RatingElementFactory.rebuildStars(instance);
}
}
};
}Step 6: Add Validation
class RatingElementFactory extends ElementFactory {
// ... previous code
static setupElement(instance) {
// ... previous 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; // No error
};
return instance;
}
}Step 7: Register with ElementManager
// Register the element type
ElementManager.registerElement('rating', RatingElementFactory);
// Expose globally (optional)
window.RatingElementFactory = RatingElementFactory;Configuration
Static Config Object
static config = {
// Inherit from 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'
}
};Reading Configuration from HTML
Use extractCustomConfig() to read data attributes:
static extractCustomConfig(element, def, dataset) {
return {
// Boolean: compare to 'true'
myBoolean: dataset.myBoolean === 'true',
// Number: use parseNumeric helper
myNumber: this.parseNumeric('myNumber', element, def, dataset),
// String: use dataset or default
myString: dataset.myString || def.myString,
// Function reference
myCallback: dataset.myCallback
? window[dataset.myCallback]
: def.myCallback
};
}Configuration Priority
- JavaScript options (highest priority)
- *data- attributes**
- HTML attributes (min, max, required)
- Static config (lowest priority)
Property Handlers
Property handlers provide getter/setter for element properties:
static propertyHandlers = {
propertyName: {
get(element) {
// Return current value
return element.value;
},
set(instance, newValue) {
// Apply new value
instance.element.value = newValue;
// Update UI if needed
MyFactory.updateDisplay(instance);
}
}
};Common Properties
| Property | Purpose |
|---|---|
value |
Main element value |
min, max |
Range constraints |
disabled |
Enabled/disabled state |
required |
Required validation |
options |
For select-type elements |
Validation
validateSpecific Method
Add custom validation in setupElement:
instance.validateSpecific = function(value) {
// Check custom rules
if (!this.isValidFormat(value)) {
return 'Invalid format';
}
// Return null if valid
return null;
};Custom Validators
Add to the factory's validators:
static validators = {
customRule: (value) => {
// Return true if valid
return /pattern/.test(value);
}
};Use in HTML:
<input data-element="my-element" data-validate="customRule">Event Handling
setupEventListeners
Override to add custom event handlers:
static setupEventListeners(instance) {
const { element, config } = instance;
// Call parent
super.setupEventListeners?.(instance);
// Add custom handlers
EventSystemManager.addHandler(element, 'focus', () => {
// Handle focus
});
EventSystemManager.addHandler(element, 'blur', () => {
// Handle blur
});
}Dispatching Custom Events
element.dispatchEvent(new CustomEvent('rating:change', {
bubbles: true,
detail: {
value: newValue,
oldValue: oldValue
}
}));Registration
With ElementManager
// Register element type
ElementManager.registerElement('my-element', MyElementFactory);Auto-Enhancement
Once registered, elements are auto-enhanced:
<input data-element="my-element" name="field">Manual Creation
// Create from scratch
MyElementFactory.create({
name: 'field',
value: 'initial'
});
// Enhance existing element
MyElementFactory.enhance(existingElement, {
option: 'value'
});Complete Example
PhoneElementFactory
/**
* PhoneElementFactory - Thai phone number input with formatting
*/
class PhoneElementFactory extends TextElementFactory {
static config = {
...TextElementFactory.config,
type: 'tel',
pattern: '###-###-####',
placeholder: '081-234-5678',
validationMessages: {
required: 'Please enter phone number',
pattern: 'Please enter valid phone format'
}
};
static extractCustomConfig(element, def, dataset) {
const parentConfig = super.extractCustomConfig?.(element, def, dataset) || {};
return {
...parentConfig,
pattern: dataset.pattern || def.pattern
};
}
static setupElement(instance) {
// Call parent setup
super.setupElement?.(instance);
const { element, config } = instance;
// Set input attributes
element.type = 'tel';
element.inputMode = 'numeric';
element.placeholder = config.placeholder;
// Add 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;
};
// Add 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;
}
}
// Register
ElementManager.registerElement('phone', PhoneElementFactory);
window.PhoneElementFactory = PhoneElementFactory;Usage
<input data-element="phone"
name="phone"
required>
<!-- Auto-formats to: 081-234-5678 -->Best Practices
1. ✅ Extend Appropriate Base Class
// For text-based inputs
class MyElement extends TextElementFactory { }
// For numeric inputs
class MyElement extends NumberElementFactory { }
// For basic functionality
class MyElement extends ElementFactory { }2. ✅ Always Call Super Methods
static setupElement(instance) {
super.setupElement?.(instance); // Call parent first
// Then add custom behavior
}3. ✅ Use EventSystemManager
// ✅ Good: Trackable events
EventSystemManager.addHandler(element, 'click', handler);
// ❌ Bad: Native events not tracked
element.addEventListener('click', handler);4. ✅ Implement Cleanup
static cleanup(instance) {
// Remove created elements
if (instance.wrapper?.parentNode) {
instance.wrapper.parentNode.removeChild(instance.wrapper);
}
// Call parent cleanup
super.cleanup?.(instance);
}5. ✅ Support i18n
// Use Now.translate for user-facing text
const message = Now.translate(config.validationMessages.required);
// Support data-i18n for static text
placeholder.dataset.i18n = 'Select option';6. ✅ Document Your Element
Include in your element file:
- JSDoc comments
- Configuration options
- Usage examples
- Events emitted
Related Documentation
- Form Elements Overview
- ElementFactory - Base class reference
- ElementManager - Registration system
- FormManager - Form integration