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>
287 lines
7.5 KiB
Markdown
Executable File
287 lines
7.5 KiB
Markdown
Executable File
---
|
|
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
|
|
<Define:Contacts_Index_Action tag="div">
|
|
<div class="d-flex justify-content-between mb-3">
|
|
<h1>Contacts</h1>
|
|
<a href="<%= Rsx.Route('Contacts_Edit_Action') %>" class="btn btn-primary">
|
|
Add Contact
|
|
</a>
|
|
</div>
|
|
|
|
<Contacts_DataGrid />
|
|
</Define:Contacts_Index_Action>
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|
|
<Define:Contacts_View_Action tag="div">
|
|
<% if (!this.data.contact && !this.data.error) { %>
|
|
<Loading_Spinner />
|
|
<% } else if (this.data.error) { %>
|
|
<Universal_Error_Page_Component $error="<%= this.data.error %>" />
|
|
<% } else { %>
|
|
<h1><%= this.data.contact.name %></h1>
|
|
<p>Email: <%= this.data.contact.email %></p>
|
|
|
|
<a href="<%= Rsx.Route('Contacts_Edit_Action', this.data.contact.id) %>"
|
|
class="btn btn-primary">Edit</a>
|
|
<% } %>
|
|
</Define:Contacts_View_Action>
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|
|
<Define:Contacts_Edit_Action tag="div">
|
|
<% if (this.data.is_edit && !this.data.form_data.id && !this.data.error) { %>
|
|
<Loading_Spinner />
|
|
<% } else if (this.data.error) { %>
|
|
<Universal_Error_Page_Component $error="<%= this.data.error %>" />
|
|
<% } else { %>
|
|
<h1><%= this.data.is_edit ? 'Edit Contact' : 'Add Contact' %></h1>
|
|
|
|
<Rsx_Form $controller="Frontend_Contacts_Controller" $method="save"
|
|
$data="<%= JSON.stringify(this.data.form_data) %>">
|
|
<Form_Hidden_Field $name="id" />
|
|
|
|
<Form_Field $label="Name" $required=true>
|
|
<Text_Input $name="name" />
|
|
</Form_Field>
|
|
|
|
<Form_Field $label="Email">
|
|
<Text_Input $name="email" $type="email" />
|
|
</Form_Field>
|
|
|
|
<button type="submit" class="btn btn-primary">
|
|
<%= this.data.is_edit ? 'Save Changes' : 'Create Contact' %>
|
|
</button>
|
|
</Rsx_Form>
|
|
<% } %>
|
|
</Define:Contacts_Edit_Action>
|
|
```
|
|
|
|
---
|
|
|
|
## 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`
|