Now.js Framework Documentation

Now.js Framework Documentation

Creating Custom Elements

EN 10 Dec 2025 07:08

Creating Custom Elements

Guide for creating custom Element Factory classes to extend Now.js form handling capabilities.

📋 Table of Contents

  1. Overview
  2. When to Create Custom Elements
  3. Step-by-Step Guide
  4. Configuration
  5. Property Handlers
  6. Validation
  7. Event Handling
  8. Registration
  9. Complete Example
  10. Best Practices

Overview

All form elements in Now.js extend the ElementFactory base class. Creating a custom element involves:

  1. Extending ElementFactory (or a specialized factory like TextElementFactory)
  2. Defining configuration defaults
  3. Implementing setupElement() for custom behavior
  4. Optionally adding property handlers and validators
  5. 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 element

When 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

  1. JavaScript options (highest priority)
  2. *data- attributes**
  3. HTML attributes (min, max, required)
  4. 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