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)