Add skills documentation and misc updates
Add form value persistence across cache revalidation re-renders 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
284
docs/skills/forms/SKILL.md
Executable file
284
docs/skills/forms/SKILL.md
Executable file
@@ -0,0 +1,284 @@
|
||||
---
|
||||
name: forms
|
||||
description: Building RSX forms with Rsx_Form, Form_Field, input components, data binding, validation, and the vals() pattern. Use when creating forms, handling form submissions, implementing form validation, working with Form_Field or Form_Input components, or implementing polymorphic form fields.
|
||||
---
|
||||
|
||||
# RSX Form Components
|
||||
|
||||
## Core Form Structure
|
||||
|
||||
Forms use `<Rsx_Form>` with automatic data binding:
|
||||
|
||||
```jqhtml
|
||||
<Rsx_Form $data="<%= JSON.stringify(this.data.form_data) %>"
|
||||
$controller="Controller" $method="save">
|
||||
<Form_Field $name="email" $label="Email" $required=true>
|
||||
<Text_Input $type="email" />
|
||||
</Form_Field>
|
||||
|
||||
<Form_Hidden_Field $name="id" />
|
||||
</Rsx_Form>
|
||||
```
|
||||
|
||||
## Field Components
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `Form_Field` | Standard formatted field with label, errors, help text |
|
||||
| `Form_Hidden_Field` | Single-tag hidden input (extends Form_Field_Abstract) |
|
||||
| `Form_Field_Abstract` | Base class for custom formatting (advanced) |
|
||||
|
||||
## Input Components
|
||||
|
||||
| Component | Usage |
|
||||
|-----------|-------|
|
||||
| `Text_Input` | Text, email, url, tel, number, textarea |
|
||||
| `Select_Input` | Dropdown with options array |
|
||||
| `Checkbox_Input` | Checkbox with optional label |
|
||||
| `Radio_Input` | Radio button group |
|
||||
| `Wysiwyg_Input` | Rich text editor (Quill) |
|
||||
|
||||
### Text_Input Attributes
|
||||
|
||||
```jqhtml
|
||||
<Text_Input $type="email" $placeholder="user@example.com" />
|
||||
<Text_Input $type="textarea" $rows="5" />
|
||||
<Text_Input $type="number" $min="0" $max="100" />
|
||||
<Text_Input $prefix="@" $placeholder="username" />
|
||||
<Text_Input $maxlength="100" />
|
||||
```
|
||||
|
||||
### Select_Input Formats
|
||||
|
||||
```jqhtml
|
||||
<%-- Simple array --%>
|
||||
<Select_Input $options="<%= JSON.stringify(['Option 1', 'Option 2']) %>" />
|
||||
|
||||
<%-- Value/label objects --%>
|
||||
<Select_Input $options="<%= JSON.stringify([
|
||||
{value: 'opt1', label: 'Option 1'},
|
||||
{value: 'opt2', label: 'Option 2'}
|
||||
]) %>" />
|
||||
|
||||
<%-- From model enum --%>
|
||||
<Select_Input $options="<%= JSON.stringify(Project_Model.status_id__enum_select()) %>" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Disabled Fields
|
||||
|
||||
Use `$disabled=true` on input components. Unlike standard HTML, disabled fields still return values via `vals()` (useful for read-only data that should be submitted).
|
||||
|
||||
```jqhtml
|
||||
<Text_Input $type="email" $disabled=true />
|
||||
<Select_Input $options="..." $disabled=true />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Column Layouts
|
||||
|
||||
Use Bootstrap grid for multi-column field layouts:
|
||||
|
||||
```jqhtml
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<Form_Field $name="first_name" $label="First Name">
|
||||
<Text_Input />
|
||||
</Form_Field>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Form_Field $name="last_name" $label="Last Name">
|
||||
<Text_Input />
|
||||
</Form_Field>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The vals() Dual-Mode Pattern
|
||||
|
||||
Form components implement `vals()` for get/set:
|
||||
|
||||
```javascript
|
||||
class My_Form extends Component {
|
||||
vals(values) {
|
||||
if (values) {
|
||||
// Setter - populate form
|
||||
this.$sid('name').val(values.name || '');
|
||||
return null;
|
||||
} else {
|
||||
// Getter - extract values
|
||||
return {name: this.$sid('name').val()};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Form Validation
|
||||
|
||||
Apply server-side validation errors:
|
||||
|
||||
```javascript
|
||||
const response = await Controller.save(form.vals());
|
||||
if (response.errors) {
|
||||
Form_Utils.apply_form_errors(form.$, response.errors);
|
||||
}
|
||||
```
|
||||
|
||||
Errors match by `name` attribute on form fields.
|
||||
|
||||
---
|
||||
|
||||
## Action/Controller Pattern
|
||||
|
||||
Forms follow load/save mirroring traditional Laravel:
|
||||
|
||||
**Action (loads data):**
|
||||
```javascript
|
||||
on_create() {
|
||||
this.data.form_data = { title: '', status_id: Model.STATUS_ACTIVE };
|
||||
this.data.is_edit = !!this.args.id;
|
||||
}
|
||||
async on_load() {
|
||||
if (!this.data.is_edit) return;
|
||||
const record = await My_Model.fetch(this.args.id);
|
||||
this.data.form_data = { id: record.id, title: record.title };
|
||||
}
|
||||
```
|
||||
|
||||
**Controller (saves data):**
|
||||
```php
|
||||
#[Ajax_Endpoint]
|
||||
public static function save(Request $request, array $params = []) {
|
||||
if (empty($params['title'])) {
|
||||
return response_form_error('Validation failed', ['title' => 'Required']);
|
||||
}
|
||||
$record = $params['id'] ? My_Model::find($params['id']) : new My_Model();
|
||||
$record->title = $params['title'];
|
||||
$record->save();
|
||||
return ['redirect' => Rsx::Route('View_Action', $record->id)];
|
||||
}
|
||||
```
|
||||
|
||||
**Key principles:**
|
||||
- `form_data` must be serializable (plain objects, no models)
|
||||
- Keep load/save in same controller for field alignment
|
||||
- `on_load()` loads data, `on_ready()` is UI-only
|
||||
|
||||
---
|
||||
|
||||
## Repeater Fields
|
||||
|
||||
For arrays of values (relationships, multiple items):
|
||||
|
||||
**Simple repeaters (array of IDs):**
|
||||
```javascript
|
||||
// form_data
|
||||
this.data.form_data = {
|
||||
client_ids: [1, 5, 12],
|
||||
};
|
||||
|
||||
// Controller receives
|
||||
$params['client_ids'] // [1, 5, 12]
|
||||
|
||||
// Sync
|
||||
$project->clients()->sync($params['client_ids'] ?? []);
|
||||
```
|
||||
|
||||
**Complex repeaters (array of objects):**
|
||||
```javascript
|
||||
// form_data
|
||||
this.data.form_data = {
|
||||
team_members: [
|
||||
{user_id: 1, role_id: 2},
|
||||
{user_id: 5, role_id: 1},
|
||||
],
|
||||
};
|
||||
|
||||
// Controller receives
|
||||
$params['team_members'] // [{user_id: 1, role_id: 2}, ...]
|
||||
|
||||
// Sync with pivot data
|
||||
$project->team()->detach();
|
||||
foreach ($params['team_members'] ?? [] as $member) {
|
||||
$project->team()->attach($member['user_id'], [
|
||||
'role_id' => $member['role_id'],
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Data (Debug Mode)
|
||||
|
||||
Widgets can implement `seed()` for debug mode test data. Rsx_Form displays "Fill Test Data" button when `window.rsxapp.debug` is true.
|
||||
|
||||
```jqhtml
|
||||
<Text_Input $seeder="company_name" />
|
||||
<Text_Input $seeder="email" />
|
||||
<Text_Input $seeder="phone" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating Custom Input Components
|
||||
|
||||
Extend `Form_Input_Abstract`:
|
||||
|
||||
```javascript
|
||||
class My_Custom_Input extends Form_Input_Abstract {
|
||||
on_create() {
|
||||
// NO on_load() - never use this.data
|
||||
}
|
||||
|
||||
on_ready() {
|
||||
// Render elements EMPTY - form calls val(value) to populate AFTER render
|
||||
}
|
||||
|
||||
// Required: get/set value
|
||||
val(value) {
|
||||
if (value !== undefined) {
|
||||
// Set value
|
||||
this.$sid('input').val(value);
|
||||
} else {
|
||||
// Get value
|
||||
return this.$sid('input').val();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference implementations: `Select_Input`, `Text_Input`, `Checkbox_Input`
|
||||
|
||||
---
|
||||
|
||||
## Polymorphic Form Fields
|
||||
|
||||
For fields that can reference multiple model types:
|
||||
|
||||
```php
|
||||
use App\RSpade\Core\Polymorphic_Field_Helper;
|
||||
|
||||
$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
|
||||
Contact_Model::class,
|
||||
Project_Model::class,
|
||||
]);
|
||||
|
||||
if ($error = $eventable->validate('Please select an entity')) {
|
||||
$errors['eventable'] = $error;
|
||||
}
|
||||
|
||||
$model->eventable_type = $eventable->model;
|
||||
$model->eventable_id = $eventable->id;
|
||||
```
|
||||
|
||||
Client submits: `{"model":"Contact_Model","id":123}`. Always use `Model::class` for the whitelist.
|
||||
|
||||
## More Information
|
||||
|
||||
Details: `php artisan rsx:man form_conventions`, `php artisan rsx:man forms_and_widgets`
|
||||
Reference in New Issue
Block a user