Add multi-route support for controllers and SPA actions Add screenshot feature to rsx:debug and convert contacts edit to SPA 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1170 lines
33 KiB
Markdown
1170 lines
33 KiB
Markdown
<!--
|
|
===============================================================================
|
|
WARNING: READ-ONLY FRAMEWORK DOCUMENTATION
|
|
===============================================================================
|
|
|
|
This file contains RSpade framework documentation for AI/LLM assistants and is
|
|
maintained by the framework developers. It is marked read-only and will be
|
|
REPLACED during framework updates via `php artisan rsx:framework:pull`.
|
|
|
|
DO NOT modify this file directly. Any changes you make will be lost during the
|
|
next framework update.
|
|
|
|
For application-specific documentation and project context:
|
|
- Edit the CLAUDE.md file in your project root
|
|
- That file is NOT managed by the framework and persists across updates
|
|
|
|
This separation ensures:
|
|
- Framework documentation stays current with updates
|
|
- Your project-specific documentation is preserved
|
|
- AI assistants have access to both framework and application context
|
|
|
|
===============================================================================
|
|
-->
|
|
|
|
# RSpade Framework - AI/LLM Development Guide
|
|
|
|
**PURPOSE**: Essential directives for AI/LLM assistants developing RSX applications with RSpade.
|
|
|
|
## What is RSpade?
|
|
|
|
**Visual Basic-like development for PHP/Laravel.** Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel.
|
|
|
|
**Philosophy**: Modern anti-modernization. While JavaScript fragments into complexity and React demands quarterly paradigm shifts, RSpade asks: "What if coding was easy again?" Business apps need quick builds and easy maintenance, not bleeding-edge architecture.
|
|
|
|
**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
|
|
|
|
### 🔴 RSpade Builds Automatically - NEVER RUN BUILD COMMANDS
|
|
|
|
**RSpade is an INTERPRETED framework** - like Python or PHP, changes are automatically detected and compiled on-the-fly. There is NO manual build step.
|
|
|
|
**ABSOLUTELY FORBIDDEN** (unless explicitly instructed):
|
|
- `npm run compile` / `npm run build` - **DO NOT EXIST**
|
|
- `bin/publish` - Creates releases for OTHER developers (not for testing YOUR changes)
|
|
- `rsx:bundle:compile` - Bundles compile automatically in dev mode
|
|
- `rsx:manifest:build` - Manifest rebuilds automatically in dev mode
|
|
- ANY command with "build", "compile", or "publish"
|
|
|
|
**How it works**:
|
|
1. Edit JS/SCSS/PHP files
|
|
2. Refresh browser
|
|
3. Changes are live (< 1 second)
|
|
|
|
**If you find yourself wanting to run build commands**: STOP. You're doing something wrong. Changes are already live.
|
|
|
|
### 🔴 Framework Updates
|
|
|
|
```bash
|
|
php artisan rsx:framework:pull # 5-minute timeout required
|
|
```
|
|
|
|
Updates take 2-5 minutes. Includes code pull, manifest rebuild, bundle recompilation. **For AI: Always use 5-minute timeout.**
|
|
|
|
### 🔴 Fail Loud - No Silent Fallbacks
|
|
|
|
**ALWAYS fail visibly.** No redundant fallbacks, silent failures, or alternative code paths.
|
|
|
|
```php
|
|
// ❌ CATASTROPHIC
|
|
try { $clean = Sanitizer::sanitize($input); }
|
|
catch (Exception $e) { $clean = $input; } // DISASTER
|
|
|
|
// ✅ CORRECT
|
|
$clean = Sanitizer::sanitize($input); // Let it throw
|
|
```
|
|
|
|
**SECURITY-CRITICAL**: If sanitization/validation/auth fails, NEVER continue. Always throw immediately.
|
|
|
|
**NO BLANKET TRY/CATCH**: Use try/catch only for expected failures (file uploads, external APIs, user input parsing). NEVER wrap database operations or entire functions "just in case".
|
|
|
|
```php
|
|
// ❌ WRONG - Defensive "on error resume"
|
|
try {
|
|
$user->save();
|
|
$result = process_data($user);
|
|
return $result;
|
|
} catch (Exception $e) {
|
|
throw new Exception("Failed: " . $e->getMessage(), 0, $e);
|
|
}
|
|
|
|
// ✅ CORRECT - Let exceptions bubble
|
|
$user->save();
|
|
$result = process_data($user);
|
|
return $result;
|
|
```
|
|
|
|
Exception handlers format errors for different contexts (Ajax JSON, CLI, HTML). Don't wrap exceptions with generic messages - let them bubble to the 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.
|
|
|
|
### 🔴 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.
|
|
|
|
### 🔴 DO NOT RUN `rsx:clean`
|
|
|
|
**RSpade's cache auto-invalidates on file changes.** Running `rsx:clean` causes 30-60 second rebuilds with zero benefit.
|
|
|
|
**When to use**: Only on catastrophic corruption, after framework updates (automatic), or when explicitly instructed.
|
|
|
|
**Correct workflow**: Edit → Save → Reload browser → See changes (< 1 second)
|
|
|
|
### 🔴 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.blade.php
|
|
frontend_calendar_event.js
|
|
```
|
|
|
|
**Critical**: Never create same-name different-case files (e.g., `user.php` and `User.php`).
|
|
|
|
---
|
|
|
|
## DIRECTORY STRUCTURE
|
|
|
|
```
|
|
/var/www/html/
|
|
├── rsx/ # YOUR CODE
|
|
│ ├── app/ # Modules
|
|
│ ├── models/ # Database models
|
|
│ ├── public/ # Static files (web-accessible)
|
|
│ ├── resource/ # Framework-ignored
|
|
│ └── theme/ # Global assets
|
|
└── system/ # FRAMEWORK (read-only)
|
|
```
|
|
|
|
### 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
|
|
{
|
|
#[Auth('Permission::anybody()')]
|
|
#[Route('/', methods: ['GET'])]
|
|
public static function index(Request $request, array $params = [])
|
|
{
|
|
return rsx_view('Frontend_Index', [
|
|
'bundle' => Frontend_Bundle::render()
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Rules**: Only GET/POST allowed. Use `:param` syntax. All routes MUST have `#[Auth]`.
|
|
|
|
### #[Auth] Attribute
|
|
|
|
```php
|
|
#[Auth('Permission::anybody()')] // Public
|
|
#[Auth('Permission::authenticated()')] // Require login
|
|
#[Auth('Permission::has_role("admin")')] // Custom
|
|
```
|
|
|
|
**Controller-wide**: Add to `pre_dispatch()`. Multiple attributes = all must pass.
|
|
|
|
### 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
|
|
Rsx::Route('User_Controller::show', 123);
|
|
|
|
// PHP - With query parameters
|
|
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'});
|
|
```
|
|
|
|
**Signature**: `Rsx::Route($action, $params = null)` / `Rsx.Route(action, params = null)`
|
|
- `$action` - Controller class, SPA action, or "Class::method" (defaults to 'index' if no `::` present)
|
|
- `$params` - Integer sets 'id', array/object provides named params
|
|
- **Unimplemented routes**: Prefix method with `#` → `Rsx::Route('Feature::#index')` generates `href="#"` and bypasses validation
|
|
|
|
**Query Parameters**: Extra params become query string - automatically URL-encoded
|
|
|
|
**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 feature/bundle)**
|
|
```php
|
|
class Frontend_Spa_Controller extends Rsx_Controller_Abstract {
|
|
#[SPA]
|
|
#[Auth('Permission::authenticated()')]
|
|
public static function index(Request $request, array $params = []) {
|
|
return rsx_view(SPA);
|
|
}
|
|
}
|
|
```
|
|
**CRITICAL**: One #[SPA] per feature/bundle (e.g., `/app/frontend`, `/app/root`, `/app/login`). Bundles separate features to save bandwidth, reduce processing time, and segregate confidential code (e.g., root admin from unauthorized users). The #[SPA] bootstrap performs server-side auth checks with failure/redirect before loading client-side actions. Typically one #[SPA] per feature at `rsx/app/(feature)/(feature)_spa_controller::index`.
|
|
|
|
**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 `$id="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
|
|
|
|
```
|
|
/rsx/app/frontend/
|
|
├── Frontend_Spa_Controller.php # Single SPA bootstrap
|
|
├── Frontend_Layout.js
|
|
├── Frontend_Layout.jqhtml
|
|
└── contacts/
|
|
├── 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
|
|
|
|
Details: `php artisan rsx:man spa`
|
|
|
|
---
|
|
|
|
## CONVERTING BLADE PAGES TO SPA ACTIONS
|
|
|
|
**7-step procedure to convert server-side pages to client-side SPA:**
|
|
|
|
**1. Create Action Files**
|
|
```bash
|
|
# In feature directory
|
|
touch Feature_Index_Action.js Feature_Index_Action.jqhtml
|
|
```
|
|
|
|
**2. Create Action Class (.js)**
|
|
```javascript
|
|
@route('/path')
|
|
@layout('Frontend_Spa_Layout')
|
|
@spa('Frontend_Spa_Controller::index')
|
|
@title('Page Title')
|
|
class Feature_Index_Action extends Spa_Action {
|
|
full_width = true; // For DataGrid pages
|
|
async on_load() {
|
|
this.data.items = await Feature_Controller.fetch_items();
|
|
}
|
|
}
|
|
```
|
|
|
|
**3. Convert Template (.jqhtml)**
|
|
|
|
**Blade → jqhtml syntax:**
|
|
- `{{ $var }}` → `<%= this.data.var %>`
|
|
- `{!! $html !!}` → `<%!= this.data.html %>`
|
|
- `@if($cond)` → `<% if (this.data.cond) { %>`
|
|
- `@foreach($items as $item)` → `<% for (let item of this.data.items) { %>`
|
|
- `@endforeach` → `<% } %>`
|
|
- `{{-- comment --}}` → `<%-- comment --%>`
|
|
- `{{ Rsx::Route('Class') }}` → `<%= Rsx.Route('Class') %>`
|
|
|
|
```jqhtml
|
|
<Define:Feature_Index_Action>
|
|
<Page>
|
|
<Page_Header><Page_Title>Title</Page_Title></Page_Header>
|
|
<% for (let item of this.data.items) { %>
|
|
<div><%= item.name %></div>
|
|
<% } %>
|
|
</Page>
|
|
</Define:Feature_Index_Action>
|
|
```
|
|
|
|
**4. Update Controller - Remove server-side route entirely:**
|
|
```php
|
|
// Remove #[Route] method completely. Add Ajax endpoints:
|
|
#[Ajax_Endpoint]
|
|
public static function fetch_items(Request $request, array $params = []): array {
|
|
return ['items' => Feature_Model::all()];
|
|
}
|
|
```
|
|
|
|
**CRITICAL**: Do NOT add `#[SPA]` to feature controllers. The `#[SPA]` attribute only exists in the bootstrap controller (e.g., `Frontend_Spa_Controller::index`). Feature controllers should only contain `#[Ajax_Endpoint]` methods for data fetching.
|
|
|
|
**5. Update Route References**
|
|
|
|
**Search entire codebase for old route references:**
|
|
```bash
|
|
grep -r "Feature_Controller::method" rsx/app/
|
|
```
|
|
|
|
Find/replace in all files:
|
|
- `Rsx::Route('Feature_Controller::method')` → `Rsx::Route('Feature_Action')`
|
|
- `Rsx.Route('Feature_Controller::method')` → `Rsx.Route('Feature_Action')`
|
|
- Hardcoded `/feature/method/123` → `Rsx.Route('Feature_Action', 123)`
|
|
|
|
**Check:** DataGrids, dashboards, save endpoints, navigation, breadcrumbs
|
|
|
|
**6. Archive Old Files**
|
|
```bash
|
|
mkdir -p rsx/resource/archive/frontend/feature/
|
|
mv feature_index.blade.php rsx/resource/archive/frontend/feature/
|
|
```
|
|
|
|
**7. Test**
|
|
```bash
|
|
php artisan rsx:debug /path
|
|
```
|
|
|
|
Verify: No JS errors, page renders, data loads.
|
|
|
|
**Common Patterns:**
|
|
|
|
**DataGrid (no data loading):**
|
|
```javascript
|
|
class Items_Index_Action extends Spa_Action {
|
|
full_width = true;
|
|
async on_load() {} // DataGrid loads own data
|
|
}
|
|
```
|
|
|
|
**Detail page (load data):**
|
|
```javascript
|
|
@route('/items/:id')
|
|
class Items_View_Action extends Spa_Action {
|
|
async on_load() {
|
|
this.data.item = await Items_Controller.get({id: this.args.id});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## BLADE & VIEWS
|
|
|
|
```blade
|
|
@rsx_id('Frontend_Index') {{-- Every view starts with this --}}
|
|
|
|
<body class="{{ rsx_body_class() }}"> {{-- Adds view class --}}
|
|
|
|
@rsx_include('Component_Name') {{-- Include by name, not path --}}
|
|
```
|
|
|
|
**NO inline styles, scripts, or event handlers** in Blade views:
|
|
- No `<style>` tags
|
|
- No `<script>` tags
|
|
- No inline event handlers: `onclick=""`, `onchange=""`, etc. are **forbidden**
|
|
- Use companion `.scss` and `.js` files instead
|
|
- Exception: jqhtml templates may use `@click` directive syntax (jqhtml-specific feature)
|
|
|
|
**Why no inline handlers**:
|
|
- Violates separation of concerns (HTML structure vs behavior)
|
|
- Makes code harder to maintain and test
|
|
- `rsx:check` will flag inline event handlers and require refactoring
|
|
|
|
**Correct pattern**:
|
|
```php
|
|
// Blade view - NO event handlers
|
|
<button id="submit-btn" class="btn btn-primary">Submit</button>
|
|
|
|
// Companion .js file
|
|
$('#submit-btn').click(() => {
|
|
// Handle click
|
|
});
|
|
```
|
|
|
|
### SCSS Pairing
|
|
|
|
```scss
|
|
/* frontend_index.scss - Same directory as view */
|
|
.Frontend_Index { /* Matches @rsx_id */
|
|
.content { padding: 20px; }
|
|
}
|
|
```
|
|
|
|
### 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.
|
|
|
|
---
|
|
|
|
## JAVASCRIPT
|
|
|
|
### Auto-Initialization
|
|
|
|
```javascript
|
|
class Frontend_Index {
|
|
static async on_app_ready() {
|
|
// DOM ready
|
|
}
|
|
|
|
static async on_jqhtml_ready() {
|
|
// Components ready
|
|
}
|
|
}
|
|
```
|
|
|
|
**CRITICAL**: JavaScript only executes when bundle rendered.
|
|
|
|
---
|
|
|
|
## BUNDLE SYSTEM
|
|
|
|
**One bundle per page required.**
|
|
|
|
```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/app/frontend', // Directory
|
|
'rsx/models', // For JS stubs
|
|
'/public/vendor/css/core.css', // Public directory asset (filemtime cache-busting)
|
|
],
|
|
];
|
|
}
|
|
}
|
|
```
|
|
|
|
Bundles support `/public/` prefix for including static assets from public directories with automatic cache-busting.
|
|
|
|
Auto-compiles on page reload in development.
|
|
|
|
```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** (v2.2.162+): Apply attributes conditionally using `<% if (condition) { %>attr="value"<% } %>`
|
|
directly in attribute context. Works with static values, interpolations, and multiple conditions per element.
|
|
Example: `<input <% if (this.args.required) { %>required="required"<% } %> />`
|
|
|
|
### 🔴 State Management Rules (ENFORCED)
|
|
|
|
**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)
|
|
|
|
**Quick Guide:**
|
|
- Loading from API? Use `this.data` in `on_load()`
|
|
- Need to reload with new params? Modify `this.args`, call `reload()`
|
|
- UI state (toggles, counters, etc)? Use `this.state`
|
|
|
|
```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).
|
|
|
|
### 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
|
|
- **`$id="name"`** → Scoped element ID
|
|
|
|
### Component Access
|
|
|
|
**this.$id(name)** → jQuery object (for DOM):
|
|
```javascript
|
|
this.$id('button').on('click', ...);
|
|
```
|
|
|
|
**this.id(name)** → Component instance (for methods):
|
|
```javascript
|
|
const comp = this.id('child'); // ✅ Returns component
|
|
await comp.reload();
|
|
|
|
const comp = this.id('child').component(); // ❌ WRONG
|
|
```
|
|
|
|
### Incremental Scaffolding
|
|
|
|
**Undefined components work immediately** - they render as div with the component name as a class.
|
|
|
|
```blade
|
|
<Dashboard>
|
|
<Stats_Panel />
|
|
<Recent_Activity />
|
|
</Dashboard>
|
|
```
|
|
|
|
### Common Pitfalls
|
|
|
|
1. `<Define>` IS the element - use `tag=""` attribute
|
|
2. `this.data` starts empty `{}`, set defaults in on_create()
|
|
3. ONLY modify `this.data` in on_create() and on_load() (enforced)
|
|
4. on_load() can ONLY access this.args and this.data (enforced)
|
|
5. Use `this.state = {}` in on_create() for component state (not loaded from Ajax)
|
|
6. Use this.args for reload parameters, reload() to re-fetch
|
|
7. Use `Controller.method()` not `$.ajax()`
|
|
8. Blade components self-closing only
|
|
9. `on_create/render/stop` must be sync
|
|
10. Use this.id() for components, NOT this.id().component()
|
|
|
|
### Bundle Integration Required
|
|
|
|
```blade
|
|
{!! Frontend_Bundle::render() !!} {{-- Required for JS --}}
|
|
<User_Card user_id="123" /> {{-- Now JS executes --}}
|
|
```
|
|
|
|
For advanced topics: `php artisan rsx:man jqhtml`
|
|
|
|
---
|
|
|
|
## 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.$id('name').val(values.name || '');
|
|
return null;
|
|
} else {
|
|
// Getter - extract values
|
|
return {name: this.$id('name').val()};
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Validation**: `Form_Utils.apply_form_errors(form.$, errors)` - Matches by `name` attribute.
|
|
|
|
---
|
|
|
|
## MODALS
|
|
|
|
**Basic dialogs**:
|
|
```javascript
|
|
await Modal.alert("File saved");
|
|
if (await Modal.confirm("Delete?")) { /* confirmed */ }
|
|
let name = await Modal.prompt("Enter name:");
|
|
```
|
|
|
|
**Form modals**:
|
|
```javascript
|
|
const result = await Modal.form({
|
|
title: "Edit User",
|
|
component: "User_Form",
|
|
component_args: {data: user},
|
|
on_submit: async (form) => {
|
|
const values = form.vals();
|
|
const response = await User_Controller.save(values);
|
|
|
|
if (response.errors) {
|
|
Form_Utils.apply_form_errors(form.$, response.errors);
|
|
return false; // Keep open
|
|
}
|
|
|
|
return response.data; // Close and return
|
|
}
|
|
});
|
|
```
|
|
|
|
**Requirements**: Form component must implement `vals()` and include `<div $id="error_container"></div>`.
|
|
|
|
**Modal Classes** (for complex/reusable modals):
|
|
```javascript
|
|
// Define modal class
|
|
class Add_User_Modal extends Modal_Abstract {
|
|
static async show() {
|
|
const result = await Modal.form({...});
|
|
return result || false;
|
|
}
|
|
}
|
|
|
|
// Use from page JS
|
|
const user = await Add_User_Modal.show();
|
|
if (user) {
|
|
// Orchestrate post-modal actions
|
|
grid.reload();
|
|
await Next_Modal.show(user.id);
|
|
}
|
|
```
|
|
|
|
Pattern: Extend `Modal_Abstract`, implement static `show()`, return data or `false`. Page JS orchestrates flow, modal classes encapsulate UI.
|
|
|
|
Details: `php artisan rsx:man modals`
|
|
|
|
---
|
|
|
|
## JQUERY EXTENSIONS
|
|
|
|
RSpade extends jQuery with utility methods:
|
|
|
|
**Element existence**: `$('.element').exists()` instead of `.length > 0`
|
|
|
|
**Component traversal**: `this.$.shallowFind('.Widget')` - Finds child elements matching selector that don't have another element of the same class as a parent between them and the component. Prevents selecting widgets from nested child components.
|
|
|
|
```javascript
|
|
// Use case: Finding form widgets without selecting nested widgets
|
|
this.$.shallowFind('.Form_Field').each(function() {
|
|
// Only processes fields directly in this form,
|
|
// not fields in nested sub-forms
|
|
});
|
|
```
|
|
|
|
**Sibling component lookup**: `$('.element').closest_sibling('.Widget')` - Searches for elements within progressively higher ancestors. Like `.closest()` but searches within ancestors instead of matching them. Stops at body tag. Useful for component-to-component communication.
|
|
|
|
**Form validation**: `$('form').checkValidity()` instead of `$('form')[0].checkValidity()`
|
|
|
|
**Click override**: `.click()` automatically calls `e.preventDefault()`. Use `.click_allow_default()` for native behavior.
|
|
|
|
For complete 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
|
|
|
|
**🔴 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.
|
|
|
|
### Migrations
|
|
|
|
**Forward-only, no rollbacks.**
|
|
|
|
```bash
|
|
php artisan make:migration:safe create_users_table
|
|
php artisan migrate:begin
|
|
php artisan migrate
|
|
php artisan migrate:commit
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
**Return data directly - framework auto-wraps as `{success: true, data: ...}`**
|
|
|
|
```php
|
|
#[Ajax_Endpoint]
|
|
public static function fetch_client(Request $request, array $params = []) {
|
|
$client = Client_Model::find($params['id']);
|
|
if (!$client) throw new \Exception('Not found');
|
|
return $client; // ✅ Return data directly
|
|
}
|
|
```
|
|
|
|
**❌ NEVER**: `return ['success' => true, 'data' => $client]` - framework adds this
|
|
|
|
**Special returns** (bypass auto-wrap):
|
|
- `['errors' => [...]]` - Form validation errors
|
|
- `['redirect' => '/path']` - Client-side navigation
|
|
- `['error' => 'msg']` - Operation failure
|
|
|
|
```php
|
|
// Form validation
|
|
if (empty($params['name'])) {
|
|
return ['errors' => ['name' => 'Required']];
|
|
}
|
|
|
|
// Success with redirect
|
|
Flash_Alert::success('Saved');
|
|
return ['redirect' => Rsx::Route('View_Action', $id)];
|
|
|
|
// Permission error
|
|
if (!can_delete()) {
|
|
return ['error' => 'Permission denied'];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Flash Alerts in Ajax Endpoints
|
|
|
|
Use server-side success alerts ONLY with redirects (no on-screen element to show message).
|
|
|
|
**Client-side**: Use `Flash_Alert.error()`, `Flash_Alert.warning()` on case-by-case basis.
|
|
|
|
---
|
|
|
|
## BROWSER STORAGE
|
|
|
|
**Rsx_Storage** - Scoped sessionStorage/localStorage with automatic fallback and quota management. All keys automatically scoped by session, user, site, and build. Gracefully handles unavailable storage and quota exceeded errors. Storage is volatile - use only for non-critical data (caching, UI state, transient messages).
|
|
|
|
`Rsx_Storage.session_set(key, value)` / `Rsx_Storage.session_get(key)` / `Rsx_Storage.local_set(key, value)` / `Rsx_Storage.local_get(key)`
|
|
|
|
Details: `php artisan rsx:man storage`
|
|
|
|
---
|
|
|
|
### Model Fetch
|
|
|
|
```php
|
|
#[Ajax_Endpoint_Model_Fetch]
|
|
public static function fetch($id)
|
|
{
|
|
if (!RsxAuth::check()) return false;
|
|
return static::find($id);
|
|
}
|
|
```
|
|
|
|
```javascript
|
|
const user = await User_Model.fetch(1);
|
|
```
|
|
|
|
---
|
|
|
|
## AUTHENTICATION
|
|
|
|
**Always use RsxAuth**, never Laravel Auth or $_SESSION.
|
|
|
|
```php
|
|
RsxAuth::check(); // Is authenticated
|
|
RsxAuth::user(); // User model
|
|
RsxAuth::id(); // User 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 /path --help # Show all options
|
|
```
|
|
|
|
Screenshot presets: mobile, iphone-mobile, tablet, desktop-small, desktop-medium, desktop-large
|
|
|
|
Use this instead of manually browsing, especially for SPA pages and Ajax-heavy features.
|
|
|
|
### 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.
|
|
|
|
Run `rsx:check` before commits. Enforces naming, prohibits animations on non-actionable elements.
|
|
|
|
---
|
|
|
|
## MAIN_ABSTRACT MIDDLEWARE
|
|
|
|
Optional `/rsx/main.php`:
|
|
|
|
```php
|
|
class Main extends Main_Abstract
|
|
{
|
|
public function init() { } // Bootstrap once
|
|
public function pre_dispatch($request, $params) { return null; } // Before routes
|
|
public function unhandled_route($request, $params) { } // 404s
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 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 RsxAuth** - 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]** - No exceptions
|
|
|
|
---
|
|
|
|
## GETTING HELP
|
|
|
|
```bash
|
|
php artisan rsx:man <topic> # Detailed docs
|
|
php artisan list rsx # All commands
|
|
```
|
|
|
|
**Topics**: bundle_api, jqhtml, routing, migrations, console_debug, model_fetch, vs_code_extension, deployment, framework_divergences
|
|
|
|
**Remember**: RSpade prioritizes simplicity and rapid development. When in doubt, choose the straightforward approach.
|