--- name: form-input description: 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 ```javascript 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: ```javascript _get_value() { return this.$sid('input').val(); } ``` ### _set_value(value) (REQUIRED) Set the value on DOM/component: ```javascript _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")`. ```javascript _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: ```javascript on_ready() { this._mark_ready(); // REQUIRED - apply buffered value // ... setup event handlers } ``` For async initialization, call `_mark_ready()` when actually ready: ```javascript 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: ```javascript // 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: ```javascript 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 ```javascript 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 ```javascript // 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()`: ```jqhtml <%-- CORRECT --%> <%-- WRONG - never set value in template --%> ``` **Note**: Input components have NO `on_load()`, so `this.data` is always `{}`. --- ## Value Transformation Example For inputs that need to convert values (like Checkbox): ```javascript 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: ```javascript 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 documentation - `rsx/theme/components/inputs/CLAUDE.md` - Implementation details