Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 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
|
|
|
|
**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**
|
|
```php
|
|
// Change #[Route] to #[SPA]
|
|
#[SPA]
|
|
public static function index(Request $request, array $params = []) {
|
|
return rsx_view(SPA);
|
|
}
|
|
|
|
// Add Ajax endpoints
|
|
#[Ajax_Endpoint]
|
|
public static function fetch_items(Request $request, array $params = []): array {
|
|
return ['items' => Feature_Model::all()];
|
|
}
|
|
```
|
|
|
|
**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
|
|
|
|
```php
|
|
#[Ajax_Endpoint]
|
|
public static function get_data(Request $request, array $params = [])
|
|
{
|
|
return ['success' => true, 'data' => ...];
|
|
}
|
|
```
|
|
|
|
```javascript
|
|
const result = await Demo_Controller.get_data({user_id: 123});
|
|
```
|
|
|
|
### Flash Alerts in Ajax Endpoints
|
|
|
|
**Server-side: Use success alerts ONLY when providing a redirect.**
|
|
|
|
When the Ajax response includes a redirect, there's no on-screen element from the previous page to display the success message. Flash alerts persist across navigation to show the result.
|
|
|
|
```php
|
|
#[Ajax_Endpoint]
|
|
public static function save(Request $request, array $params = []): array {
|
|
// Validation errors - return for client to handle
|
|
if (empty($params['name'])) {
|
|
return ['success' => false, 'errors' => ['name' => 'Required']];
|
|
}
|
|
|
|
$client->save();
|
|
|
|
// Success with redirect - use flash alert
|
|
Flash_Alert::success('Client saved successfully');
|
|
|
|
return [
|
|
'client_id' => $client->id,
|
|
'redirect' => Rsx::Route('Clients_View_Action', $client->id),
|
|
];
|
|
}
|
|
```
|
|
|
|
**Server pattern:** Success alerts with redirect only. Client handles errors by updating on-screen UI.
|
|
|
|
**Client-side:** Flash alerts are a UI choice. Use `Flash_Alert.error()`, `Flash_Alert.warning()`, etc. on case-by-case basis (e.g., "Failed to add item to cart - item not available").
|
|
|
|
---
|
|
|
|
## 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 /path --help # Show all options
|
|
```
|
|
|
|
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.
|