Tighten CLAUDE.dist.md for LLM audience - 15% size reduction Add Repeater_Simple_Input component for managing lists of simple values Add Polymorphic_Field_Helper for JSON-encoded polymorphic form fields Fix incorrect data-sid selector in route-debug help example Fix Form_Utils to use component.$sid() instead of data-sid selector Add response helper functions and use _message as reserved metadata key 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1337 lines
45 KiB
Markdown
Executable File
1337 lines
45 KiB
Markdown
Executable File
# RSpade Framework - AI/LLM Development Guide
|
|
|
|
**PURPOSE**: Essential directives for AI/LLM assistants developing RSX applications with RSpade.
|
|
|
|
## CRITICAL: Questions vs Commands
|
|
|
|
- **Questions get answers, NOT actions** - "Is that fire?" gets "Yes" not "Let me run through it". User has a plan, don't take destructive action when asked a question.
|
|
- **Commands get implementation** - Clear directives result in code changes
|
|
|
|
## What is RSpade?
|
|
|
|
**Visual Basic-like development for PHP/Laravel.** Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel.
|
|
|
|
**Philosophy**: RSpade is rebellion against JavaScript fatigue. Like VB6 made Windows programming accessible, RSpade makes web development simple again. No build steps, no config hell, just code and reload.
|
|
|
|
**Important**: RSpade is built on Laravel but diverges significantly. Do not assume Laravel patterns work in RSX without verification.
|
|
|
|
**Terminology**: **RSpade** = Complete framework | **RSX** = Your application code in `/rsx/`
|
|
|
|
---
|
|
|
|
## CRITICAL RULES
|
|
|
|
### CRITICAL: RSpade Builds Automatically - NEVER RUN BUILD COMMANDS
|
|
|
|
**RSpade is INTERPRETED** - changes compile on-the-fly. NO manual build steps.
|
|
|
|
**FORBIDDEN** (unless explicitly instructed):
|
|
- `npm run compile/build` - Don't exist
|
|
- `bin/publish` - For releases, not testing
|
|
- `rsx:bundle:compile` / `rsx:manifest:build` / `rsx:clean` - Automatic
|
|
- ANY "build", "compile", or "publish" command
|
|
|
|
Edit → Save → Refresh browser → Changes live (< 1 second)
|
|
|
|
### Framework Updates
|
|
|
|
```bash
|
|
php artisan rsx:framework:pull # User-initiated only
|
|
```
|
|
|
|
### Fail Loud - No Silent Fallbacks
|
|
|
|
**ALWAYS fail visibly.** No redundant fallbacks, silent failures, or alternative code paths.
|
|
|
|
```php
|
|
// ❌ WRONG
|
|
try { $clean = Sanitizer::sanitize($input); }
|
|
catch (Exception $e) { $clean = $input; } // DISASTER
|
|
|
|
// ✅ CORRECT
|
|
$clean = Sanitizer::sanitize($input); // Let it throw
|
|
```
|
|
|
|
**SECURITY-CRITICAL**: Never continue after security failures. Only catch expected failures (file uploads, APIs, user input). Let exceptions bubble to global handler.
|
|
|
|
### No Defensive Coding
|
|
|
|
Core classes ALWAYS exist. Never check.
|
|
|
|
```javascript
|
|
// ❌ BAD: if (typeof Rsx !== 'undefined') { Rsx.Route('Controller::method') }
|
|
// ✅ GOOD: Rsx.Route('Controller::method')
|
|
```
|
|
|
|
### Static-First Philosophy
|
|
|
|
Classes are namespacing tools. Use static unless instances needed (models, resources). Avoid dependency injection.
|
|
|
|
### No Field Aliasing
|
|
|
|
**Field names must be identical across all layers.** Database → PHP → JSON → JavaScript: same names, always.
|
|
|
|
```php
|
|
// ❌ WRONG - renaming during serialization
|
|
return ['type_label' => $contact->type_id_label];
|
|
|
|
// ✅ CORRECT - names match source
|
|
return ['type_id_label' => $contact->type_id_label];
|
|
```
|
|
|
|
One string everywhere. Grep finds all usages. No mental mapping between layers.
|
|
|
|
### Git Workflow - Framework is READ-ONLY
|
|
|
|
**NEVER modify `/var/www/html/system/`** - It's like node_modules or the Linux kernel.
|
|
|
|
- **App repo**: `/var/www/html/.git` (you control)
|
|
- **Framework**: `/var/www/html/system/` (submodule, don't touch)
|
|
- **Your code**: `/var/www/html/rsx/` (all changes here)
|
|
|
|
**Commit discipline**: ONLY commit when explicitly asked. Commits are milestones, not individual changes.
|
|
|
|
### Class Overrides
|
|
|
|
To customize framework classes without modifying `/system/`, copy them to `rsx/` with the same class name. The manifest automatically uses your version and renames the framework file to `.upstream`.
|
|
|
|
**Common override targets** (copy from `system/app/RSpade/Core/Models/`):
|
|
- `User_Model` - Add custom fields, relationships, methods
|
|
- `User_Profile_Model` - Extend profile data
|
|
- `Site_Model` - Add site-specific settings
|
|
|
|
```bash
|
|
cp system/app/RSpade/Core/Models/User_Model.php rsx/models/user_model.php
|
|
# Edit namespace to Rsx\Models, customize as needed
|
|
```
|
|
|
|
Details: `php artisan rsx:man class_override`
|
|
|
|
### Trust Code Quality Rules
|
|
|
|
Each `rsx:check` rule has remediation text that tells AI assistants exactly what to do:
|
|
- Some rules say "fix immediately"
|
|
- Some rules say "present options and wait for decision"
|
|
|
|
AI should follow the rule's guidance precisely. Rules are deliberately written and well-reasoned.
|
|
|
|
---
|
|
|
|
## NAMING CONVENTIONS
|
|
|
|
**Enforced by `rsx:check`**:
|
|
|
|
| Context | Convention | Example |
|
|
|---------|------------|---------|
|
|
| PHP Methods/Variables | `underscore_case` | `user_name` |
|
|
| PHP Classes | `Like_This` | `User_Controller` |
|
|
| JavaScript Classes | `Like_This` | `User_Card` |
|
|
| Files | `lowercase_underscore` | `user_controller.php` |
|
|
| Database Tables | `lowercase_plural` | `users` |
|
|
| Constants | `UPPERCASE` | `MAX_SIZE` |
|
|
|
|
### File Prefix Grouping
|
|
|
|
Files sharing a common prefix are a related set. When renaming, maintain the grouping across ALL files with that prefix.
|
|
|
|
**Example** - `rsx/app/frontend/calendar/`:
|
|
```
|
|
frontend_calendar_event.scss
|
|
frontend_calendar_event_controller.php
|
|
frontend_calendar_event.jqhtml
|
|
frontend_calendar_event.js
|
|
```
|
|
|
|
**Critical**: Never create same-name different-case files (e.g., `user.php` and `User.php`).
|
|
|
|
### Component Naming Pattern
|
|
|
|
Input components follow: `{Supertype}_{Variant}_{Supertype}` → e.g., `Select_Country_Input`, `Select_State_Input`, `Select_Ajax_Input`
|
|
|
|
---
|
|
|
|
## DIRECTORY STRUCTURE
|
|
|
|
```
|
|
/var/www/html/
|
|
├── rsx/ # YOUR CODE
|
|
│ ├── app/ # Modules
|
|
│ ├── models/ # Database models
|
|
│ ├── services/ # Background tasks, external integrations
|
|
│ ├── public/ # Static files (web-accessible)
|
|
│ ├── resource/ # Framework-ignored
|
|
│ └── theme/ # Global assets
|
|
└── system/ # FRAMEWORK (read-only)
|
|
```
|
|
|
|
**Services**: Extend `Rsx_Service_Abstract` for non-HTTP functionality like scheduled tasks or external system integrations.
|
|
|
|
### Special Directories (Path-Agnostic)
|
|
|
|
**`resource/`** - ANY directory named this is framework-ignored. Store helpers, docs, third-party code. Exception: `/rsx/resource/config/` IS processed.
|
|
|
|
**`public/`** - ANY directory named this is web-accessible, framework-ignored. 5min cache, 30d with `?v=`.
|
|
|
|
### Path-Agnostic Loading
|
|
|
|
Classes found by name, not path. No imports needed.
|
|
|
|
```php
|
|
$user = User_Model::find(1); // Framework finds it
|
|
// NOT: use Rsx\Models\User_Model; // Auto-generated
|
|
```
|
|
|
|
---
|
|
|
|
## CONFIGURATION
|
|
|
|
**Two-tier system**:
|
|
- **Framework**: `/system/config/rsx.php` (never modify)
|
|
- **User**: `/rsx/resource/config/rsx.php` (your overrides)
|
|
|
|
Merged via `array_merge_deep()`. Common overrides: `development.auto_rename_files`, `bundle_aliases`, `console_debug`.
|
|
|
|
---
|
|
|
|
## ROUTING & CONTROLLERS
|
|
|
|
```php
|
|
class Frontend_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
public static function pre_dispatch(Request $request, array $params = [])
|
|
{
|
|
if (!Session::is_logged_in()) return response_unauthorized();
|
|
return null;
|
|
}
|
|
|
|
#[Route('/', methods: ['GET'])]
|
|
public static function index(Request $request, array $params = [])
|
|
{
|
|
return rsx_view('Frontend_Index', [
|
|
'bundle' => Frontend_Bundle::render()
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Rules**: Only GET/POST. Use `:param` syntax. Manual auth checks in pre_dispatch or method body.
|
|
|
|
### Authentication Pattern
|
|
|
|
```php
|
|
// Controller-wide auth (recommended)
|
|
public static function pre_dispatch(Request $request, array $params = []) {
|
|
if (!Session::is_logged_in()) return response_unauthorized();
|
|
return null;
|
|
}
|
|
|
|
```
|
|
|
|
### Type-Safe URLs
|
|
|
|
**MANDATORY**: All URLs must be generated using `Rsx::Route()` - hardcoded URLs are forbidden.
|
|
|
|
```php
|
|
// PHP - Controller (defaults to 'index' method)
|
|
Rsx::Route('User_Controller')
|
|
|
|
// PHP - Controller with explicit method. Passing an integer for param two implies the param two is 'id' (id=123)
|
|
Rsx::Route('User_Controller::show', 123);
|
|
|
|
// PHP - With query parameters. Extra params not defined in route itself become query string - automatically URL-encoded
|
|
Rsx::Route('Login_Controller::logout', ['redirect' => '/dashboard']);
|
|
// Generates: /logout?redirect=%2Fdashboard
|
|
|
|
// JavaScript (identical syntax)
|
|
Rsx.Route('User_Controller')
|
|
Rsx.Route('User_Controller::show', 123);
|
|
Rsx.Route('Login_Controller::logout', {redirect: '/dashboard'});
|
|
```
|
|
- **Unimplemented routes**: Prefix method with `#` → `Rsx::Route('Feature::#index')` generates `href="#"` and bypasses validation
|
|
|
|
**Enforcement**: `rsx:check` will flag hardcoded URLs like `/login` or `/logout?redirect=...` and require you to use `Rsx::Route()`. Do it right the first time to avoid rework.
|
|
|
|
---
|
|
|
|
## SPA (SINGLE PAGE APPLICATION) ROUTING
|
|
|
|
Client-side routing for authenticated application areas. One PHP bootstrap controller, multiple JavaScript actions that navigate without page reloads.
|
|
|
|
### SPA Components
|
|
|
|
**1. PHP Bootstrap Controller** - ONE per module with auth in pre_dispatch
|
|
```php
|
|
public static function pre_dispatch(Request $request, array $params = []) {
|
|
if (!Session::is_logged_in()) return response_unauthorized();
|
|
return null;
|
|
}
|
|
|
|
#[SPA]
|
|
public static function index(Request $request, array $params = []) {
|
|
return rsx_view(SPA);
|
|
}
|
|
```
|
|
One #[SPA] per module at `rsx/app/(module)/(module)_spa_controller::index`. Segregates code by permission level.
|
|
|
|
**2. JavaScript Actions (MANY)**
|
|
```javascript
|
|
@route('/contacts')
|
|
@layout('Frontend_Layout')
|
|
@spa('Frontend_Spa_Controller::index')
|
|
@title('Contacts') // Optional browser title
|
|
class Contacts_Index_Action extends Spa_Action {
|
|
async on_load() {
|
|
this.data.contacts = await Frontend_Contacts_Controller.datagrid_fetch();
|
|
}
|
|
}
|
|
```
|
|
|
|
**3. Layout**
|
|
```javascript
|
|
class Frontend_Layout extends Spa_Layout {
|
|
on_action(url, action_name, args) {
|
|
// Called after action created, before on_ready
|
|
// Access this.action immediately
|
|
this.update_navigation(url);
|
|
}
|
|
}
|
|
```
|
|
|
|
Layout template must have `$sid="content"` element where actions render.
|
|
|
|
### URL Generation & Navigation
|
|
|
|
```php
|
|
// PHP/JavaScript - same syntax
|
|
Rsx::Route('Contacts_Index_Action') // /contacts
|
|
Rsx::Route('Contacts_View_Action', 123) // /contacts/123
|
|
```
|
|
|
|
```javascript
|
|
Spa.dispatch('/contacts/123'); // Programmatic navigation
|
|
Spa.layout // Current layout instance
|
|
Spa.action // Current action instance
|
|
```
|
|
|
|
### URL Parameters
|
|
|
|
```javascript
|
|
// URL: /contacts/123?tab=history
|
|
@route('/contacts/:id')
|
|
class Contacts_View_Action extends Spa_Action {
|
|
on_create() {
|
|
console.log(this.args.id); // "123" (route param)
|
|
console.log(this.args.tab); // "history" (query param)
|
|
}
|
|
}
|
|
```
|
|
|
|
### File Organization
|
|
|
|
Pattern: `/rsx/app/(module)/(feature)/`
|
|
- **Module**: Major functionality (login, frontend, root)
|
|
- **Feature**: Screen within module (contacts, reports, invoices)
|
|
- **Submodule**: Feature grouping (settings), often with sublayouts
|
|
|
|
```
|
|
/rsx/app/frontend/ # Module
|
|
├── Frontend_Spa_Controller.php # Single SPA bootstrap
|
|
├── Frontend_Layout.js
|
|
├── Frontend_Layout.jqhtml
|
|
└── contacts/ # Feature
|
|
├── frontend_contacts_controller.php # Ajax endpoints only
|
|
├── Contacts_Index_Action.js # /contacts
|
|
├── Contacts_Index_Action.jqhtml
|
|
├── Contacts_View_Action.js # /contacts/:id
|
|
└── Contacts_View_Action.jqhtml
|
|
```
|
|
|
|
**Use SPA for:** Authenticated areas, dashboards, admin panels
|
|
**Avoid for:** Public pages (SEO needed), simple static pages
|
|
|
|
### Sublayouts
|
|
|
|
**Sublayouts** are `Spa_Layout` classes for nested persistent UI (e.g., settings sidebar). Use multiple `@layout` decorators - first is outermost: `@layout('Frontend_Spa_Layout')` then `@layout('Settings_Layout')`. Each must have `$sid="content"`. Layouts persist when unchanged; only differing parts recreated. All receive `on_action(url, action_name, args)` with final action info.
|
|
|
|
Details: `php artisan rsx:man spa`
|
|
|
|
### View Action Pattern (Loading Data)
|
|
|
|
For SPA actions that load data (view/edit CRUD pages), use the three-state pattern:
|
|
|
|
```javascript
|
|
on_create() {
|
|
this.data.record = { name: '' }; // Stub prevents undefined errors
|
|
this.data.error_data = null;
|
|
this.data.loading = true;
|
|
}
|
|
async on_load() {
|
|
try {
|
|
this.data.record = await Controller.get({id: this.args.id});
|
|
} catch (e) {
|
|
this.data.error_data = e;
|
|
}
|
|
this.data.loading = false;
|
|
}
|
|
```
|
|
|
|
Template uses three states: `<Loading_Spinner>` → `<Universal_Error_Page_Component>` → content.
|
|
|
|
**Details**: `php artisan rsx:man view_action_patterns`
|
|
|
|
---
|
|
|
|
## CONVERTING BLADE PAGES TO SPA ACTIONS
|
|
|
|
For converting server-side Blade pages to client-side SPA actions, see `php artisan rsx:man blade_to_spa`.
|
|
The process involves creating Action classes with @route decorators and converting templates from Blade to jqhtml syntax.
|
|
|
|
---
|
|
|
|
## BLADE & VIEWS
|
|
|
|
**Note**: SPA pages are the preferred standard. Use Blade only for SEO-critical public pages or authentication flows.
|
|
|
|
```blade
|
|
@rsx_id('Frontend_Index') {{-- Every view starts with this --}}
|
|
<body class="{{ rsx_body_class() }}"> {{-- Adds view class --}}
|
|
```
|
|
|
|
**NO inline styles, scripts, or event handlers** - Use companion `.scss` and `.js` files.
|
|
**jqhtml components** work fully in Blade (no slots).
|
|
|
|
### SCSS Component-First Architecture
|
|
|
|
**Philosophy**: Every styled element is a component. If it needs custom styles, give it a name, a jqhtml definition, and scoped SCSS. This eliminates CSS spaghetti - generic classes overriding each other unpredictably across files.
|
|
|
|
**Recognition**: When building a page, ask: "Is this structure unique, or a pattern?" A datagrid page with toolbar, tabs, filters, and search is a *pattern* - create `Datagrid_Card` once with slots, use it everywhere. A one-off project dashboard is *unique* - create `Project_Dashboard` for that page. If you're about to copy-paste structural markup, stop and extract a component.
|
|
|
|
**Composition**: Use slots to separate structure from content. The component owns layout and styling; pages provide the variable parts via slots. This keeps pages declarative and components reusable.
|
|
|
|
**Enforcement**: SCSS in `rsx/app/` and `rsx/theme/components/` must wrap in a single component class matching the jqhtml/blade file. This works because all jqhtml components, SPA actions/layouts, and Blade views with `@rsx_id` automatically render with `class="Component_Name"` on their root element. `rsx/lib/` is for non-visual plumbing (validators, utilities). `rsx/theme/` (outside components/) holds primitives, variables, Bootstrap overrides.
|
|
|
|
**BEM Child Classes**: When using BEM notation, child element classes must use the component's exact class name as prefix. SCSS `.Component_Name { &__element }` compiles to `.Component_Name__element`, so HTML must match: `<div class="Component_Name__element">` not `<div class="component-name__element">`. No kebab-case conversion.
|
|
|
|
**Variables**: Define shared values (colors, spacing, border-radius) in `rsx/theme/variables.scss` or similar. These must be explicitly included before directory includes in bundle definitions. Component-local variables can be defined within the scoped rule.
|
|
|
|
**Supplemental files**: Multiple SCSS files can target the same component (e.g., breakpoint-specific styles) if a primary file with matching filename exists.
|
|
|
|
Details: `php artisan rsx:man scss`
|
|
|
|
### Responsive Breakpoints
|
|
|
|
RSX replaces Bootstrap's default breakpoints (xs/sm/md/lg/xl/xxl) with semantic device names.
|
|
|
|
**Tier 1 - Semantic**:
|
|
- `mobile`: 0 - 1023px (phone + tablet)
|
|
- `desktop`: 1024px+
|
|
|
|
**Tier 2 - Granular**:
|
|
- `phone`: 0 - 799px | `tablet`: 800 - 1023px | `desktop-sm`: 1024 - 1699px | `desktop-md`: 1700 - 2199px | `desktop-lg`: 2200px+
|
|
|
|
**SCSS Mixins**: `@include mobile { }`, `@include desktop { }`, `@include phone { }`, `@include tablet { }`, `@include desktop-sm { }`, etc.
|
|
|
|
**Bootstrap Classes**: `.col-mobile-6`, `.col-desktop-4`, `.d-mobile-none`, `.d-tablet-block`, `.col-phone-12 .col-tablet-6 .col-desktop-sm-4`
|
|
|
|
**Utility Classes**: `.mobile-only`, `.desktop-only`, `.phone-only`, `.hide-mobile`, `.hide-tablet`
|
|
|
|
**Note**: Bootstrap's default classes like `.col-md-6` or `.d-lg-none` do NOT work - use the RSX breakpoint names instead.
|
|
|
|
**JS Detection**: `Responsive.is_mobile()`, `Responsive.is_desktop()` (Tier 1 - broad); `Responsive.is_phone()`, `Responsive.is_tablet()`, `Responsive.is_desktop_sm()`, `Responsive.is_desktop_md()`, `Responsive.is_desktop_lg()` (Tier 2 - specific ranges)
|
|
|
|
Details: `php artisan rsx:man responsive`
|
|
|
|
### JavaScript for Blade Pages
|
|
|
|
Unlike SPA actions (which use component lifecycle), Blade pages use static `on_app_ready()` with a page guard:
|
|
|
|
```javascript
|
|
class My_Page { // Matches @rsx_id('My_Page')
|
|
static on_app_ready() {
|
|
if (!$('.My_Page').exists()) return; // Guard required - fires for ALL pages in bundle
|
|
// Page code here
|
|
}
|
|
}
|
|
```
|
|
|
|
### Passing Data to JavaScript
|
|
|
|
Use `@rsx_page_data` for page-specific data needed by JavaScript (IDs, config, etc.):
|
|
|
|
```blade
|
|
@rsx_page_data(['user_id' => $user->id, 'mode' => 'edit'])
|
|
|
|
@section('content')
|
|
<!-- Your HTML -->
|
|
@endsection
|
|
```
|
|
|
|
Access in JavaScript:
|
|
```javascript
|
|
const user_id = window.rsxapp.page_data.user_id;
|
|
```
|
|
|
|
Use when data doesn't belong in DOM attributes. Multiple calls merge together.
|
|
|
|
---
|
|
|
|
## BUNDLE SYSTEM
|
|
|
|
**One bundle per module (rsx/app/(module)).** Compiles JS/CSS automatically on request - no manual build steps.
|
|
|
|
```php
|
|
class Frontend_Bundle extends Rsx_Bundle_Abstract
|
|
{
|
|
public static function define(): array
|
|
{
|
|
return [
|
|
'include' => [
|
|
'jquery', // Required
|
|
'lodash', // Required
|
|
'rsx/theme/variables.scss', // Order matters
|
|
'rsx/theme', // Everything else from theme - but variables.scss will be first
|
|
'rsx/app/frontend', // Directory -
|
|
'rsx/models', // For JS stubs
|
|
],
|
|
];
|
|
}
|
|
}
|
|
```
|
|
|
|
```blade
|
|
<head>
|
|
{!! Frontend_Bundle::render() !!}
|
|
</head>
|
|
```
|
|
|
|
---
|
|
|
|
## JQHTML COMPONENTS
|
|
|
|
### Philosophy
|
|
|
|
For mechanical thinkers who see structure, not visuals. Write `<User_Card>` not `<div class="card">`. Name what things ARE.
|
|
|
|
### Template Syntax
|
|
|
|
**CRITICAL: `<Define>` IS the element, not a wrapper**
|
|
|
|
```jqhtml
|
|
<!-- ✅ CORRECT - Define becomes button -->
|
|
<Define:Save_Button tag="button" class="btn btn-primary">
|
|
Save
|
|
</Define:Save_Button>
|
|
|
|
<!-- Renders as: -->
|
|
<button class="Save_Button Component btn btn-primary">Save</button>
|
|
```
|
|
|
|
**Interpolation**: `<%= escaped %>` | `<%!= unescaped %>` | `<% javascript %>`
|
|
**Conditional Attributes** `<input <% if (this.args.required) { %>required="required"<% } %> />`
|
|
**Inline Logic**: `<% this.handler = () => action(); %>` then `@click=this.handler` - No JS file needed for simple components
|
|
**Event Handlers**: `@click=this.method` (unquoted) - Methods defined inline or in companion .js
|
|
**Validation**: `<% if (!this.args.required) throw new Error('Missing arg'); %>` - Fail loud in template
|
|
|
|
### Simple Components (No JS File Needed)
|
|
|
|
For simple components without external data or complex state, write JS directly in the template:
|
|
|
|
```jqhtml
|
|
<Define:CSV_Renderer>
|
|
<%
|
|
// Validate input
|
|
if (!this.args.csv_data) throw new Error('csv_data required');
|
|
|
|
// Parse CSV
|
|
const rows = this.args.csv_data.split('\n').map(r => r.split(','));
|
|
|
|
// Define click handler inline
|
|
this.toggle = () => { this.args.expanded = !this.args.expanded; this.render(); };
|
|
%>
|
|
|
|
<table>
|
|
<% for (let row of rows) { %>
|
|
<tr>
|
|
<% for (let cell of row) { %>
|
|
<td><%= cell %></td>
|
|
<% } %>
|
|
</tr>
|
|
<% } %>
|
|
</table>
|
|
|
|
<button @click=this.toggle>Toggle View</button>
|
|
</Define:CSV_Renderer>
|
|
```
|
|
|
|
**When to use inline JS**: Simple data transformations, conditionals, loops, basic event handlers
|
|
**When to create .js file**: External data loading, complex state management, multiple methods, or when JS overwhelms the template (should look mostly like HTML with some logic, not JS with some HTML)
|
|
|
|
### State Management Rules (ENFORCED)
|
|
|
|
**Quick Guide:**
|
|
- Loading from API? → Use `this.data` in `on_load()`
|
|
- Need reload with different params? → Modify `this.args`, call `reload()`
|
|
- UI state (toggles, selections)? → Use `this.state`
|
|
|
|
**this.args** - Component arguments (read-only in on_load(), modifiable elsewhere)
|
|
**this.data** - Ajax-loaded data (writable ONLY in on_create() and on_load())
|
|
**this.state** - Arbitrary component state (modifiable anytime)
|
|
|
|
```javascript
|
|
// WITH Ajax data
|
|
class Users_List extends Component {
|
|
on_create() {
|
|
this.data.users = []; // Defaults
|
|
}
|
|
async on_load() {
|
|
this.data.users = await User_Controller.fetch({filter: this.args.filter});
|
|
}
|
|
on_ready() {
|
|
// Change filter → reload
|
|
this.args.filter = 'new';
|
|
this.reload();
|
|
}
|
|
}
|
|
|
|
// WITHOUT Ajax data
|
|
class Toggle_Button extends Component {
|
|
on_create() {
|
|
this.state = {is_open: false};
|
|
}
|
|
on_ready() {
|
|
this.$.on('click', () => {
|
|
this.state.is_open = !this.state.is_open;
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**on_load() restrictions** (enforced):
|
|
- ✅ Read `this.args`, write `this.data`
|
|
- ❌ NO DOM access, NO `this.state`, NO modifying `this.args`
|
|
|
|
### Lifecycle
|
|
|
|
1. **on_create()** → Setup default state BEFORE template (sync)
|
|
2. **render** → Template executes with initialized state
|
|
3. **on_render()** → Hide uninitialized UI (sync)
|
|
4. **on_load()** → Fetch data into `this.data` (async)
|
|
5. **on_ready()** → DOM manipulation safe (async)
|
|
|
|
**on_create() now runs first** - Initialize `this.data` properties here so templates can safely reference them:
|
|
|
|
```javascript
|
|
on_create() {
|
|
this.data.rows = []; // Prevents "not iterable" errors
|
|
this.data.loading = true; // Template can check loading state
|
|
}
|
|
```
|
|
|
|
**Double-render**: If `on_load()` modifies `this.data`, component renders twice (defaults → populated).
|
|
|
|
### Component API - CRITICAL FOR LLM AGENTS
|
|
|
|
This section clarifies common misunderstandings. Read carefully.
|
|
|
|
**DOM Access Methods:**
|
|
|
|
| Method | Returns | Purpose |
|
|
|--------|---------|---------|
|
|
| `this.$` | jQuery | Root element of component (stable, survives redraws) |
|
|
| `this.$sid('name')` | jQuery | Child element with `$sid="name"` (always returns jQuery, even if empty) |
|
|
| `this.sid('name')` | Component or null | Child component instance (null if not found or not a component) |
|
|
| `this.$.find('.class')` | jQuery | Standard jQuery find (use when `$sid` isn't appropriate) |
|
|
|
|
**WRONG:** `this.$el` - This does not exist. Use `this.$`
|
|
|
|
**The reload() Paradigm - MANDATORY:**
|
|
|
|
```
|
|
reload() = on_load() → render() → on_ready()
|
|
render() = template redraw only (NO on_ready)
|
|
```
|
|
|
|
**LLM agents must ALWAYS use `reload()`, NEVER call `render()` directly.**
|
|
|
|
When you need to refresh a component after a mutation (add, edit, delete), call `this.reload()`. Yes, this makes another server call via `on_load()`. This is intentional. The extra round-trip is acceptable - our server is fast and the paradigm simplicity is worth it.
|
|
|
|
**WRONG approach (do NOT do this):**
|
|
```javascript
|
|
// ❌ BAD - Trying to be "efficient" by skipping server round-trip
|
|
async add_item() {
|
|
const new_item = await Controller.add({name: 'Test'});
|
|
this.data.items.push(new_item); // ERROR: Cannot modify this.data outside on_load
|
|
this.render(); // WRONG: Event handlers will break, on_ready won't run
|
|
}
|
|
```
|
|
|
|
**CORRECT approach:**
|
|
```javascript
|
|
// ✅ GOOD - Clean, consistent, reliable
|
|
async add_item() {
|
|
await Controller.add({name: 'Test'});
|
|
this.reload(); // Calls on_load() to refresh this.data, then on_ready() for handlers
|
|
}
|
|
```
|
|
|
|
**Event Handlers - Set Up in on_ready():**
|
|
|
|
Event handlers must be registered in `on_ready()`. Since `on_ready()` runs after every `reload()`, handlers automatically reattach when the DOM is redrawn.
|
|
|
|
```javascript
|
|
on_ready() {
|
|
this.$sid('save_btn').click(() => this.save());
|
|
this.$sid('delete_btn').click(() => this.delete());
|
|
}
|
|
```
|
|
|
|
**WRONG:** Event delegation to avoid `reload()`. If you find yourself writing `this.$.on('click', '[data-sid="btn"]', handler)` to "survive" render calls, you're doing it wrong. Use `reload()` and let `on_ready()` reattach handlers.
|
|
|
|
**this.data Modification Rules (ENFORCED):**
|
|
|
|
- `on_create()`: Set defaults only (e.g., `this.data.items = []`)
|
|
- `on_load()`: Fetch and assign from server (e.g., `this.data.items = await Controller.list()`)
|
|
- **Everywhere else**: Read-only. Attempting to modify `this.data` outside these methods throws an error.
|
|
|
|
**on_render() - LLM Should Not Use:**
|
|
|
|
`on_render()` exists for human developers doing performance optimization. LLM agents should pretend it doesn't exist. Use `on_ready()` for all post-render DOM work.
|
|
|
|
### Loading Pattern
|
|
|
|
```javascript
|
|
async on_load() {
|
|
const result = await Product_Controller.list({page: 1});
|
|
this.data.products = result.products;
|
|
this.data.loaded = true; // Simple flag at END
|
|
}
|
|
```
|
|
|
|
```jqhtml
|
|
<% if (!this.data.loaded) { %>
|
|
Loading...
|
|
<% } else { %>
|
|
<!-- Show data -->
|
|
<% } %>
|
|
```
|
|
|
|
**NEVER call `this.render()` in `on_load()` - automatic re-render happens.**
|
|
|
|
### Attributes
|
|
|
|
- **`$quoted="string"`** → String literal
|
|
- **`$unquoted=expression`** → JavaScript expression
|
|
- **`$sid="name"`** → Scoped element ID
|
|
- **`attr="<%= expr %>"`** → HTML attribute with interpolation
|
|
|
|
**Key restrictions:**
|
|
- **`<Define>` attributes are static** - No `<%= %>` on the `<Define>` tag. For dynamic attributes on the root element, use inline JS: `<% this.$.attr('data-id', this.args.id); %>`
|
|
- **`$prefix` = component args, NOT HTML attributes** - `<My_Component $data-id=123 />` creates `this.args['data-id']`, not a `data-id` DOM attribute
|
|
- **Conditional attributes use if-statements** - `<% if (cond) { %>checked<% } %>` not ternaries
|
|
|
|
### Component Access
|
|
|
|
**$sid** attribute = "scoped ID" - unique within component instance
|
|
|
|
From within component methods:
|
|
- **this.$** → jQuery selector for the component element itself
|
|
- **this.$sid(name)** → jQuery selector for child element with `$sid="name"`
|
|
- **this.sid(name)** → Component instance of child (or null if not a component)
|
|
- **$(selector).component()** → Get component instance from jQuery element
|
|
- **`await $(selector).component().ready()`** → Await component initialization. Rarely needed - `on_ready()` auto-waits for children created during render. Use for dynamically created components or Blade page JS interaction.
|
|
|
|
### Custom Component Events
|
|
|
|
Fire: `this.trigger('event_name', data)` | Listen: `this.sid('child').on('event_name', (component, data) => {})`
|
|
|
|
**Key difference from jQuery**: Events fired BEFORE handler registration still trigger the callback when registered. This solves component lifecycle timing issues where child events fire before parent registers handlers. Never use `this.$.trigger()` for custom events (enforced by JQHTML-EVENT-01).
|
|
|
|
### Dynamic Component Creation
|
|
|
|
To dynamically create/replace a component in JavaScript:
|
|
```javascript
|
|
// Destroys existing component (if any) and creates new one in its place
|
|
$(selector).component('Component_Name', { arg1: value1, arg2: value2 });
|
|
|
|
// Example: render a component into a container
|
|
this.$sid('result_container').component('My_Component', {
|
|
data: myData,
|
|
some_option: true
|
|
});
|
|
```
|
|
|
|
### Incremental Scaffolding
|
|
|
|
**Undefined components work immediately** - they render as div with the component name as a class.
|
|
|
|
```blade
|
|
<Dashboard>
|
|
<Stats_Panel />
|
|
<Recent_Activity />
|
|
</Dashboard>
|
|
```
|
|
|
|
### Key Pitfalls (ABSOLUTE RULES)
|
|
|
|
1. `<Define>` IS the element - use `tag=""` attribute
|
|
2. `this.data` starts empty `{}` - MUST set defaults in `on_create()`
|
|
3. ONLY modify `this.data` in `on_create()` and `on_load()` (enforced by framework)
|
|
4. `on_load()` can ONLY access `this.args` and `this.data` (no DOM, no `this.state`)
|
|
5. Use `this.state = {}` in `on_create()` for UI state (not from Ajax)
|
|
6. Use `this.args` for reload parameters, call `reload()` to re-fetch
|
|
7. Use `Controller.method()` not `$.ajax()` - PHP methods with #[Ajax_Endpoint] auto-callable from JS
|
|
8. `on_create/render/stop` must be sync
|
|
9. `this.sid()` returns component instance, `$(selector).component()` converts jQuery to component
|
|
|
|
---
|
|
|
|
## FORM COMPONENTS
|
|
|
|
**Form fields** (`<Rsx_Form>` with `$data`, `$controller`, `$method`):
|
|
```blade
|
|
<Rsx_Form $data="{!! json_encode($form_data) !!}" $controller="Controller" $method="save">
|
|
<Form_Field $name="email" $label="Email" $required=true>
|
|
<Text_Input $type="email" />
|
|
</Form_Field>
|
|
|
|
<Form_Field_Hidden $name="id" />
|
|
</Rsx_Form>
|
|
```
|
|
|
|
- **Form_Field** - Standard formatted field with label, errors, help text
|
|
- **Form_Field_Hidden** - Single-tag hidden input (extends Form_Field_Abstract)
|
|
- **Form_Field_Abstract** - Base class for custom formatting (advanced)
|
|
|
|
**Disabled fields**: Use `$disabled=true` attribute on input components to disable fields. Unlike standard HTML, disabled fields still return values via `vals()` (useful for read-only data that should be submitted).
|
|
|
|
```blade
|
|
<Text_Input $type="email" $disabled=true />
|
|
<Select_Input $options="{!! json_encode($options) !!}" $disabled=true />
|
|
<Checkbox_Input $label="Subscribe" $disabled=true />
|
|
```
|
|
|
|
**Form component classes** use the **vals() dual-mode pattern**:
|
|
|
|
```javascript
|
|
class My_Form extends Component {
|
|
vals(values) {
|
|
if (values) {
|
|
// Setter - populate form
|
|
this.$sid('name').val(values.name || '');
|
|
return null;
|
|
} else {
|
|
// Getter - extract values
|
|
return {name: this.$sid('name').val()};
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**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('Validation failed', ['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`
|
|
|
|
### Polymorphic Form Fields
|
|
|
|
For fields that can reference multiple model types (e.g., an Activity linked to either a Contact or Project), use JSON-encoded polymorphic values.
|
|
|
|
```php
|
|
use App\RSpade\Core\Polymorphic_Field_Helper;
|
|
|
|
$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
|
|
Contact_Model::class,
|
|
Project_Model::class,
|
|
]);
|
|
|
|
if ($error = $eventable->validate('Please select an entity')) {
|
|
$errors['eventable'] = $error;
|
|
}
|
|
|
|
$model->eventable_type = $eventable->model;
|
|
$model->eventable_id = $eventable->id;
|
|
```
|
|
|
|
Client submits: `{"model":"Contact_Model","id":123}`. Always use `Model::class` for the whitelist.
|
|
|
|
Details: `php artisan rsx:man polymorphic`
|
|
|
|
---
|
|
|
|
## MODALS
|
|
|
|
### Built-in Dialog Types
|
|
|
|
| Method | Returns | Description |
|
|
|--------|---------|-------------|
|
|
| `Modal.alert(body)` | `void` | Simple notification |
|
|
| `Modal.alert(title, body, buttonLabel?)` | `void` | Alert with title |
|
|
| `Modal.confirm(body)` | `boolean` | Yes/no confirmation |
|
|
| `Modal.confirm(title, body, confirmLabel?, cancelLabel?)` | `boolean` | Confirmation with labels |
|
|
| `Modal.prompt(body)` | `string\|false` | Text input |
|
|
| `Modal.prompt(title, body, default?, multiline?)` | `string\|false` | Prompt with options |
|
|
| `Modal.select(body, options)` | `string\|false` | Dropdown selection |
|
|
| `Modal.select(title, body, options, default?, placeholder?)` | `string\|false` | Select with options |
|
|
| `Modal.error(error, title?)` | `void` | Error with red styling |
|
|
| `Modal.unclosable(title, body)` | `void` | Modal user cannot close |
|
|
|
|
```javascript
|
|
// Alert variations
|
|
await Modal.alert("File saved");
|
|
await Modal.alert("Success", "Your changes have been saved.");
|
|
await Modal.alert("Done", "Operation complete.", "Got it");
|
|
|
|
// Confirm variations
|
|
if (await Modal.confirm("Delete this item?")) { /* confirmed */ }
|
|
if (await Modal.confirm("Delete", "This cannot be undone.", "Delete", "Keep")) { /* ... */ }
|
|
|
|
// Prompt variations
|
|
const name = await Modal.prompt("Enter your name:");
|
|
const notes = await Modal.prompt("Notes", "Enter notes:", "", true); // multiline
|
|
|
|
// Select dropdown
|
|
const choice = await Modal.select("Choose option:", [{value: 'a', label: 'Option A'}, {value: 'b', label: 'Option B'}]);
|
|
|
|
// Error display
|
|
await Modal.error("Something went wrong");
|
|
await Modal.error({message: "Validation failed", errors: {...}}, "Error");
|
|
```
|
|
|
|
### Form Modals
|
|
|
|
```javascript
|
|
const result = await Modal.form({
|
|
title: "Edit User",
|
|
component: "User_Form",
|
|
component_args: {data: user},
|
|
on_submit: async (form) => {
|
|
const response = await User_Controller.save(form.vals());
|
|
if (response.errors) {
|
|
Form_Utils.apply_form_errors(form.$, response.errors);
|
|
return false; // Keep open
|
|
}
|
|
return response.data; // Close and return
|
|
}
|
|
});
|
|
```
|
|
|
|
Form component must implement `vals()` and include `<div $sid="error_container"></div>`.
|
|
|
|
### Modal Classes
|
|
|
|
For complex/reusable modals, create dedicated classes:
|
|
|
|
```javascript
|
|
class Add_User_Modal extends Modal_Abstract {
|
|
static async show() {
|
|
return await Modal.form({...}) || false;
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const user = await Add_User_Modal.show();
|
|
if (user) {
|
|
grid.reload();
|
|
await Next_Modal.show(user.id);
|
|
}
|
|
```
|
|
|
|
Pattern: Extend `Modal_Abstract`, implement static `show()`, return data or `false`.
|
|
|
|
Details: `php artisan rsx:man modals`
|
|
|
|
---
|
|
|
|
## JQUERY EXTENSIONS
|
|
|
|
| Method | Purpose |
|
|
|--------|---------|
|
|
| `.exists()` | Check element exists (instead of `.length > 0`) |
|
|
| `.shallowFind(selector)` | Find children without nested component interference |
|
|
| `.closest_sibling(selector)` | Search within ancestor hierarchy |
|
|
| `.checkValidity()` | Form validation helper |
|
|
| `.click()` | Auto-prevents default |
|
|
| `.click_allow_default()` | Native click behavior |
|
|
|
|
Details: `php artisan rsx:man jquery`
|
|
|
|
---
|
|
|
|
## MODELS & DATABASE
|
|
|
|
### No Mass Assignment
|
|
|
|
```php
|
|
// ✅ CORRECT
|
|
$user = new User_Model();
|
|
$user->email = $email;
|
|
$user->save();
|
|
|
|
// ❌ WRONG
|
|
User_Model::create(['email' => $email]);
|
|
```
|
|
|
|
### Enums
|
|
|
|
**CRITICAL: Read `php artisan rsx:man enum` for complete documentation before implementing.**
|
|
|
|
Integer-backed enums with model-level mapping to constants, labels, and custom properties.
|
|
|
|
```php
|
|
class Project_Model extends Rsx_Model_Abstract {
|
|
public static $enums = [
|
|
'status_id' => [
|
|
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'badge' => 'bg-success'],
|
|
2 => ['constant' => 'STATUS_ARCHIVED', 'label' => 'Archived', 'selectable' => false],
|
|
],
|
|
];
|
|
}
|
|
|
|
// Usage
|
|
$project->status_id = Project_Model::STATUS_ACTIVE;
|
|
echo $project->status_label; // "Active"
|
|
echo $project->status_badge; // "bg-success" (custom property)
|
|
```
|
|
|
|
**Migration:** Use BIGINT for enum columns, TINYINT(1) for booleans. Run `rsx:migrate:document_models` after adding enums.
|
|
|
|
### Model Fetch
|
|
|
|
```php
|
|
#[Ajax_Endpoint_Model_Fetch]
|
|
public static function fetch($id)
|
|
{
|
|
if (!Session::is_logged_in()) return false;
|
|
return static::find($id);
|
|
}
|
|
```
|
|
|
|
```javascript
|
|
const project = await Project_Model.fetch(1); // Throws if not found
|
|
const maybe = await Project_Model.fetch_or_null(999); // Returns null if not found
|
|
console.log(project.status_label); // Enum properties populated
|
|
console.log(Project_Model.STATUS_ACTIVE); // Static enum constants
|
|
|
|
// Lazy relationships (requires #[Ajax_Endpoint_Model_Fetch] on relationship method)
|
|
const client = await project.client(); // belongsTo → Model or null
|
|
const tasks = await project.tasks(); // hasMany → Model[]
|
|
```
|
|
|
|
**Security**: Both `fetch()` and relationships require `#[Ajax_Endpoint_Model_Fetch]` attribute. Related models must also implement `fetch()` with this attribute.
|
|
|
|
Details: `php artisan rsx:man model_fetch`
|
|
|
|
### Migrations
|
|
|
|
**Forward-only, no rollbacks. Deterministic transformations against known state.**
|
|
|
|
```bash
|
|
php artisan make:migration:safe create_users_table
|
|
php artisan migrate:begin
|
|
php artisan migrate
|
|
php artisan migrate:commit
|
|
```
|
|
|
|
**NO defensive coding in migrations:**
|
|
```php
|
|
// ❌ WRONG - conditional logic
|
|
$fk_exists = DB::select("SELECT ... FROM information_schema...");
|
|
if (!empty($fk_exists)) { DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar"); }
|
|
|
|
// ✅ CORRECT - direct statements
|
|
DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar");
|
|
DB::statement("ALTER TABLE foo DROP COLUMN baz");
|
|
```
|
|
|
|
No `IF EXISTS`, no `information_schema` queries, no fallbacks. Know current state, write exact transformation. Failures fail loud - snapshot rollback exists for recovery.
|
|
|
|
---
|
|
|
|
## FILE ATTACHMENTS
|
|
|
|
Files upload UNATTACHED → validate → assign via API. Session-based validation prevents cross-user file assignment.
|
|
|
|
```php
|
|
// Controller: Assign uploaded file
|
|
$attachment = File_Attachment_Model::find_by_key($params['photo_key']);
|
|
if ($attachment && $attachment->can_user_assign_this_file()) {
|
|
$attachment->attach_to($user, 'profile_photo'); // Single (replaces)
|
|
$attachment->add_to($project, 'documents'); // Multiple (adds)
|
|
}
|
|
|
|
// Model: Retrieve attachments
|
|
$photo = $user->get_attachment('profile_photo');
|
|
$documents = $project->get_attachments('documents');
|
|
|
|
// Display
|
|
$photo->get_thumbnail_url('cover', 128, 128);
|
|
$photo->get_url();
|
|
$photo->get_download_url();
|
|
```
|
|
|
|
**Endpoints:** `POST /_upload`, `GET /_download/:key`, `GET /_thumbnail/:key/:type/:width/:height`
|
|
|
|
Details: `php artisan rsx:man file_upload`
|
|
|
|
---
|
|
|
|
## AJAX ENDPOINTS
|
|
|
|
```php
|
|
#[Ajax_Endpoint]
|
|
public static function method(Request $request, array $params = []) {
|
|
return $data; // Success - framework wraps as {_success: true, _ajax_return_value: ...}
|
|
}
|
|
```
|
|
|
|
**PHP→JS Auto-mapping:**
|
|
```php
|
|
// PHP: My_Controller class
|
|
#[Ajax_Endpoint]
|
|
public static function save(Request $request, array $params = []) {
|
|
return ['id' => 123];
|
|
}
|
|
|
|
// JS: Automatically callable
|
|
const result = await My_Controller.save({name: 'Test'});
|
|
console.log(result.id); // 123
|
|
```
|
|
|
|
### Error Responses
|
|
|
|
Use `response_error(Ajax::ERROR_CODE, $metadata)`:
|
|
|
|
```php
|
|
// Not found
|
|
return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found');
|
|
|
|
// Validation
|
|
return response_error(Ajax::ERROR_VALIDATION, [
|
|
'email' => 'Invalid',
|
|
'name' => 'Required'
|
|
]);
|
|
|
|
// Auto-message
|
|
return response_error(Ajax::ERROR_UNAUTHORIZED);
|
|
```
|
|
|
|
**Codes:** `ERROR_VALIDATION`, `ERROR_NOT_FOUND`, `ERROR_UNAUTHORIZED`, `ERROR_AUTH_REQUIRED`, `ERROR_FATAL`, `ERROR_GENERIC`
|
|
|
|
**Client:**
|
|
```javascript
|
|
try {
|
|
const data = await Controller.get(id);
|
|
} catch (e) {
|
|
if (e.code === Ajax.ERROR_NOT_FOUND) {
|
|
// Handle
|
|
} else {
|
|
alert(e.message); // Generic
|
|
}
|
|
}
|
|
```
|
|
|
|
Unhandled errors auto-show flash alert.
|
|
|
|
---
|
|
|
|
## DATA FETCHING (CRITICAL)
|
|
|
|
**DEFAULT**: Use `Model.fetch(id)` for all single-record retrieval from JavaScript.
|
|
|
|
```javascript
|
|
const user = await User_Model.fetch(1); // Throws if not found
|
|
const user = await User_Model.fetch_or_null(1); // Returns null if not found
|
|
```
|
|
|
|
Requires `#[Ajax_Endpoint_Model_Fetch]` on the model's `fetch()` method.
|
|
|
|
Auto-populates enum properties and enables lazy relationship loading.
|
|
|
|
**If model not available in JS bundle**: STOP and ask the developer. Bundles should include all models they need (`rsx/models` in include paths). Do not create workaround endpoints without approval.
|
|
|
|
**Custom Ajax endpoints require developer approval** and are only for:
|
|
- Aggregations, batch operations, or complex result sets
|
|
- System/root-only models intentionally excluded from bundle
|
|
- Queries beyond simple ID lookup
|
|
|
|
Details: `php artisan rsx:man model_fetch`
|
|
|
|
---
|
|
|
|
## AUTHENTICATION
|
|
|
|
**Always use Session** - Static methods only. Never Laravel Auth or $_SESSION.
|
|
|
|
```php
|
|
Session::is_logged_in(); // Returns true if user logged in
|
|
Session::get_user(); // Returns user model or null
|
|
Session::get_user_id(); // Returns user ID or null
|
|
Session::get_site(); // Returns site model
|
|
Session::get_site_id(); // Returns current site ID
|
|
Session::get_session_id(); // Returns session ID
|
|
```
|
|
|
|
Sessions persist 365 days. Never implement "Remember Me".
|
|
|
|
---
|
|
|
|
## JAVASCRIPT DECORATORS
|
|
|
|
```javascript
|
|
/** @decorator */
|
|
function logCalls(target, key, descriptor) { /* ... */ }
|
|
|
|
class Service {
|
|
@logCalls
|
|
@mutex
|
|
async save() { /* ... */ }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## COMMANDS
|
|
|
|
### Module Creation
|
|
|
|
```bash
|
|
rsx:app:module:create <name> # /name
|
|
rsx:app:module:feature:create <m> <f> # /m/f
|
|
rsx:app:component:create --name=x # Component
|
|
```
|
|
|
|
### Development
|
|
|
|
```bash
|
|
rsx:check # Code quality
|
|
rsx:debug /page # Test routes (see below)
|
|
rsx:man <topic> # Documentation
|
|
db:query "SQL" --json
|
|
```
|
|
|
|
### Testing Routes
|
|
|
|
**`rsx:debug /path`** - Preferred method for testing routes
|
|
|
|
Uses Playwright to render the page and show rendered output, JavaScript errors, and console messages.
|
|
|
|
```bash
|
|
rsx:debug /clients # Test route
|
|
rsx:debug /dashboard --user=1 # Simulate authenticated user
|
|
rsx:debug /contacts --console # Show console.log output
|
|
rsx:debug /page --screenshot-path=/tmp/page.png --screenshot-width=mobile # Capture screenshot
|
|
rsx:debug /page --dump-dimensions=".card" # Add position/size data attributes to elements
|
|
rsx:debug /path --help # Show all options
|
|
|
|
# Simulate user interactions with --eval (executes before DOM capture)
|
|
rsx:debug /contacts --user=1 --eval="$('.page-link[data-page=\"2\"]').click(); await sleep(2000)"
|
|
rsx:debug /form --eval="$('#name').val('test'); $('form').submit(); await sleep(500)"
|
|
```
|
|
|
|
Screenshot presets: mobile, iphone-mobile, tablet, desktop-small, desktop-medium, desktop-large
|
|
|
|
The `--eval` option runs JavaScript after page load but before DOM capture. Use `await sleep(ms)` to wait for async operations. This is powerful for testing pagination, form submissions, and other interactive behavior.
|
|
|
|
Use this instead of manually browsing, especially for SPA pages and Ajax-heavy features.
|
|
|
|
**CRITICAL: SPA routes ARE server routes.** The server knows all SPA routes. `rsx:debug` uses Playwright to fully render pages including all JavaScript and SPA navigation. If you get a 404, the route genuinely doesn't exist - check your URL pattern and route definitions. Never dismiss 404s as "SPA routes can't be tested server-side" - this analysis is incorrect.
|
|
|
|
### Debugging
|
|
|
|
- **rsx_dump_die()** - Debug output
|
|
- **console_debug("CHANNEL", ...)** - Channel logging
|
|
- **CONSOLE_DEBUG_FILTER=CHANNEL** - Filter output
|
|
|
|
---
|
|
|
|
## ERROR HANDLING
|
|
|
|
```php
|
|
if (!$expected) {
|
|
shouldnt_happen("Class {$expected} missing");
|
|
}
|
|
```
|
|
|
|
Use for "impossible" conditions that indicate broken assumptions.
|
|
|
|
---
|
|
|
|
## CODE QUALITY
|
|
|
|
**Professional UI**: Hover effects ONLY on buttons, links, form fields. Static elements remain static.
|
|
|
|
**z-index**: Bootstrap defaults + 1100 (modal children), 1200 (flash alerts), 9000+ (system). Details: `rsx:man zindex`
|
|
|
|
Run `rsx:check` before commits. Enforces naming, prohibits animations on non-actionable elements.
|
|
|
|
---
|
|
|
|
## KEY REMINDERS
|
|
|
|
1. **Fail loud** - No silent failures
|
|
2. **Static by default** - Unless instances needed
|
|
3. **Path-agnostic** - Reference by name
|
|
4. **Bundles required** - For JavaScript
|
|
5. **Use Session** - Never Laravel Auth
|
|
6. **No mass assignment** - Explicit only
|
|
7. **Forward migrations** - No rollbacks
|
|
8. **Don't run rsx:clean** - Cache auto-invalidates
|
|
9. **All routes need auth checks** - In pre_dispatch() or method body (@auth-exempt for public)
|
|
|
|
---
|
|
|
|
## GETTING HELP
|
|
|
|
```bash
|
|
php artisan rsx:man <topic> # Framework documentation
|
|
php artisan list rsx # All commands
|
|
```
|
|
|
|
**Topics**: bundle_api, jqhtml, routing, migrations, console_debug, model_fetch, vs_code_extension, deployment, framework_divergences
|
|
|
|
---
|
|
|
|
## PROJECT DOCUMENTATION
|
|
|
|
Project-specific technical documentation lives in `/rsx/resource/man/`. These are man-page-style text files documenting features specific to your application that build on or extend the framework.
|
|
|
|
**When to create a project man page**:
|
|
- Feature has non-obvious implementation details
|
|
- Multiple components interact in ways that need explanation
|
|
- Configuration options or patterns need documentation
|
|
- AI agents or future developers need reference material
|
|
|
|
**Format**: Plain text files (`.txt`) following Unix man page conventions. See `/rsx/resource/man/CLAUDE.md` for writing guidelines.
|
|
|
|
**Remember**: RSpade prioritizes simplicity and rapid development. When in doubt, choose the straightforward approach.
|