Files
rspade_system/app/RSpade/man/form_conventions.txt
root 21a7149486 Switch Ajax transport to JSON with proper Content-Type
Migrate $name from Form_Field to input components
Refactor form inputs: $name moves from Form_Field to input components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 09:43:23 +00:00

360 lines
11 KiB
Plaintext
Executable File

FORM_CONVENTIONS(3) RSX Framework Manual FORM_CONVENTIONS(3)
NAME
Form Conventions - Architectural patterns for SPA form loading and saving
SYNOPSIS
// Action: Load form data
async on_load() {
const record = await My_Model.fetch(this.args.id);
this.data.form_data = {
id: record.id,
title: record.title,
status_id: record.status_id,
};
}
// Template: Bind form data
<Rsx_Form $data=this.data.form_data $controller="My_Controller" $method="save">
// Controller: Single save endpoint
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
// Validate, save, return redirect
}
DESCRIPTION
The RSX form system follows a traditional server-side form pattern adapted
for SPA applications. The key principle is: one endpoint for loading form
state, one endpoint for validating and saving. All data flows through a
single serializable form_data object.
This approach mirrors how forms work in traditional Laravel Blade
applications:
- Controller::edit() loads data and passes to view
- Controller::update() validates and saves
- Both methods live in the same controller, making it easy to verify
field alignment between load and save
In RSX SPA, this becomes:
- Action::on_load() fetches data into this.data.form_data
- Controller::save() receives form values and persists them
- Both reference the same field names, ensuring alignment
DATA FLOW
LOADING:
1. Action.on_load() fetches data from Model or Controller
2. Assigns structured object to this.data.form_data
3. Template passes form_data to <Rsx_Form $data=...>
4. Rsx_Form.on_ready() calls vals() to distribute values
5. Each widget receives its value via val(value) setter
SUBMITTING:
1. User clicks submit button
2. Rsx_Form.submit() calls vals() to gather all widget values
3. Ajax POST sends {field: value, ...} to Controller::method
4. Controller validates, saves, returns response
5. On success with redirect, Rsx_Form navigates to result.redirect
6. On validation error, Rsx_Form displays errors on fields
ACTION RESPONSIBILITIES
The SPA Action is responsible for loading all data needed by the form.
on_create() - Set Defaults:
on_create() {
// 1. Default form_data for "add" mode
this.data.form_data = {
title: '',
status_id: Project_Model.STATUS_ACTIVE,
team_members: [], // Repeater fields start as empty arrays
};
// 2. Dropdown options (from model enums or static lists)
this.data.status_options = Project_Model.status_id_enum_select();
// 3. Mode detection
this.data.is_edit = !!this.args.id;
}
on_load() - Fetch Data:
async on_load() {
if (!this.data.is_edit) return; // Add mode uses defaults
// Option A: Simple forms - use Model.fetch()
const record = await Project_Model.fetch(this.args.id);
this.data.form_data = {
id: record.id,
title: record.title,
status_id: record.status_id,
};
// Option B: Complex forms - use controller endpoint
// this.data.form_data = await Project_Controller.get_form_data({
// id: this.args.id
// });
}
IMPORTANT: on_ready() is for UI interactions only (show/hide, focus).
Never load data in on_ready() - it runs after vals() has already
distributed values to widgets.
TEMPLATE PATTERN
Standard form template structure:
<Rsx_Form $data=this.data.form_data
$controller="Project_Controller"
$method="save">
<% if (this.data.is_edit) { %>
<Form_Hidden_Field $name="id" />
<% } %>
<Form_Field $label="Title" $required=true>
<Text_Input $name="title" />
</Form_Field>
<Form_Field $label="Status">
<Select_Input $name="status_id" $options=this.data.status_options />
</Form_Field>
<Form_Field $label="Team">
<Form_Repeater $name="team_members"
$edit_input="Team_Member_Edit"
$display_input="Team_Member_Display"
/>
</Form_Field>
<button type="button" class="btn btn-primary"
@click=this.sid('form').submit()>
Save
</button>
</Rsx_Form>
Key points:
- $data receives the form_data object from Action
- Field $name attributes match keys in form_data
- Hidden field for ID only shown in edit mode
- Repeater field names match array keys in form_data
CONTROLLER RESPONSIBILITIES
Load Endpoint (Optional):
For complex forms with relationships, provide a dedicated load endpoint:
#[Ajax_Endpoint]
public static function get_form_data(Request $request, array $params = [])
{
$project = Project_Model::find($params['id']);
return [
'id' => $project->id,
'title' => $project->title,
'status_id' => $project->status_id,
// Repeater data as arrays
'team_members' => $project->team->map(fn($t) => [
'user_id' => $t->user_id,
'role_id' => $t->role_id,
])->toArray(),
];
}
This keeps the Action simple and ensures server controls the data shape.
Save Endpoint:
#[Ajax_Endpoint]
public static function save(Request $request, array $params = [])
{
// 1. VALIDATE
$errors = [];
if (empty($params['title'])) {
$errors['title'] = 'Title is required';
}
if (!empty($errors)) {
return response_form_error('Please correct errors', $errors);
}
// 2. SAVE MAIN RECORD
$project = !empty($params['id'])
? Project_Model::find($params['id'])
: new Project_Model();
$project->title = $params['title'];
$project->status_id = $params['status_id'];
$project->save();
// 3. SYNC RELATIONSHIPS
// Repeater sends array: [{user_id: 1, role_id: 2}, ...]
$project->team()->detach();
foreach ($params['team_members'] ?? [] as $member) {
$project->team()->attach($member['user_id'], [
'role_id' => $member['role_id'],
]);
}
// 4. RETURN REDIRECT
Flash_Alert::success('Saved successfully');
return ['redirect' => Rsx::Route('Project_View_Action', $project->id)];
}
FIELD ALIGNMENT
The critical benefit of this pattern is ensuring field alignment between
load and save. Keep both endpoints in the same controller file:
// Controller file shows the complete data contract
class Project_Controller extends Rsx_Controller_Abstract
{
// LOAD: These fields go INTO the form
public static function get_form_data(...) {
return [
'id' => ...,
'title' => ...,
'status_id' => ...,
'team_members' => [...],
];
}
// SAVE: These same fields come BACK from the form
public static function save(...) {
$params['id'];
$params['title'];
$params['status_id'];
$params['team_members'];
}
}
When adding a new field:
1. Add to get_form_data() return array
2. Add to save() parameter handling
3. Add to template Form_Field
4. Add to on_create() defaults (for add mode)
All four locations are easily verified by searching the field name.
REPEATER FIELDS
Repeater fields collect multiple values into an array.
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'],
]);
}
KEY PRINCIPLES
1. SERIALIZABLE STATE
form_data must be JSON-serializable. No model instances, no
functions, no circular references. Plain objects and arrays only.
2. SINGLE SUBMIT
One Ajax call sends all form data. No per-field saves, no partial
updates. The form is atomic - it succeeds or fails as a unit.
3. SERVER SYNCS RELATIONSHIPS
Controller receives arrays from repeaters and syncs them server-side.
Delete removed items, add new items, update changed items.
4. ACTION LOADS, FORM DISTRIBUTES
Action's on_load() prepares form_data as a plain object.
Rsx_Form's on_ready() distributes values to widgets via vals().
Widgets never know where data came from.
5. CONTROLLER ALIGNMENT
Keep get_form_data() and save() in the same file. This makes it
trivial to verify field names match between load and save.
VALIDATION RESPONSES
Success with redirect:
return ['redirect' => Rsx::Route('View_Action', $id)];
Validation errors:
return response_form_error('Please correct errors', [
'title' => 'Title is required',
'email' => 'Invalid email format',
]);
Rsx_Form automatically displays field errors below each Form_Field.
COMMON MISTAKES
Loading data in on_ready():
// WRONG - on_ready() runs AFTER form already populated
on_ready() {
this.data.form_data = await Controller.get_data();
}
// CORRECT - on_load() runs BEFORE render
async on_load() {
this.data.form_data = await Controller.get_data();
}
Non-serializable form_data:
// WRONG - Model instance is not serializable
this.data.form_data = await Project_Model.fetch(id);
// CORRECT - Extract plain object
const record = await Project_Model.fetch(id);
this.data.form_data = {
id: record.id,
title: record.title,
};
Field name mismatch:
// form_data uses 'name'
this.data.form_data = { name: 'Test' };
// Template uses 'title' - VALUE WILL BE EMPTY
<Form_Field $label="Title">
<Text_Input $name="title" />
SEE ALSO
forms_and_widgets(3), jqhtml(3), ajax(3)
RSX Framework December 2024 FORM_CONVENTIONS(3)