Now.js Framework Documentation
Special Elements
Special Elements
Documentation for specialized form elements: ColorElementFactory, RangeElementFactory, TagsElementFactory, MaskElementFactory, and TextareaElementFactory.
📋 Table of Contents
- Overview
- ColorElementFactory
- RangeElementFactory
- TagsElementFactory
- MaskElementFactory
- TextareaElementFactory
- Best Practices
Overview
Special elements provide enhanced UI for specific input types that need more than basic text or number handling.
| Element | data-element | Description |
|---|---|---|
| ColorElementFactory | color |
Color picker with palette |
| RangeElementFactory | range |
Single/dual range slider |
| TagsElementFactory | tags |
Tags/chips input with autocomplete |
| MaskElementFactory | mask |
Input masking (phone, date, etc.) |
| TextareaElementFactory | textarea |
Auto-resize textarea |
ColorElementFactory
ColorElementFactory creates a custom color picker with predefined palette and hex input.
Basic Usage
<input type="color"
data-element="color"
name="theme_color"
value="#007bff">
<!-- Or with text input -->
<input type="text"
data-element="color"
name="bg_color"
value="#ffffff">HTML Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-element |
string | - | Set to color |
value |
string | #000000 |
Initial color (hex) |
disabled |
boolean | false |
Disable picker |
readonly |
boolean | false |
Read-only mode |
Default Color Palette
const COLOR_PALETTE = [
'#FF5722', '#F44336', '#E91E63', '#9C27B0',
'#673AB7', '#3F51B5', '#2196F3', '#03A9F4',
'#00BCD4', '#009688', '#4CAF50', '#8BC34A',
'#CDDC39', '#FFEB3B', '#FFC107', '#FF9800',
// ... and more standard colors
'#FFFFFF', '#000000'
];JavaScript API
const instance = ElementManager.getInstance(colorInput);
// Get current color
const color = instance.getColor(); // '#007bff'
// Set color
instance.setColor('#ff5722');
// Open/close picker
instance.open();
instance.close();
instance.toggle();
// Disable/enable
instance.setDisabled(true);
instance.setReadonly(true);Features
- Color Palette: Click predefined colors
- Hex Input: Type custom hex values
- Preview: Color preview in button
- Copy/Paste: Ctrl+C to copy, Ctrl+V to paste colors
- Contrast Text: Button text auto-adjusts for visibility
Events
colorInput.addEventListener('change', (e) => {
console.log('Selected color:', e.target.value);
});RangeElementFactory
RangeElementFactory creates enhanced range sliders with better UX than native range inputs. Supports single and dual range modes.
Basic Usage
<!-- Single range slider -->
<input type="range"
data-element="range"
min="0"
max="100"
value="50"
name="volume">
<!-- Dual range slider (min-max) -->
<input type="range"
data-element="range"
min="0"
max="1000"
data-value="200-800"
data-dual="true"
name="price_range">HTML Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-element |
string | - | Set to range |
min |
number | 0 |
Minimum value |
max |
number | 100 |
Maximum value |
step |
number | 1 |
Step increment |
value |
string | - | Single value or "min-max" for dual |
data-dual |
boolean | false |
Enable dual handles |
data-show-value |
boolean | true |
Show value label |
data-format |
string | - | Value format (e.g., {value}%) |
disabled |
boolean | false |
Disable slider |
readonly |
boolean | false |
Read-only mode |
Single Range
<input type="range"
data-element="range"
min="0"
max="100"
step="5"
value="75"
data-format="{value}%"
name="opacity">Dual Range (Min-Max)
<input type="range"
data-element="range"
min="0"
max="10000"
step="100"
data-value="2000-8000"
data-dual="true"
data-format="฿{value}"
name="budget">JavaScript API
const instance = ElementManager.getInstance(rangeInput);
// Get value
const value = instance.getValue();
// Single: 75
// Dual: {min: 2000, max: 8000}
// Set value
instance.setValue(80); // Single
instance.setValue({min: 1000, max: 5000}); // Dual
// Update constraints
instance.setMin(10);
instance.setMax(200);
instance.setStep(10);
// Enable/disable
instance.setDisabled(true);
instance.setReadonly(true);Keyboard Navigation
| Key | Action |
|---|---|
← / ↓ |
Decrease by step |
→ / ↑ |
Increase by step |
Home |
Go to min |
End |
Go to max |
Page Up |
Increase by 10 steps |
Page Down |
Decrease by 10 steps |
Events
// Input event (while dragging)
rangeInput.addEventListener('input', (e) => {
console.log('Current value:', e.detail.value);
});
// Change event (after release)
rangeInput.addEventListener('change', (e) => {
console.log('Final value:', e.detail.value);
});TagsElementFactory
TagsElementFactory transforms text input into a tags/chips input with visual tags, keyboard navigation, and optional autocomplete.
Basic Usage
<!-- Simple tags input -->
<input type="text"
data-element="tags"
name="keywords"
placeholder="Add tags...">
<!-- Suggestions from the options of the form's load API -->
<input type="text"
data-element="tags"
data-options-key="tags"
name="categories">
<!-- Suggestions searched from an endpoint, only picked values become tags -->
<input type="text"
data-element="tags"
data-source="api/example/members"
data-allow-new="false"
name="members">
<!-- Tags with datalist -->
<input type="text"
data-element="tags"
list="available-tags"
name="skills">
<datalist id="available-tags">
<option value="1" label="JavaScript">
<option value="2" label="Python">
</datalist>HTML Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-element |
string | - | Set to tags |
data-source |
string | - | A URL (api/…, /…, http…) is searched as you type, see below. Anything else names a global variable or holds a JSON list |
data-options-key |
string | - | Key of the suggestion list in options of the form's load API |
list |
string | - | Datalist ID for options |
data-allow-new |
boolean | true |
false = only a picked suggestion becomes a tag. Enter on typed text and a pasted list add nothing |
data-separator |
string | , |
Separator for pasted values |
data-max-tags |
number | none | Maximum tags allowed |
data-duplicates |
boolean | false |
Allow duplicate tags |
data-min-length |
number | 2 for a URL |
Characters typed before an endpoint is asked. A list filters from the first character and shows every option on focus |
data-depends |
string | - | Other fields of the form sent with the search, or used to filter a list — the same attribute text inputs read, see text-elements.md "Filtering by Other Fields" |
data-depends-clear |
boolean | false |
true = remove every tag when a data-depends field changes |
Searching an Endpoint
When data-source is a URL the input asks the endpoint instead of filtering a list.
The request and response are the ones a text input with data-autocomplete uses, so one endpoint serves both.
- Typing at least
data-min-lengthcharacters sendsGET <data-source>?q=<typed>(300 ms after the last key) - The endpoint answers with the standard envelope,
datais a list of{value, text} - Rows are shown as returned — the endpoint has already matched them, possibly on a column
textdoes not show - An answer to an older query that arrives after a newer one is dropped
- Picking a row closes the suggestions. Focus asks again only when enough text is still typed
// GET api/example/members?q=som
$search = $request->get('q')->topic();
return $this->successResponse([
['value' => '12', 'text' => 'Somchai Jaidee (somchai@example.com)']
], 'Search completed');An endpoint cannot turn a saved value back into its label, so a load API binds saved
values as {key, value} pairs — data-attr="value:members" with members: [{key: '12', value: 'Somchai Jaidee'}].
Only Picked Values
data-allow-new="false" is for values that must exist on the server, such as member IDs.
Typed text stays in the input to be corrected and is never submitted, so the server only receives keys the endpoint offered.
Configuration
TagsElementFactory.config = {
type: 'text',
placeholder: 'Add tags...',
separator: ',',
maxTags: null,
duplicates: false,
allowNew: true,
autocomplete: {enabled: false, source: null, minLength: 2, maxResults: 10, delay: 300}
};Keyboard Interaction
| Key | Action |
|---|---|
Enter |
Pick the highlighted suggestion, or add the typed text when new tags are allowed |
Backspace |
Remove last tag (when input empty) |
↓ / ↑ |
Navigate autocomplete |
Escape |
Close autocomplete |
JavaScript API
const instance = ElementManager.getInstance(tagsInput);
// Get all tags
const tags = instance.getTags();
// [{key: '1', value: 'JavaScript'}, {key: '2', value: 'Python'}]
// Add tag
instance.addTag('3', 'React');
// Remove tag
instance.removeTag('2');
// Set all tags
instance.setTags([
{key: '1', value: 'JavaScript'},
{key: '4', value: 'Vue'}
]);
// Clear all
instance.clear();
// Update autocomplete options
instance.updateOptions([
{value: '1', text: 'JavaScript'},
{value: '2', text: 'Python'}
]);Form Submission
Tags are submitted as hidden inputs:
<!-- Original input -->
<input data-element="tags" name="skills">
<!-- Generated hidden inputs -->
<input type="hidden" name="skills[]" value="1">
<input type="hidden" name="skills[]" value="2">
<input type="hidden" name="skills[]" value="3">Events
tagsInput.addEventListener('change', (e) => {
const instance = ElementManager.getInstance(e.target);
console.log('Tags:', instance.getTags());
});TagsElementFactory emits no tags:* events — adding or removing a tag fires the
native change event only. Compare instance.getTags() against the previous
list if you need to know which tag moved.
MaskElementFactory
MaskElementFactory applies input masks for structured data like phone numbers, dates, credit cards, etc.
Basic Usage
<!-- Phone number -->
<input type="tel"
data-element="mask"
data-mask="(###) ###-####"
name="phone">
<!-- Thai phone -->
<input type="tel"
data-element="tel"
name="mobile">
<!-- Credit card -->
<input type="text"
data-element="mask"
data-mask="#### #### #### ####"
name="card">
<!-- Date -->
<input type="text"
data-element="mask"
data-mask="##/##/####"
name="date">HTML Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-element |
string | - | mask or tel |
data-mask |
string | - | Mask pattern |
data-placeholder-char |
string | _ |
Character for unfilled positions |
data-mask-on-init |
boolean | true |
Apply mask on init |
data-mask-on-change |
boolean | true |
Apply mask on change |
data-mask-on-blur |
boolean | true |
Apply mask on blur |
data-unmask-on-submit |
boolean | false |
Remove mask on form submit |
Mask Pattern Characters
| Character | Accepts |
|---|---|
# |
Digit (0-9) |
A |
Letter (A-Z, a-z) |
* |
Alphanumeric |
| Other | Literal (shown as-is) |
Common Patterns
| Use Case | Pattern | Example |
|---|---|---|
| US Phone | (###) ###-#### |
(123) 456-7890 |
| Thai Phone | 99-9999-9999 |
08-1234-5678 |
| Date | ##/##/#### |
31/12/2025 |
| Time | ##:## |
14:30 |
| Credit Card | #### #### #### #### |
1234 5678 9012 3456 |
| SSN | ###-##-#### |
123-45-6789 |
| Zip+4 | #####-#### |
12345-6789 |
TelElementFactory
Mask for Thai phone numbers, applied to every <input type="tel">:
<input type="tel" name="phone" maxlength="10">
<!-- Typing 0812345678 shows 08-1234-5678 -->The mask is 99-9999-9999; a value must match ^[0-9]{2}-[0-9]{4}-[0-9]{3,4}$.
maxlength and masks: maxlength may be written for the raw value (10
digits). A fixed mask adds its own separators, so a maxlength shorter than the
mask grows to the mask length (12 here) — otherwise typing would stop at
08-1234-56 and validation would reject the complete number. A longer
maxlength, and masks containing # (variable length), stay as written.
The server receives the masked text (08-1234-5678) and strips the separators
(Kotchasan: $request->post('phone')->number()).
JavaScript API
// Assigning the value formats it
maskedInput.value = '0898765432'; // shows 08-9876-5432
// The raw digits
const raw = maskedInput.value.replace(/\D/g, ''); // '0898765432'Events
maskedInput.addEventListener('change', (e) => {
console.log('Masked:', e.target.value);
console.log('Raw:', e.target.value.replace(/\D/g, ''));
});TextareaElementFactory
TextareaElementFactory enhances textarea with auto-resize functionality.
Basic Usage
<textarea data-element="textarea"
name="description"
rows="3"
placeholder="Enter description..."></textarea>HTML Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-element |
string | - | Set to textarea |
data-auto-resize |
boolean | true |
Enable auto-resize |
data-min-rows |
number | 3 |
Minimum rows |
data-max-rows |
number | 10 |
Maximum rows before scroll |
maxlength |
number | - | Character limit |
data-show-counter |
boolean | false |
Show character counter |
Auto-Resize Behavior
The textarea automatically adjusts height based on content:
/* Textarea expands with content */
[data-element="textarea"] {
min-height: calc(3 * 1.5em); /* 3 rows minimum */
max-height: calc(10 * 1.5em); /* 10 rows maximum */
overflow-y: auto; /* Scroll when max reached */
resize: vertical; /* Allow manual resize */
}Character Counter
<textarea data-element="textarea"
data-show-counter="true"
maxlength="500"
name="bio"></textarea>
<!-- Shows: 0/500 characters -->JavaScript API
const instance = ElementManager.getInstance(textareaEl);
// Force resize recalculation
instance.resize();
// Get current height
const height = instance.getHeight();Best Practices
1. ✅ Use Appropriate Element for Data Type
<!-- ✅ Colors -->
<input data-element="color" name="theme">
<!-- ✅ Price ranges -->
<input data-element="range" data-dual="true" name="budget">
<!-- ✅ Multiple categories -->
<input data-element="tags" name="categories">
<!-- ✅ Structured input -->
<input data-element="mask" data-mask="##/##/####" name="date">2. ✅ Set Sensible Limits
<!-- ✅ Good: Limit tags -->
<input data-element="tags" data-max-tags="5" name="keywords">
<!-- ✅ Good: Limit textarea -->
<textarea data-element="textarea" maxlength="1000" data-max-rows="15"></textarea>3. ✅ Provide Autocomplete for Tags
<!-- ✅ Good: User can discover existing tags -->
<input data-element="tags"
data-source="/api/tags"
name="skills">
<!-- ❌ Bad: User must guess tag names -->
<input data-element="tags" name="skills">4. ✅ Use tel Type for Phone Numbers
<!-- ✅ Good: Mobile keyboard shows numbers -->
<input type="tel"
data-element="tel"
name="phone">
<!-- ❌ Less ideal: Text keyboard on mobile -->
<input type="text"
data-element="mask"
data-mask="###-###-####"
name="phone">5. ✅ Consider Unmask on Submit
<!-- ✅ Good: Server receives clean number -->
<input data-element="mask"
data-mask="(###) ###-####"
data-unmask-on-submit="true"
name="phone">
<!-- Form submits: 1234567890 instead of (123) 456-7890 -->6. ✅ Use Dual Range for Filters
<!-- ✅ Good: Price filter with min-max -->
<input data-element="range"
data-dual="true"
min="0"
max="10000"
step="100"
data-format="฿{value}"
name="price">Related Documentation
- Form Elements Overview
- ElementFactory - Base class
- Text Elements - Text inputs
- Number Elements - Number inputs
- FormManager - Form-level validation