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:
268
docs/skills/form-input/SKILL.md
Executable file
268
docs/skills/form-input/SKILL.md
Executable file
@@ -0,0 +1,268 @@
|
||||
---
|
||||
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.
|
||||
|
||||
## 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 || '');
|
||||
}
|
||||
```
|
||||
|
||||
### _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 --%>
|
||||
<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):
|
||||
|
||||
```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
|
||||
Reference in New Issue
Block a user