diff --git a/app/RSpade/man/form_conventions.txt b/app/RSpade/man/form_conventions.txt new file mode 100755 index 000000000..eac65d8de --- /dev/null +++ b/app/RSpade/man/form_conventions.txt @@ -0,0 +1,358 @@ +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 + + + // 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 + 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: + + + + <% if (this.data.is_edit) { %> + + <% } %> + + + + + + + + + + + + + + + + + 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 + + +SEE ALSO + forms_and_widgets(3), jqhtml(3), ajax(3) + +RSX Framework December 2024 FORM_CONVENTIONS(3) diff --git a/docs/CLAUDE.dist.md b/docs/CLAUDE.dist.md index fea8cd766..59b7cc034 100644 --- a/docs/CLAUDE.dist.md +++ b/docs/CLAUDE.dist.md @@ -891,6 +891,39 @@ class My_Form extends Component { **Validation**: `Form_Utils.apply_form_errors(form.$, errors)` - Matches by `name` attribute. +### Form Conventions (Action/Controller Pattern) + +Forms follow a load/save pattern mirroring traditional Laravel: Action loads data, Controller saves it. + +```javascript +// Action: on_create() sets defaults, on_load() fetches for edit mode +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 }; +} +``` + +```php +// Controller: save() receives all form values, validates, persists +#[Ajax_Endpoint] +public static function save(Request $request, array $params = []) { + if (empty($params['title'])) return response_form_error('Error', ['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 + +Details: `php artisan rsx:man form_conventions` + --- ## MODALS