Implement template method pattern for Form_Input_Abstract 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
350 lines
11 KiB
Plaintext
Executable File
350 lines
11 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.
|
|
<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
|