Files
rspade_system/app/RSpade/man/form_input.txt
2025-12-29 10:48:10 +00:00

360 lines
12 KiB
Plaintext
Executable File

FORM_INPUT(3) RSX Framework Manual FORM_INPUT(3)
NAME
form_input - Form input component contract and implementation
SYNOPSIS
// Creating a custom input component (template method pattern)
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);
});
}
}
DESCRIPTION
Form input components are the building blocks of RSX forms. They provide
a consistent interface for getting and setting values, handling user
interaction, and integrating with the form system.
The base class (Form_Input_Abstract) implements a template method pattern
that handles all common logic:
- Pre-initialization value buffering
- val() getter/setter implementation
- Automatic trigger('val') on value changes
- Applying buffered values when ready
Concrete input classes only need to implement:
- _get_value() - How to read the current value
- _set_value(value) - How to write a value
- on_ready() - Call _mark_ready() and setup event handlers
Key difference from Laravel:
- Laravel: Form inputs are standard HTML elements, values read via request
- RSX: Form inputs are components with rich behavior and event system
Unlike regular JQHTML components:
- Input components have NO on_load() method
- Input components do not use this.data (it's always empty)
- Values are managed via the base class val() method
- Templates render EMPTY elements (no value attributes)
TEMPLATE METHOD PATTERN
The base class handles the complex logic, concrete classes provide simple
hooks for getting and setting values.
Base Class Provides:
on_create() Initializes _pending_value and _is_ready
val() Full getter/setter with buffering and events
_mark_ready() Apply buffered value, mark component ready
_transform_value() Default pass-through (override if needed)
Concrete Class Implements:
_get_value() Return current DOM/component value (REQUIRED)
_set_value(value) Set DOM/component value (REQUIRED)
_transform_value() Transform buffered value for getter (OPTIONAL)
on_ready() Call _mark_ready(), setup events (REQUIRED)
Benefits:
- Cannot forget trigger('val') - base class handles it
- Cannot forget buffering - base class handles it
- Reduced boilerplate: ~10 lines instead of ~40
- Consistent behavior across all inputs
MINIMAL IMPLEMENTATION
class My_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.
VALUE TRANSFORMATION
Some inputs need to transform the buffered value when returning it.
Override _transform_value() for this:
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();
// setup change handler...
}
}
ASYNC INITIALIZATION
Some components initialize asynchronously (e.g., waiting for external
libraries). Call _mark_ready() when actually ready, not at the start
of on_ready().
class Wysiwyg_Input extends Form_Input_Abstract {
_get_value() {
if (!this.quill) return '';
return safe_html(this.quill.root.innerHTML);
}
_set_value(value) {
if (value) {
this.quill.root.innerHTML = value;
}
}
_transform_value(value) {
return safe_html(value);
}
on_ready() {
const that = this;
quill_ready(function() {
that._initialize_quill();
that._mark_ready(); // Called AFTER quill is ready
});
}
}
EVENT SYSTEM
Two events serve different purposes:
'input' Event:
Fires only on user interaction. Use for dependent fields, live search,
or any logic that should respond to user changes but not programmatic
population.
this.sid('country').on('input', (component, value) => {
this.reload_states(value);
});
'val' Event:
Fires on ALL value changes. Because jqhtml triggers already-fired
events when .on() is registered late, this provides immediate callback
with the current value plus all future changes.
this.sid('amount').on('val', (component, value) => {
// Fires immediately with current value
// Then fires on every subsequent change
this.update_total(value);
});
Event Triggering:
- Base class val() setter automatically triggers 'val'
- Concrete classes trigger BOTH 'input' and 'val' on user interaction
- Never trigger 'input' from the val() setter
PRE-INITIALIZATION
Why Required:
Forms call vals() during on_ready() to populate all inputs with data.
Child input components may still be loading (e.g., Select_Input
fetching options via Ajax). The base class buffers the value and
applies it when _mark_ready() is called.
Example Scenario:
1. Form on_ready() calls this.vals(data)
2. Select_Input is still loading options from server
3. Form sets val('123') on the Select_Input
4. Base class buffers '123' in _pending_value
5. Select_Input on_ready() calls _mark_ready()
6. Base class applies buffered '123' via _set_value()
The base class handles all this automatically. Concrete classes just
need to implement _get_value() and _set_value().
BUILT-IN INPUTS
Text_Input
Text, email, url, tel, number, textarea inputs.
REQUIRES $max_length argument for character limits.
<Text_Input $name="email" $max_length=User_Model.field_length('email') />
<Text_Input $name="notes" $type="textarea" $max_length=-1 $rows="5" />
$max_length values:
- Positive number: Sets HTML maxlength attribute
- -1: Unlimited (no maxlength applied)
- Undefined: Console error with guidance
Use Model.field_length('column') for database-driven limits.
Subclasses (Phone_Text_Input, Currency_Input) are exempt.
Select_Input
Dropdown with static options.
<Select_Input $options="<%= JSON.stringify(options) %>" />
Select_Ajax_Input
Dropdown that loads options via Ajax.
<Select_Ajax_Input $controller="Controller" $method="get_options" />
Checkbox_Input
Boolean checkbox input.
<Checkbox_Input $label="I agree to terms" />
Checkbox_Multiselect
Multiple checkbox selection as array value.
Wysiwyg_Input
Rich text editor (Quill-based).
EXTENDING OTHER INPUTS
When extending another input (e.g., Select_Ajax_Input extends Select_Input),
call super methods appropriately:
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
}
CREATING CUSTOM INPUTS
1. Create JavaScript class extending Form_Input_Abstract
2. Create jqhtml template (NO class="Widget" needed)
3. Implement _get_value() and _set_value()
4. Implement on_ready() calling _mark_ready() and setting up events
Template Example:
<Define:My_Custom_Input>
<div class="my-input-wrapper">
<input type="text" $sid="input" class="form-control" />
</div>
</Define:My_Custom_Input>
JavaScript Example:
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 val = that.val();
that.trigger('input', val);
that.trigger('val', val);
});
}
}
COMMON MISTAKES
Implementing val():
Don't override val(). Implement _get_value() and _set_value() instead.
The base class handles all the buffering and event logic.
Forgetting _mark_ready():
Must call in on_ready() to apply buffered values and mark ready.
Calling _mark_ready() too early:
For async initialization, wait until actually ready before calling.
Using this.data:
Input components have no on_load(), so this.data is always {}.
Never use this.data.value or similar patterns.
Adding class="Widget":
No longer required. The .Form_Input_Abstract class is automatic.
Not triggering both events:
User interaction must trigger both 'input' and 'val'.
Setting value in template:
Templates must render empty. Forms populate via val().
TROUBLESHOOTING
Value Not Appearing:
- Check _set_value() actually sets DOM element
- Verify _mark_ready() is called in on_ready()
- Confirm template has $sid="input" or equivalent
Events Not Firing:
- Verify on_ready() sets up DOM event listeners
- Confirm both 'input' and 'val' triggered on user interaction
- Base class handles trigger('val') in setter automatically
Form Not Finding Input:
- Input must extend Form_Input_Abstract
- Check for typos in class name
- Verify component is inside Rsx_Form
Pre-Init Values Lost:
- Verify _mark_ready() is called in on_ready()
- For async init, call _mark_ready() after initialization completes
- Check _get_value() returns correct value
SEE ALSO
jqhtml(3) - Component lifecycle and templates
form_conventions(3) - Form patterns and data flow
spa(3) - SPA action forms
rsx/theme/components/inputs/CLAUDE.md - Implementation details