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>
360 lines
11 KiB
Plaintext
Executable File
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)
|