Now.js Framework Documentation
Text-based Elements
Text-based Elements
Documentation for TextElementFactory, PasswordElementFactory, and SearchElementFactory - text input components with autocomplete, validation, and specialized features.
📋 Table of Contents
Overview
Text-based elements handle text input with features like autocomplete, validation, and formatting. They share the TextElementFactory base and register different data-element types.
| Element | data-element | Description |
|---|---|---|
| TextElementFactory | text |
Text input with autocomplete, hidden input support |
| PasswordElementFactory | password |
Password with strength meter, toggle visibility |
| SearchElementFactory | search |
Search input with debounce, results container |
TextElementFactory
TextElementFactory manages text inputs with autocomplete and hidden input support. When autocomplete is enabled, it creates a hidden input to store the selected value (key) while displaying the text (label) to the user.
Basic Usage
<!-- Simple text input -->
<input type="text"
data-element="text"
name="username"
placeholder="Enter username">
<!-- Text with validation -->
<input type="text"
data-element="text"
data-validate="email"
name="email"
placeholder="Enter email">Autocomplete Feature
Static Source (Array)
<input type="text"
data-element="text"
data-autocomplete="true"
data-source='[{"value":"1","text":"Bangkok"},{"value":"2","text":"Chiang Mai"}]'
name="city"
placeholder="Select city">Datalist Source
<input type="text"
data-element="text"
list="cities"
name="city">
<datalist id="cities">
<option value="1" label="Bangkok">
<option value="2" label="Chiang Mai">
<option value="3" label="Phuket">
</datalist>💡 Note: When using datalist, the
<datalist>element is automatically removed and converted to an autocomplete dropdown.
Ajax Source
<input type="text"
data-element="text"
data-autocomplete="true"
data-source="/api/cities"
data-min-length="2"
name="city"
placeholder="Search city...">Expected API Response:
{
"success": true,
"data": [
{"value": "1", "text": "Bangkok"},
{"value": "2", "text": "Chiang Mai"}
]
}Filtering by Other Fields
data-depends sends the values of other fields of the same form along with the search.
It works on every autocomplete — text inputs (and the search, email, url and username inputs built on them) and tags inputs.
<select name="department">...</select>
<input type="checkbox" name="status[]" value="1">
<input type="text"
data-autocomplete="true"
data-source="api/example/members"
data-depends="department,status"
name="member">
<!-- GET api/example/members?q=som&department=2&status[]=1 -->- Entries are comma separated: a field name, or
param:fieldto send the field under another name (dept:department→&dept=2) - A field is found in the form by
name(fieldorfield[]), then by id - Values are read the way the form submits them: the checked boxes, the checked radio, every selected option, the tags of a tags field, the picked value of another autocomplete
- A name ending in
[]or a multiple select is sent as an array (status[]=1&status[]=2), anything else as one value - Disabled fields and fields without a value send nothing, so an empty filter does not narrow the search
- A list source (array, datalist,
data-options-key) is filtered on the row property named like the parameter:{value: '5', text: 'Somchai', department: '2'}is shown while the department field holds2. Rows without that property are always shown, and an array property matches any of its values - Changing a filter field keeps what was already picked.
data-depends-clear="true"empties the input (or removes every tag) when a filter field changes
Filling Other Fields From the Selected Item
Whatever else the API returns with a suggestion is written into the fields of the
same name, so one selection can fill a whole block of the form. No markup is
needed: the API decides what a selection fills.
{
"success": true,
"data": [
{"value": "SN-001", "text": "SN-001 : Notebook", "topic": "Notebook", "price": "25000"}
]
}<input type="text" id="product_no" name="product_no"
data-autocomplete="true" data-source="api/repair/autocomplete/find">
<!-- Filled by the item's `topic` and `price` when a suggestion is chosen -->
<input type="text" id="topic" name="topic" readonly>
<input type="text" id="price" name="price">A target is looked up by id first, then by name, and only inside the same
form — two forms on one page never fill each other. Every element type is
supported: text inputs and textareas take the value, a checkbox is ticked by the
truthiness of the value, a radio group selects the member whose value matches, a
multiple select selects each value of a comma-separated list or array, and an
element that is not a form control (a <span>, <div>, or table cell) receives
the text. A target that is itself an autocomplete is written through its own
instance, so its visible text and hidden value stay in step.
What is never filled: value, text, label, key and options describe
the suggestion itself, and id almost always means the record the API returned,
not the record the form is editing — filling it would overwrite the form's own
id. The autocomplete's own input is skipped too. Map any of them explicitly
with data-fill when you do want them.
Keeping the form honest: a property the item does not carry empties its
field, and typing again after a selection clears everything that selection
filled. A field never keeps showing data belonging to an item that is no longer
selected.
Rows may share a value. The row the user picks — by mouse or by
Arrow + Enter — is the row whose properties are filled, even when several rows
carry the same value (a sub-district name that exists in several provinces).
Still, give each row a distinct value when the API can: it is what the hidden
input submits.
Mapping different names, or turning it off — data-fill takes a list of
field:property pairs (a bare name means the field and the property share it),
or JSON. When data-fill is present, only what it lists is filled:
<!-- The item's `topic` fills the field named `equipment`; nothing else is filled -->
<input type="text" name="product_no" data-autocomplete="true" data-source="/api/products"
data-fill="equipment:topic">
<!-- Same thing in JSON -->
<input type="text" name="product_no" data-autocomplete="true" data-source="/api/products"
data-fill='{"equipment":"topic","price":"unit_price"}'>
<!-- Fill nothing -->
<input type="text" name="product_no" data-autocomplete="true" data-source="/api/products"
data-fill="false">What the Input Shows After a Choice
An item's text is the line shown in the dropdown while searching. Once an item is
chosen, the input shows the item's property named like the field — its name — so a
code field shows the code, not the whole search line. The hidden input still submits
value.
{"value": "C0001", "text": "C0001 Alpha Co. (Head office)", "customer_no": "C0001"}<!-- Shows "C0001" once chosen; the dropdown still lists the full text -->
<input type="text" name="customer_no" data-autocomplete="true"
data-source="api/inventory/customers/autocomplete?by=customer_no">- A property holding a
{value, text}pair shows itstext. - A
data-fillentry that targets the field itself names the property to show,
for an API whose key differs from the field name:
data-fill="course_code:code,course_name"shows the item'scode. - An item without such a property shows its
text, as before. - Text the user types without choosing anything stays exactly as typed, and is what
the hidden input submits.
To fill another autocomplete from a choice, send a {value, text} pair under that
field's name — a bare id would show the number in the visible input:
{"value": "C0001", "text": "C0001 Alpha Co.", "customer_id": {"value": "7", "text": "Alpha Co."}}Hierarchical Search
For address fields (province → district → subdistrict → zipcode):
<input type="text"
data-element="text"
data-autocomplete="true"
data-search-api="/api/address/search"
data-search-field="subdistrict"
name="subdistrict"
placeholder="Search subdistrict...">When user selects an item, all related address fields are populated automatically.
Hidden Input Support
When autocomplete is enabled, TextElementFactory creates a hidden input to store the key value:
<!-- Before enhancement -->
<input type="text" name="city" data-element="text" data-autocomplete="true" ...>
<!-- After enhancement -->
<input type="text" name="city_text" ...> <!-- Shows display text -->
<input type="hidden" name="city"> <!-- Stores key value -->HTML Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-element |
string | - | Set to text |
data-autocomplete |
boolean | false |
Enable autocomplete |
data-source |
string/JSON | - | Autocomplete source (URL, array, or global variable name) |
data-min-length |
number | 2 |
Minimum characters before search |
data-max-results |
number | 10 |
Maximum results to display |
data-delay |
number | 300 |
Debounce delay (ms) |
data-hierarchy |
string | - | Hierarchical level (province/district/subdistrict) |
data-dependent |
string | - | ID of parent field for dependent dropdowns |
data-depends |
string | - | Other fields of the form sent with the search, or used to filter a list: field or param:field, comma separated. See "Filtering by Other Fields" |
data-depends-clear |
boolean | false |
true = empty the input when a data-depends field changes |
data-callback |
string | - | Custom callback function name for rendering items |
data-fill |
string/JSON | - | Fields the selected item fills: field:property list, JSON map, or false to turn off. Omit it to fill by matching id/name. An entry naming the field itself picks the property the field shows |
data-search-api |
string | - | API endpoint for hierarchical search |
data-search-field |
string | - | Field name for hierarchical search |
data-validate |
string | - | Validation rules (email, url, alpha, etc.) |
data-formatter |
string | - | Formatter function name |
Built-in Validators
| Validator | Pattern | Description |
|---|---|---|
email |
^[^\s@]+@[^\s@]+\.[^\s@]+$ |
Valid email format |
url |
HTTP/HTTPS URL pattern | Valid URL |
number |
^-?\d*\.?\d+$ |
Numeric value |
integer |
^-?\d+$ |
Whole number |
alpha |
^[a-zA-Z]+$ |
Letters only |
alphanumeric |
^[a-zA-Z0-9]+$ |
Letters and numbers |
usernameOrEmail |
Combined pattern | Username or email |
JavaScript API
// Get instance
const cityInput = document.querySelector('[name="city"]');
const instance = ElementManager.getInstance(cityInput);
// Access values
console.log(instance.element.value); // Display text
console.log(instance.hiddenInput.value); // Key value
console.log(instance.selectedValue); // Currently selected key
console.log(instance.selectedItem); // The whole item that was selected, including its extra properties
// Programmatically update autocomplete options
instance.populate([
{value: '1', text: 'Option 1'},
{value: '2', text: 'Option 2'}
]);
// Show/hide dropdown
instance.show();
instance.hide();Custom Render Callback
// Define callback function
window.renderCityItem = function({key, value, search, level}) {
const div = document.createElement('div');
div.className = 'city-item';
div.innerHTML = `
<strong>${value}</strong>
<span class="code">${key}</span>
`;
return div;
};<input type="text"
data-element="text"
data-autocomplete="true"
data-source="/api/cities"
data-callback="renderCityItem"
name="city">PasswordElementFactory
PasswordElementFactory extends TextElementFactory with password-specific features: strength meter, toggle visibility, criteria list, and match validation.
Basic Usage
<!-- Password with toggle visibility -->
<input type="password"
data-element="password"
name="password"
minlength="8"
required>
<!-- Password with strength meter -->
<input type="password"
data-element="password"
data-password-strength="true"
name="password"
required>Password Strength Meter
<input type="password"
data-element="password"
data-password-strength="true"
id="password"
name="password">
<span id="result_password" class="comment">At least 8 characters</span>
<!-- Strength bar will be inserted after comment -->CSS Classes on Strength Bar:
.weak- Red (< 40% criteria met).medium- Yellow (40-80% criteria met).strong- Green (> 80% criteria met)
Password Criteria List
<input type="password"
data-element="password"
data-password-criteria-list="true"
name="password">Default Criteria:
- ✅ At least 8 characters
- ✅ At least one uppercase letter
- ✅ At least one lowercase letter
- ⬜ At least one number (disabled by default)
- ⬜ At least one special character (disabled by default)
Password Match Validation
<!-- Main password field -->
<input type="password"
data-element="password"
id="password"
name="password"
required>
<!-- Confirm password field -->
<input type="password"
data-element="password"
data-target-password="password"
name="password_confirm"
required>The confirm field will show valid/invalid state based on whether passwords match.
HTML Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-element |
string | - | Set to password |
data-password-strength |
boolean | false |
Show strength meter |
data-password-criteria-list |
boolean | false |
Show criteria checklist |
data-target-password |
string | - | ID of main password field (for confirm field) |
minlength |
number | 8 |
Minimum password length |
maxlength |
number | 50 |
Maximum password length |
Password Toggle Visibility
Password toggle is enabled by default. Users can:
- Click the eye icon to show/hide password
- Press
Alt + Skeyboard shortcut
CSS Classes:
.icon-published0- Password hidden (eye icon).icon-published1- Password visible (eye-slash icon)
JavaScript API
const instance = ElementManager.getInstance(passwordInput);
// Check if passwords match
const confirmInput = document.getElementById('password_confirm');
const matches = instance.checkMatch(confirmInput);
// Check specific criterion
instance.passesCriterion('minLength', 'myPassword123'); // true/false
instance.passesCriterion('uppercase', 'myPassword123'); // true
instance.passesCriterion('numbers', 'myPassword123'); // true
// Get all failed criteria
const failed = instance.getFailedCriteria('weak');
// ['At least 8 characters', 'At least one uppercase letter']
// Update UI manually
instance.updateStrengthBar();
instance.updateCriteriaList();
instance.updateMatchStatus();Customizing Criteria
// Override default criteria in create/enhance
PasswordElementFactory.enhance(element, {
passwordCriteria: {
minLength: {enabled: true, value: 12, label: 'At least {value} characters'},
uppercase: {enabled: true, label: 'At least one uppercase letter'},
lowercase: {enabled: true, label: 'At least one lowercase letter'},
numbers: {enabled: true, label: 'At least one number'},
special: {enabled: true, label: 'At least one special character (!@#$%^&*)'}
}
});SearchElementFactory
SearchElementFactory extends TextElementFactory with search-specific features: debounced input, clear button, results container, and FormManager integration.
Basic Usage
<!-- Simple search input -->
<input type="search"
data-element="search"
name="search"
placeholder="Search...">
<!-- Search with onSearch callback -->
<input type="search"
data-element="search"
id="product-search"
name="search"
placeholder="Search products...">
<script>
const searchInput = document.getElementById('product-search');
searchInput.addEventListener('input', async (e) => {
// Handle search with debounce built-in
});
</script>Search with Results
// Create search with custom onSearch handler
SearchElementFactory.enhance(searchInput, {
onSearch: async (query) => {
const response = await fetch(`/api/products?q=${query}`);
const data = await response.json();
return data.results; // [{value, text}, ...]
}
});The search runs data-search-delay ms after the last keystroke, so a burst of typing makes one request, and an answer
to an older query that arrives late is ignored. Result text is shown as text (escaped), with the matching part in
<em>. ArrowDown and ArrowUp move through the results, Enter picks one, and Escape closes the list. The results and the
clear button go into the field's wrapper (or its parent), so the input must already be in the page, or be created with
wrapper; a field created with wrapper gets the search class on it.
HTML Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-element |
string | - | Set to search |
data-search-delay |
number | 300 |
Debounce delay (ms) |
data-min-length |
number | 2 |
Minimum characters before search |
data-max-length |
number | 100 |
Maximum input length |
data-max-results |
number | 10 |
Maximum results to show |
data-highlight-matches |
boolean | true |
Highlight matching text |
Configuration Options
SearchElementFactory.config = {
type: 'search',
inputMode: 'search',
searchDelay: 300,
minLength: 2,
maxLength: 100,
placeholder: 'Search',
wrapperClass: 'search',
inputClass: 'search-input',
clearClass: 'search-clear',
loadingClass: 'search-loading',
resultsClass: 'search-results',
activeClass: 'active',
maxResults: 10,
highlightMatches: true,
formIntegration: true,
keyboard: {
selectKeys: ['Enter', ' '],
closeKeys: ['Escape'],
navigateUpKeys: ['ArrowUp'],
navigateDownKeys: ['ArrowDown']
}
};Clear Button
A clear button is automatically added and shown when the input has a value:
.search-clear {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
}Loading State
.search-loading {
position: relative;
}
.search-loading::after {
content: '';
position: absolute;
right: 40px;
top: 50%;
width: 16px;
height: 16px;
border: 2px solid #ccc;
border-top-color: #333;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}JavaScript API
const instance = ElementManager.getInstance(searchInput);
// Show results programmatically
instance.createSuggestionList([
{value: '1', text: 'Result 1'},
{value: '2', text: 'Result 2'}
]);
// Access clear button
searchInput.clearButton.click(); // Clear inputEvents
All text-based elements emit these events:
Common Events
| Event | When Emitted | Detail |
|---|---|---|
element:change |
Value changed | {value, valid} |
element:valid |
Validation passed | {value} |
element:invalid |
Validation failed | {value, error} |
input |
User types | Native event |
change |
Value confirmed | Native event |
focus |
Element focused | Native event |
blur |
Element blurred | Native event |
TextElementFactory Events
element.addEventListener('change', (e) => {
const instance = ElementManager.getInstance(e.target);
console.log('Display value:', e.target.value);
console.log('Selected key:', instance.hiddenInput?.value);
});PasswordElementFactory Events
// Listen to password changes with strength info
passwordInput.addEventListener('input', (e) => {
const instance = ElementManager.getInstance(e.target);
const failed = instance.getFailedCriteria(e.target.value);
console.log('Failed criteria:', failed);
});SearchElementFactory Events
// Now.js event system
Now.on('search:performed', ({elementId, query}) => {
console.log(`Search performed on ${elementId}: ${query}`);
});Best Practices
1. ✅ Use Hidden Input for Key-Value Pairs
<!-- ✅ Good: Autocomplete stores key in hidden input -->
<input type="text"
data-element="text"
data-autocomplete="true"
data-source="/api/cities"
name="city">
<!-- Form submits: city=123 (hidden input value, not display text) -->2. ✅ Set Appropriate Min Length for Autocomplete
<!-- ✅ Good: Wait for at least 2 characters -->
<input type="text"
data-element="text"
data-autocomplete="true"
data-source="/api/products"
data-min-length="2">
<!-- ❌ Bad: Triggers on every keystroke -->
<input type="text"
data-autocomplete="true"
data-source="/api/products"
data-min-length="0">3. ✅ Use Password Strength for Registration
<!-- ✅ Good: Show strength meter during registration -->
<form data-form="register">
<input type="password"
data-element="password"
data-password-strength="true"
data-password-criteria-list="true"
name="password">
</form>4. ✅ Match Passwords with data-target-password
<!-- ✅ Good: Use built-in match validation -->
<input type="password" id="password" name="password">
<input type="password"
data-target-password="password"
name="password_confirm">
<!-- ❌ Bad: Custom JavaScript for matching -->
<script>
confirmInput.addEventListener('input', () => {
if (passwordInput.value !== confirmInput.value) { ... }
});
</script>5. ✅ Debounce Search Input
<!-- ✅ Good: Built-in debounce with SearchElementFactory -->
<input type="search"
data-element="search"
data-search-delay="300">
<!-- ❌ Bad: No debounce on every keystroke -->
<input type="text" oninput="search(this.value)">6. ✅ Provide Clear Error Messages
<input type="text"
data-element="text"
data-validate="email"
data-error-email="Please enter a valid email address like example@domain.com"
name="email">Related Documentation
- Form Elements Overview
- ElementFactory - Base class
- FormManager - Form-level validation
- Number Elements - Number inputs
- Select Elements - Dropdown inputs