--- name: crud-patterns description: Standard CRUD implementation patterns in RSX including directory structure, DataGrid lists, view pages, dual-route edit actions, and three-state loading. Use when building list/view/edit pages, implementing DataGrid, creating add/edit forms, or following RSX CRUD conventions. --- # RSX CRUD Patterns ## Directory Structure Each CRUD feature follows this organization: ``` rsx/app/frontend/{feature}/ ├── {feature}_controller.php # Ajax endpoints ├── list/ │ ├── {Feature}_Index_Action.js # List page │ ├── {Feature}_Index_Action.jqhtml │ ├── {feature}_datagrid.php # DataGrid backend │ └── {feature}_datagrid.jqhtml # DataGrid template ├── view/ │ ├── {Feature}_View_Action.js # Detail view │ └── {Feature}_View_Action.jqhtml └── edit/ ├── {Feature}_Edit_Action.js # Add/Edit (dual-route) └── {Feature}_Edit_Action.jqhtml rsx/models/{feature}_model.php # With fetch() method ``` --- ## The Three Subdirectories | Directory | Purpose | Route Pattern | |-----------|---------|---------------| | `list/` | Index with DataGrid | `/contacts` | | `view/` | Single record detail | `/contacts/:id` | | `edit/` | Add AND edit form | `/contacts/add`, `/contacts/:id/edit` | --- ## Feature Controller The controller provides Ajax endpoints for all operations: ```php class Frontend_Contacts_Controller extends Rsx_Controller_Abstract { public static function pre_dispatch(Request $request, array $params = []) { if (!Session::is_logged_in()) return response_unauthorized(); return null; } #[Ajax_Endpoint] public static function datagrid_fetch(Request $request, array $params = []) { return Contacts_DataGrid::fetch($params); } #[Ajax_Endpoint] public static function save(Request $request, array $params = []) { // Validation if (empty($params['name'])) { return response_error(Ajax::ERROR_VALIDATION, ['name' => 'Required']); } // Create or update $contact = $params['id'] ? Contact_Model::find($params['id']) : new Contact_Model(); $contact->name = $params['name']; $contact->email = $params['email']; $contact->save(); return ['redirect' => Rsx::Route('Contacts_View_Action', $contact->id)]; } #[Ajax_Endpoint] public static function delete(Request $request, array $params = []) { $contact = Contact_Model::find($params['id']); $contact->delete(); return ['redirect' => Rsx::Route('Contacts_Index_Action')]; } } ``` --- ## List Page (Index Action) ```javascript @route('/contacts') @layout('Frontend_Layout') @spa('Frontend_Spa_Controller::index') class Contacts_Index_Action extends Spa_Action { async on_load() { // DataGrid fetches its own data } } ``` ```jqhtml

Contacts

Add Contact
``` --- ## DataGrid Backend ```php class Contacts_DataGrid extends DataGrid_Abstract { protected static function query(): Builder { return Contact_Model::query() ->where('site_id', Session::get_site_id()); } protected static function columns(): array { return [ 'name' => ['label' => 'Name', 'sortable' => true], 'email' => ['label' => 'Email', 'sortable' => true], 'created_at' => ['label' => 'Created', 'sortable' => true], ]; } protected static function default_sort(): array { return ['name' => 'asc']; } } ``` --- ## View Page (Three-State Pattern) View pages use the three-state loading pattern: loading → error → content. ```javascript @route('/contacts/:id') @layout('Frontend_Layout') @spa('Frontend_Spa_Controller::index') class Contacts_View_Action extends Spa_Action { on_create() { this.data.contact = null; this.data.error = null; } async on_load() { try { this.data.contact = await Contact_Model.fetch(this.args.id); } catch (e) { this.data.error = e; } } } ``` ```jqhtml <% if (!this.data.contact && !this.data.error) { %> <% } else if (this.data.error) { %> <% } else { %>

<%= this.data.contact.name %>

Email: <%= this.data.contact.email %>

Edit <% } %>
``` --- ## Edit Page (Dual-Route Action) A single action handles both add and edit modes: ```javascript @route('/contacts/add') @route('/contacts/:id/edit') @layout('Frontend_Layout') @spa('Frontend_Spa_Controller::index') class Contacts_Edit_Action extends Spa_Action { on_create() { this.data.form_data = { name: '', email: '' }; this.data.is_edit = !!this.args.id; this.data.error = null; } async on_load() { if (!this.data.is_edit) return; try { const contact = await Contact_Model.fetch(this.args.id); this.data.form_data = { id: contact.id, name: contact.name, email: contact.email }; } catch (e) { this.data.error = e; } } } ``` ```jqhtml <% if (this.data.is_edit && !this.data.form_data.id && !this.data.error) { %> <% } else if (this.data.error) { %> <% } else { %>

<%= this.data.is_edit ? 'Edit Contact' : 'Add Contact' %>

<% } %>
``` --- ## Model with fetch() ```php class Contact_Model extends Rsx_Model_Abstract { #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { if (!Session::is_logged_in()) { return false; } $contact = static::where('site_id', Session::get_site_id()) ->find($id); return $contact ?: false; } } ``` --- ## Key Principles 1. **SPA controllers = Ajax endpoints only** - No page rendering 2. **Single action handles add/edit** - Dual `@route` decorators 3. **Models implement `fetch()`** - With `#[Ajax_Endpoint_Model_Fetch]` 4. **DataGrids extend `DataGrid_Abstract`** - Query + columns + sorting 5. **Three-state pattern** - Loading → Error → Content 6. **form_data must be serializable** - Plain objects, not models ## More Information Details: `php artisan rsx:man crud`