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. $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_Ajax_Input Dropdown that loads options via Ajax. Checkbox_Input Boolean checkbox input. 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:
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