🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
8.0 KiB
Executable File
name, description
| name | description |
|---|---|
| form-input | Creating custom form input components that extend Form_Input_Abstract. Use when building custom input widgets, implementing _get_value()/_set_value(), handling input events, or integrating inputs with Rsx_Form. |
Form Input Component Contract
Overview
Form input components extend Form_Input_Abstract which implements a template method pattern. The base class handles value buffering, events, and the val() interface. Concrete classes only implement how to get and set values.
Core Contract: Timing Indifference
Callers must never care about component lifecycle timing.
.val(value) must produce identical results whether called:
- Before the component initializes (buffered, applied at
_mark_ready()) - After the component initializes (applied immediately via
_set_value()) - At any point during the component's lifecycle
This is non-negotiable. If a caller needs to use await component.ready() or timing tricks to make val() work, the component is broken.
Implementation requirement: Your _set_value() must work identically whether called during initial value application (via _mark_ready()) or later (via direct val() calls). If your implementation relies on initialization state or timing, you've violated this contract.
Template Method Pattern
Base class handles:
- Pre-initialization value buffering (
_pending_value,_is_ready) - Complete
val()getter/setter logic - Automatic
trigger('val', value)on all setter calls - Applying buffered values via
_mark_ready()
Concrete classes implement:
_get_value()- Return current DOM/component value (REQUIRED)_set_value(value)- Set DOM/component value (REQUIRED)_transform_value(value)- Transform buffered value for getter (OPTIONAL)on_ready()- Call_mark_ready()and setup user interaction events
Minimal Implementation
class My_Custom_Input extends Form_Input_Abstract {
_get_value() {
return this.$sid('input').val();
}
_set_value(value) {
this.$sid('input').val(value || '');
}
on_ready() {
this._mark_ready();
const that = this;
this.$sid('input').on('input', function() {
const value = that.val();
that.trigger('input', value);
that.trigger('val', value);
});
}
}
That's it. No on_create(), no val() method, no buffering logic.
Key Methods
_get_value() (REQUIRED)
Return the current value from DOM/component:
_get_value() {
return this.$sid('input').val();
}
_set_value(value) (REQUIRED)
Set the value on DOM/component:
_set_value(value) {
this.$sid('input').val(value || '');
}
String/Number Value Coercion
Input components must accept both string and numeric values. A select with options "3", "5", "9" must work with .val(3) or .val("3").
_set_value(value) {
// Convert to string for comparison with option values
const str_value = value == null ? '' : str(value);
this.tom_select.setValue(str_value, true);
}
_mark_ready()
Call in on_ready() to apply buffered values and mark component ready:
on_ready() {
this._mark_ready(); // REQUIRED - apply buffered value
// ... setup event handlers
}
For async initialization, call _mark_ready() when actually ready:
on_ready() {
const that = this;
quill_ready(function() {
that._initialize_quill();
that._mark_ready(); // Called AFTER quill is ready
});
}
_transform_value(value) (OPTIONAL)
Transform buffered value for getter return. Used when value needs conversion:
// Example: Checkbox converts to checked_value/unchecked_value
_transform_value(value) {
const should_check = (value === this.checked_value || value === '1' || value === 1);
return should_check ? this.checked_value : this.unchecked_value;
}
Event Triggers
On user interaction, trigger BOTH events:
this.$sid('input').on('input', function() {
const value = that.val();
that.trigger('input', value); // User interaction only
that.trigger('val', value); // All changes
});
input event: Fires on user interaction ONLY
val event: Fires on ALL value changes (base class handles setter, you handle user interaction)
Event Usage Patterns
React to User Changes Only
this.sid('country').on('input', (component, value) => {
// Only fires when user selects, not when form populates
this.reload_states(value);
});
Get Current Value + All Future Changes
// Because jqhtml triggers already-fired events on late .on() registration,
// this callback fires IMMEDIATELY with current value, then on every change
this.sid('amount').on('val', (component, value) => {
this.update_total(value);
});
Template Requirements
Templates render elements EMPTY. Forms populate via val():
<%-- CORRECT --%>
<Define:My_Input>
<input type="text" $sid="input" class="form-control" />
</Define:My_Input>
<%-- WRONG - never set value in template --%>
<Define:My_Input>
<input type="text" $sid="input" value="<%= this.data.value %>" />
</Define:My_Input>
Note: Input components have NO on_load(), so this.data is always {}.
Value Transformation Example
For inputs that need to convert values (like Checkbox):
class Checkbox_Input extends Form_Input_Abstract {
on_create() {
super.on_create();
this.checked_value = this.args.checked_value || '1';
this.unchecked_value = this.args.unchecked_value || '0';
}
_get_value() {
return this.$sid('input').prop('checked')
? this.checked_value
: this.unchecked_value;
}
_set_value(value) {
const should_check = (value === this.checked_value ||
value === '1' || value === 1 || value === true);
this.$sid('input').prop('checked', should_check);
}
_transform_value(value) {
const should_check = (value === this.checked_value ||
value === '1' || value === 1 || value === true);
return should_check ? this.checked_value : this.unchecked_value;
}
on_ready() {
this._mark_ready();
const that = this;
this.$sid('input').on('change', function() {
const value = that.val();
that.trigger('input', value);
that.trigger('val', value);
});
}
}
Extending Other Inputs
When extending another input:
class Select_Ajax_Input extends Select_Input {
async on_load() {
this.data.select_values = await Controller.get_options();
}
on_ready() {
// Parent sets up TomSelect and calls _mark_ready()
super.on_ready();
}
// _get_value() and _set_value() inherited from Select_Input
}
Common Mistakes
| Mistake | Correct Approach |
|---|---|
Implementing val() |
Use _get_value() and _set_value() instead |
Forgetting _mark_ready() |
Must call in on_ready() to apply buffered values |
Using this.data |
Inputs have no on_load(), use val() only |
Adding class="Widget" |
Not needed, .Form_Input_Abstract is automatic |
| Not triggering both events | User interaction must trigger both 'input' and 'val' |
Calling _mark_ready() too early |
For async init, wait until actually ready |
| Setting value in template | Templates render empty, forms populate via val() |
Built-in Reference Implementations
| Component | Notes |
|---|---|
text/text_input.js |
Simple text input - minimal implementation |
select/select_input.js |
Dropdown with TomSelect |
checkbox/checkbox_input.js |
Uses _transform_value() |
wysiwyg/wysiwyg_input.js |
Async initialization |
More Information
php artisan rsx:man form_input- Complete documentationrsx/theme/components/inputs/CLAUDE.md- Implementation details