Files
rspade_system/docs/skills/form-input/SKILL.md
root 88aa57d591 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>
2025-12-29 07:05:46 +00:00

6.9 KiB
Executable File

name, description
name description
form-input 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.

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

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:

_get_value() {
    return this.$sid('input').val();
}

_set_value(value) (REQUIRED)

Set the value on DOM/component:

_set_value(value) {
    this.$sid('input').val(value || '');
}

_mark_ready()

Call in on_ready() to apply buffered values and mark component ready:

on_ready() {
    this._mark_ready();  // REQUIRED - apply buffered value
    // ... setup event handlers
}

For async initialization, call _mark_ready() when actually ready:

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:

// 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:

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

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

// 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():

<%-- CORRECT --%>
<Define:My_Input>
  <input type="text" $sid="input" class="form-control" />
</Define:My_Input>

<%-- WRONG - never set value in template --%>
<Define:My_Input>
  <input type="text" $sid="input" value="<%= this.data.value %>" />
</Define:My_Input>

Note: Input components have NO on_load(), so this.data is always {}.


Value Transformation Example

For inputs that need to convert values (like Checkbox):

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:

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