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>
7.5 KiB
Executable File
7.5 KiB
Executable File
name, description
| name | description |
|---|---|
| crud-patterns | 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:
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)
@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
}
}
<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
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.
@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;
}
}
}
<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:
@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;
}
}
}
<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()
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
- SPA controllers = Ajax endpoints only - No page rendering
- Single action handles add/edit - Dual
@routedecorators - Models implement
fetch()- With#[Ajax_Endpoint_Model_Fetch] - DataGrids extend
DataGrid_Abstract- Query + columns + sorting - Three-state pattern - Loading → Error → Content
- form_data must be serializable - Plain objects, not models
More Information
Details: php artisan rsx:man crud