Files
rspade_system/docs/skills/forms/SKILL.md
root 1b46c5270c 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>
2025-12-29 04:38:06 +00:00

6.8 KiB
Executable File

name, description
name description
forms 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:

<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

<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

<%-- 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).

<Text_Input $type="email" $disabled=true />
<Select_Input $options="..." $disabled=true />

Multi-Column Layouts

Use Bootstrap grid for multi-column field layouts:

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

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:

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

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

#[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):

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

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

<Text_Input $seeder="company_name" />
<Text_Input $seeder="email" />
<Text_Input $seeder="phone" />

Creating Custom Input Components

Extend Form_Input_Abstract:

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:

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