Rename Checkbox_Multiselect to Checkbox_Multiselect_Input
Implement template method pattern for Form_Input_Abstract 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
349
app/RSpade/man/form_input.txt
Executable file
349
app/RSpade/man/form_input.txt
Executable file
@@ -0,0 +1,349 @@
|
||||
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.
|
||||
<Text_Input $type="email" $placeholder="user@example.com" />
|
||||
<Text_Input $type="textarea" $rows="5" />
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user