Files
rspade_system/docs/CLAUDE.dist.md
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

70 KiB
Executable File
Raw Blame History

RSpade Framework - AI/LLM Development Guide

PURPOSE: This document provides comprehensive directives for AI/LLM assistants developing RSX applications with the RSpade framework.

What is RSpade?

RSpade is a Visual Basic-like development environment for PHP/Laravel that prioritizes simplicity, straightforwardness, and rapid development. Think of it as:

  • VB6 apps run in the VB6 runtime which runs in Windows
  • RSX apps run in the RSpade runtime which runs in Laravel

Core Philosophy: RSpade is the modern anti-modernization framework - a deliberate rejection of contemporary complexity in favor of straightforward, understandable patterns. While the JavaScript ecosystem fragments into ever-more-complex build pipelines and the React world demands you learn new paradigms every quarter, RSpade asks: "What if we just made coding easy again?" Like Visual Basic democratized Windows development in the 90s by hiding COM complexity, RSpade hides Laravel's architectural complexity behind simple, intuitive patterns. This isn't nostalgia - it's recognition that business applications don't need bleeding-edge architecture; they need to be built quickly and maintained easily.

Important: RSpade is built on Laravel but diverges significantly from Laravel conventions. Do not assume Laravel patterns work in RSX without verification.

Terminology: RSpade vs RSX

RSpade - The complete framework (formal product name)

RSX - Your application code in /rsx/. Also appears in:

  • Class names (e.g., Rsx_Controller_Abstract)
  • Commands (e.g., rsx:check)
  • Configuration keys (e.g., config('rsx.bundle_aliases'))

FRAMEWORK UPDATES

Updating Framework

php artisan rsx:framework:pull

IMPORTANT: Framework updates take 2-5 minutes. When the user requests a framework update, execute the command with a 5-minute timeout to ensure it completes successfully. The update process includes:

  • Pulling latest framework code from upstream
  • Cleaning and rebuilding the manifest
  • Recompiling all bundles
  • Updating dependencies

For AI/LLM assistants: Always use a 5-minute timeout when executing framework updates to prevent premature termination.

Detailed Documentation

php artisan rsx:man rspade_upstream

CRITICAL DIRECTIVES

Fail Loud - No Silent Fallbacks

CRITICAL: Always fail loudly and visibly when errors occur.

  • NO redundant fallback systems - Fail clearly, not silently
  • NO silent failures - Errors must be immediately apparent
  • Exception handlers ONLY format display - Never provide alternative functionality
  • NO alternative code paths - ONE deterministic way

Examples:

  • BAD: JavaScript fails → fall back to server-side rendering
  • GOOD: JavaScript fails → display clear error message
  • BAD: New API fails → silently use old API
  • GOOD: New API fails → throw exception with clear error
  • BAD: Sanitizer fails → skip and continue
  • GOOD: Sanitizer fails → throw exception and halt

SECURITY-CRITICAL: If any subsystem fails (sanitization, validation, authentication), NEVER continue. Always throw immediately.

// ❌ CATASTROPHIC - Silent security failure
try {
    $clean = Sanitizer::sanitize($user_input);
} catch (Exception $e) {
    $clean = $user_input; // DISASTER
}

// ✅ CORRECT - Fail loudly
$clean = Sanitizer::sanitize($user_input); // Let it throw

No Defensive Coding for Core Classes

CRITICAL: Core framework classes are ALWAYS present. Never check if they exist.

  • Assume core classes exist
  • NO typeof checks - Don't check if (typeof Rsx !== 'undefined')
  • Just use directly - Rsx.Route(...) not wrapped in checks
  • Core classes: Rsx, Rsx_Cache, all framework-bundled classes

Examples:

  • BAD: if (typeof Rsx !== 'undefined') { Rsx.Route(...) }
  • GOOD: Rsx.Route(...)
  • BAD: try { a = 2 } catch(e) { console.log('Failed') }
  • GOOD: a = 2

The build system guarantees core classes are present. Trust it.

Static-First Philosophy

Classes are static by default - Classes are namespacing tools, not OOP for OOP's sake:

  • Use static methods - Unless class represents data needing multiple instances
  • Avoid dependency injection - Direct access everywhere
  • Exceptions: Models (database rows), resources, service connectors needing mocking

This keeps code simple and straightforward - the Visual Basic philosophy.

GIT WORKFLOW

🔴 CRITICAL: Framework Repository is READ-ONLY

AI AGENTS: You must NEVER modify files in /var/www/html or commit to /var/www/html/.git

The framework repository is equivalent to the Linux kernel or node_modules - it's external code managed by upstream developers. Just as you wouldn't commit to the Linux kernel when asked "what file is this?", you must never commit to the framework repo.

Forbidden actions in /var/www/html:

  • NEVER edit files (except /var/www/html/rsx/*)
  • NEVER run git add, git commit, git rm
  • NEVER remove files from git tracking
  • NEVER stage changes
  • NEVER fix issues in framework code (report them instead)
  • NEVER answer questions by modifying files

Only exception: Updating framework via php artisan rsx:framework:pull (this is automated and safe)

Two Git Repositories

Framework repo: /var/www/html/.git (read-only, managed by RSpade team - DO NOT TOUCH) Application repo: /var/www/html/rsx/.git (your code, you control)

Working Directory Rules

ALWAYS work from /var/www/html/rsx for application code:


NAMING CONVENTIONS

All conventions enforced by code quality checker (php artisan rsx:check).

PHP Naming

Context Convention Examples
Methods/Variables underscore_case user_name, calculate_total()
Constants UPPERCASE_WITH_UNDERSCORES MAX_UPLOAD_SIZE
RSX Classes Like_This_With_Underscores Frontend_Index_Controller
Files lowercase_with_underscores frontend_index_controller.php
Temp Files name-temp.extension test-temp.php

JavaScript Naming

Context Convention Examples
Classes Like_This_With_Underscores Demo_Index, User_Card
Methods/Variables underscore_case init_sidebar(), user_data
Constants UPPERCASE_WITH_UNDERSCORES API_URL
Files lowercase_with_underscores demo_index.js

Database Naming

Element Convention Examples
Tables lowercase_plural users, products
Primary Keys Always id id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
Foreign Keys table_id suffix user_id, site_id
Booleans is_ prefix is_active, is_verified
Timestamps _at suffix created_at, updated_at

Class Naming Philosophy

All class names must identify:

  1. Module (e.g., Frontend_, Backend_)
  2. Feature (e.g., Users, Settings)
  3. Type (e.g., _Controller, _Model)

Example: Frontend_Users_Index_Controller → Module: Frontend, Feature: Users, Subfeature: Index, Type: Controller

Suffix Enforcement

Classes extending abstracts must match suffix:

  • Extends Rsx_Controller_Abstract → ends in _Controller
  • Extends Rsx_Model_Abstract → ends in _Model
  • Extends Rsx_Bundle_Abstract → ends in _Bundle

Enforced automatically by rsx:check.

Critical Filename Rule

NEVER create files with same name but different case (e.g., Helpers.php and helpers.php). Breaks Windows/macOS compatibility. Flagged as critical error by rsx:check.


DIRECTORY STRUCTURE

Directory Structure

CRITICAL: The framework uses a split directory structure:

/var/www/html/                     # Project root
├── rsx/                           # YOUR APPLICATION CODE
│   ├── app/                       # Application modules
│   │   ├── frontend/              # Public website (/)
│   │   ├── backend/               # Admin panel (/admin)
│   │   ├── dashboard/             # User dashboard
│   │   ├── login/                 # Authentication
│   │   └── api/                   # API endpoints
│   ├── lib/                       # Shared libraries
│   ├── models/                    # Database models
│   ├── public/                    # Static files (5min cache, 30d with ?v=)
│   │   └── public_ignore.json     # Patterns blocked from HTTP
│   ├── resource/config/           # User config overrides
│   │   └── rsx.php                # Merged with /system/config/rsx.php
│   ├── theme/                     # Global theme assets
│   │   ├── variables.scss
│   │   ├── layouts/
│   │   └── components/
│   ├── main.php                   # App-wide middleware
│   └── permission.php             # Authorization methods
│
└── system/                        # FRAMEWORK CODE (do not modify)
    ├── app/RSpade/                # RSpade framework runtime
    ├── config/                    # Framework configuration
    ├── storage/                   # Build artifacts and caches
    ├── bin/                       # CLI scripts
    ├── docs.dist/                 # Documentation templates
    └── artisan                    # Laravel's artisan CLI

/system/app/RSpade/ - Framework Runtime

DO NOT MODIFY - Framework runtime that executes your RSX application.

/resource/ and /vendor/ Directories

Excluded from automatic processing:

  • /resource/ - External code not following RSX conventions
  • /vendor/ - Composer dependencies

Not subject to class uniqueness or filename conventions. Suitable for third-party code.

Note: Only files in /rsx/ (excluding /resource/ and /vendor/) and the core framework in /system/app/RSpade/ are automatically discovered and processed by RSpade.


CONFIGURATION SYSTEM

Two-Tier Configuration

Framework Config: /system/config/rsx.php - Framework defaults, NEVER modify

User Config: /rsx/resource/config/rsx.php - Your overrides, merged via array_merge_deep()

Merging Example

// Framework: /system/config/rsx.php
'bundle_aliases' => ['core' => CoreBundle::class]

// User: /rsx/resource/config/rsx.php
'bundle_aliases' => ['my-app' => MyAppBundle::class]

// Result: Both present
config('rsx.bundle_aliases'); // ['core' => CoreBundle, 'my-app' => MyAppBundle]

Common Overrides

  • development.auto_rename_files
  • code_quality.root_whitelist
  • bundle_aliases
  • gatekeeper
  • console_debug

FILE ORGANIZATION PHILOSOPHY

Files of Similar Concern Together

Related files (view, JS, SCSS, controller) stored in same folder, same base name, differing by extension:

/rsx/app/dashboard/
├── dashboard_controller.php
├── dashboard_bundle.php
├── dashboard_layout.blade.php
├── dashboard_index.blade.php
├── dashboard_index.js
├── dashboard_index.scss

Hierarchy Describes Logical Position

  • Outer layer: Shared layouts, global components
  • Middle layers: Modules and features
  • Inner layers: Specific pages

Theme and Library Directories

  • /rsx/theme/ - Reusable SCSS and jqhtml components
  • /rsx/lib/ - Reusable JavaScript utilities

PATH-AGNOSTIC CLASS LOADING

CRITICAL CONCEPT: Classes found by name, not path.

How It Works

  1. Framework automatically indexes /rsx/
  2. Classes referenced by name
  3. Framework locates files
  4. Automatic loading on first reference

Implications

  • Move files freely - Location doesn't matter
  • No path management - No use statements needed (auto-generated)
  • Name-based referencing
  • Uniqueness required

Why This Matters

  • Path independence - Move files without breaking references
  • Refactoring safety - Rename files without updating imports
  • Simplicity - No import management overhead
// ✅ GOOD
$user = User_Model::find(1);
$route = Rsx::Route('Frontend_Index_Controller');

// ❌ BAD
use Rsx\Models\User_Model;  // Auto-generated
require_once 'models/user_model.php';  // Never

BLADE DIRECTIVES

@rsx_id - Define View Identity

Every Blade view starts with @rsx_id() - path-independent identity:

@rsx_id('Frontend_Index')

<!DOCTYPE html>
<html>
  <body class="{{ rsx_body_class() }}">
    @yield('content')
  </body>
</html>

Convention: Match related class names.

@rsx_include - Include Views

Include by RSX ID, not path:

{{-- ✅ GOOD --}}
@rsx_include('Layouts_Main_Footer')

{{-- ❌ BAD --}}
@include('theme.layouts.footer')

Why This Matters

  • Path independence - Move files without breaking includes
  • Refactoring safety - Rename without updating references
  • Clear dependencies - See view relationships

SCSS FILE CONVENTIONS

Paired with Views

SCSS files paired with views:

  1. Match view name (snake_case)
  2. Wrap rules in class matching @rsx_id
  3. Store in same directory
<!-- frontend_index.blade.php -->
@rsx_id('Frontend_Index')
/* frontend_index.scss */
.Frontend_Index {
  .content {
    padding: 20px;
  }
}

Why Wrap in Class?

  • Scoping - Prevents conflicts
  • Specificity - Applies only when view rendered
  • Organization - Clear ownership

rsx_body_class() Integration

{{ rsx_body_class() }} adds RSX ID as body class:

<body class="{{ rsx_body_class() }}">  <!-- Renders: <body class="Frontend_Index"> -->

JAVASCRIPT FILE CONVENTIONS

Auto-Initialization Lifecycle

on_app_ready()

DOM ready, scripts loaded:

class Frontend_Index {
    static async on_app_ready() {
        // jQuery's $(document).ready() equivalent
        Frontend_Index.init_sidebar();
    }
}

on_jqhtml_ready()

All jqhtml components loaded:

class Dashboard_Index {
    static async on_jqhtml_ready() {
        // Safe to interact with components
        const card = $('#user-card').component();
    }
}

JavaScript Requires Bundles

CRITICAL: JS only executes when bundle rendered in HTML.

  • Without bundles → No JS execution
  • JSON responses → No JS
  • Views without bundles → No JS

Always render bundle:

return rsx_view('Frontend_Index', [
    'bundle' => Frontend_Bundle::render()
]);

View Functions:

  • rsx_view() - Use RSX ID (Upper_Case matching class name)
  • Laravel's view() - Not recommended, use rsx_view() instead

Naming Pattern:

  • Filename: frontend_index.blade.php (snake_case)
  • RSX ID: @rsx_id('Frontend_Index') (Upper_Case)
  • Controller: Frontend_Index_Controller (Upper_Case)
  • View call: rsx_view('Frontend_Index') (Uses RSX ID, not filename)

ROUTING SYSTEM

Route Definition

class Frontend_Index_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()
        ]);
    }
}

Route Parameters

Use :param syntax (not Laravel's {param}):

#[Route('/users/:id')]
public static function show_user(Request $request, array $params = [])
{
    $user_id = $params['id'];  // Route parameter
}

Method Restrictions

Only GET and POST allowed. PUT, PATCH, DELETE throw exceptions.

Type-Safe URL Generation

PHP:

$url = Rsx::Route('Frontend_Index_Controller')->url();
$url = Rsx::Route('Users_Controller', 'show')->url(['id' => 123]);
Rsx::Route('Dashboard_Controller')->navigate(); // Redirect

JavaScript:

const url = Rsx.Route('Frontend_Index_Controller').url();
Rsx.Route('Dashboard_Controller').navigate(); // Sets location.href

$params Array

Contains route parameters and query string parameters:

// Route: /users/:id
// Request: /users/123?tab=profile

$params['id'];    // "123" (route)
$params['tab'];   // "profile" (query)

AUTHENTICATION & AUTHORIZATION

RsxAuth API

Always use RsxAuth - Never Laravel Auth or $_SESSION:

use App\RSpade\Core\Auth\RsxAuth;

RsxAuth::check();     // Is authenticated
RsxAuth::user();      // User model or null
RsxAuth::id();        // User ID or null
RsxAuth::session();   // Session model or null

Session System

Sessions always persist (365 days) - Never implement "Remember Me" checkboxes.

#[Auth] Attribute

CRITICAL: All routes MUST have at least one #[Auth] attribute.

Syntax

#[Auth('Permission::method_name()',
    message: 'Optional error',
    redirect: '/optional/url')]       // HTTP only

Built-in Methods

Permission::anybody()         // Public routes
Permission::authenticated()   // Require login

Examples

#[Auth('Permission::anybody()')]
#[Route('/')]
public static function index(Request $request, array $params = []) {}

#[Auth('Permission::authenticated()', redirect: '/login')]
#[Route('/dashboard')]
public static function dashboard(Request $request, array $params = []) {}

// Multiple requirements (all must pass)
#[Auth('Permission::authenticated()')]
#[Auth('Permission::has_role("admin")')]
#[Route('/admin')]
public static function admin_panel(Request $request, array $params = []) {}

Controller-Wide Protection

#[Auth('Permission::authenticated()')]
public static function pre_dispatch(Request $request, array $params = [])
{
    return null;  // Continue
}

Custom Permission Methods

Add to /rsx/permission.php:

class Permission extends Permission_Abstract
{
    public static function has_role(Request $request, array $params, string $role): mixed
    {
        if (!RsxAuth::check()) return false;
        return RsxAuth::user()->user_role_id === constant('User_Model::ROLE_' . strtoupper($role));
    }
}

Usage: #[Auth('Permission::has_role("admin")')]

Ajax Endpoint Auth

For Ajax, redirect ignored. Returns JSON:

{
    "success": false,
    "error": "Login required",
    "error_type": "permission_denied"
}

Where to Check Authentication

  1. Controller pre_dispatch() - All routes in controller
  2. /rsx/main.php pre_dispatch() - URL pattern matching
  3. Route method - Route-specific

BUNDLE SYSTEM

One bundle mandatory per page. Contains all assets and model stubs.

Creating Bundle

class Frontend_Bundle extends Rsx_Bundle_Abstract
{
    public static function define(): array
    {
        return [
            'include' => [
                'jquery',                      // Required
                'lodash',                      // Required
                'bootstrap5_src',              // Bundle alias
                'rsx/theme/variables.scss',    // Specific file
                'rsx/app/frontend',            // Directory (all files)
                'rsx/models',                  // Models (for JS stubs)
            ],
        ];
    }
}

Unified Include System

Auto-detects:

  1. Module Aliases - jquery, lodash, bootstrap5, jqhtml
  2. Bundle Aliases - From config/rsx.php
  3. Bundle Classes - Full class names
  4. Files - Specific files
  5. Directories - All files in directory

Include Order Matters

'include' => [
    'rsx/theme/variables.scss',  // Variables first
    'rsx/theme/components',      // Components using variables
    'rsx/app/frontend',          // Pages using components
],

Automatic Compilation

Bundles compile automatically when you reload the page. No manual build steps required during development.

To verify a bundle compiles successfully (troubleshooting only):

php artisan rsx:bundle:compile Frontend_Bundle

Rendering

<!DOCTYPE html>
<html>
  <head>
    {!! Frontend_Bundle::render() !!}
  </head>
</html>

JQHTML COMPONENTS

Philosophy: Semantic-First Design

JQHTML is designed for mechanical thinkers - developers who think in terms of structure, logic, and data flow rather than visual design.

From the creator:

"I think much more mechanically than visually, so UI has always been a struggle for me, which is why I designed JQHTML. The goal is to composite concepts in HTML documents, rather than actual HTML elements with cryptic class names and mental mapping overhead."

Traditional approach:

<div class="container-fluid">
  <div class="card mb-3">
    <div class="card-body">
      <h5 class="card-title">User Profile</h5>
    </div>
  </div>
</div>

JQHTML approach:

<Page_Container>
  <User_Profile_Card />
</Page_Container>

Write <User_Card> not <div class="card">. Name things what they ARE, not how they look.

Incremental Scaffolding

Undefined components work immediately - they render as <div> with the component name as a class.

<!-- Write this first - works immediately -->
<Page>
  <Page_Title>Client Details</Page_Title>
  <Client_Info />
  <Client_Activity />
  <Client_Actions>
    <Edit_Button />
    <Delete_Button />
  </Client_Actions>
</Page>

Renders as:

<div class="Page">
  <div class="Page_Title">Client Details</div>
  <div class="Client_Info"></div>
  <div class="Client_Activity"></div>
  <div class="Client_Actions">
    <div class="Edit_Button"></div>
    <div class="Delete_Button"></div>
  </div>
</div>

Define components incrementally as needed. Page structure is separate from visual implementation.

Overview

jQuery-based component system with templates (.jqhtml), classes (Jqhtml_Component), lifecycle management, and data binding.

Purpose

Abstract logical elements into reusable components for consistent design and composable architecture.

Creating Components

php artisan rsx:app:component:create --name=user_card --path=rsx/theme/components

Creates user_card.jqhtml and User_Card.js.

Template Syntax

Templates compile to JavaScript render functions at build time (not runtime parsing).

🔴 CRITICAL: <Define> IS the Element (Not a Wrapper)

The <Define:ComponentName> tag becomes the HTML element itself, not a wrapper around content. This is fundamentally different from React/Vue where components return wrapped JSX.

<!-- ✅ CORRECT - Define tag becomes the button element -->
<Define:Save_Button tag="button" class="btn btn-primary">
  <i class="fa fa-save"></i> Save Changes
</Define:Save_Button>

<!-- Renders as: -->
<button class="Save_Button Jqhtml_Component btn btn-primary">
  <i class="fa fa-save"></i> Save Changes
</button>
<!-- ❌ WRONG - Unnecessary wrapper div -->
<Define:Save_Button>
  <button class="btn btn-primary">
    <i class="fa fa-save"></i> Save Changes
  </button>
</Define:Save_Button>

<!-- Renders as nested elements (BAD): -->
<div class="Save_Button Jqhtml_Component">
  <button class="btn btn-primary">
    <i class="fa fa-save"></i> Save Changes
  </button>
</div>

Key Behaviors:

  • Defaults to <div> if no tag="" attribute specified
  • Component name and Jqhtml_Component classes added automatically - never add these manually
  • Bootstrap/styling classes go directly on <Define> tag - not on inner elements
  • Never add wrapper divs unless semantically necessary (layout grids, semantic HTML)

Common tag="" Values:

  • tag="button" - For clickable actions
  • tag="a" - For links (use with href="" attribute)
  • tag="span" - For inline text elements
  • tag="article" - For content cards
  • tag="section" - For page sections
  • tag="ul" or tag="ol" - For lists
  • tag="form" - For form components

Define Tag Attribute Types:

The <Define> tag supports three types of attributes:

  1. extends="Parent_Component" - Explicit template inheritance (alternative to class-based inheritance)
  2. $property=value - Set default this.args values in the template
  3. Regular HTML attributes - Standard attributes like class, id, data-*, etc.

Setting Default Args with $ Attributes:

Use $ prefix on <Define> tag to set default this.args values:

<!-- Template-only component with defaults -->
<Define:User_Card $handler=User_Controller.fetch_user $theme="light">
  <div class="card theme-<%= this.args.theme %>">
    <h3><%= this.data.name %></h3>
  </div>
</Define:User_Card>
// No backing class needed - template configures component
// When instantiated: <User_Card user_id="123" />
// Component automatically has:
//   this.args.handler = User_Controller.fetch_user (function reference)
//   this.args.theme = "light" (string literal)
//   this.args.user_id = "123" (from instantiation)

Quoted vs Unquoted $ Attributes:

  • Unquoted = Raw JavaScript expression (function refs, identifiers)
  • Quoted = String literal
<Define:Data_Grid
  $api=Product_Controller.list_products    <!-- Function reference -->
  $per_page=25                              <!-- Number -->
  $theme="dark"                             <!-- String literal -->
>
  ...
</Define:Data_Grid>

Use Cases:

  • Template-only components - Full component behavior without JavaScript class
  • Reusable configurations - Base component with configurable controller
  • Default values - Sensible defaults overridable at instantiation

Component Definition:

<Define:User_Card tag="article" class="card">
    <h3><%= this.data.name %></h3>
    <span class="badge <%= this.data.status_badge %>">
        <%= this.data.status_label %>
    </span>
</Define:User_Card>

Interpolation:

  • <%= expression %> - Escaped output (safe, use by default)
  • <%== expression %> - Unescaped HTML (only for pre-sanitized content)
  • <%-- comment --%> - Template comments (not rendered)

Control Flow (compiles to JavaScript):

<% if (this.data.active) { %>
  <div>Active User</div>
<% } %>

<% for (let item of this.data.items) { %>
  <div><%= item.name %></div>
<% } %>

Similar to PHP templating - write JavaScript directly in templates for loops and conditionals.

JavaScript Class

class User_Card extends Jqhtml_Component {
    async on_load() {
        // ✅ CRITICAL: ONLY modify this.data in on_load()
        // ✅ Use Ajax endpoint pattern: await Controller.method()
        this.data = await User_Controller.get_user_data({user_id: this.args.userId});

        // ❌ FORBIDDEN: No DOM manipulation in on_load()
        // this.$id('title').text(this.data.name);  // WRONG
        // this.$.addClass('loaded');               // WRONG

        // ❌ FORBIDDEN: No other properties besides this.data
        // this.state = {loading: false};           // WRONG
        // this.config = {...};                     // WRONG
    }

    on_ready() {
        // ✅ CORRECT: DOM manipulation in on_ready()
        this.$id('title').text(this.data.name);
        this.$.on('click', () => {
            console.log('Card clicked:', this.data.name);
        });
    }
}

🔴 CRITICAL on_load() RULES

HARD REQUIREMENTS - NO EXCEPTIONS:

  1. ONLY modify this.data - This is the ONLY property you may set in on_load()

    • CORRECT: this.data = await Controller.method()
    • CORRECT: this.data.field = value
    • FORBIDDEN: this.state = ... (not allowed)
    • FORBIDDEN: this.config = ... (not allowed)
    • FORBIDDEN: this.cache = ... (not allowed)
    • FORBIDDEN: this.loaded = ... (not allowed)
  2. Use Ajax endpoint pattern - NEVER use $.ajax() or fetch() for local server calls

    • CORRECT: await Controller_Name.method_name(params)
    • FORBIDDEN: await $.ajax({url: '/api/...'})
    • FORBIDDEN: await fetch('/api/...').then(r => r.json())
  3. NO DOM manipulation - Save ALL DOM operations for on_ready()

    • FORBIDDEN: this.$id('element').text(...)
    • FORBIDDEN: this.$.addClass(...)
    • FORBIDDEN: Any jQuery/DOM operations

Why these restrictions exist:

  • this.data triggers automatic re-render when modified
  • DOM may not be fully initialized during on_load()
  • Siblings execute in parallel - unpredictable DOM state
  • Setting other properties breaks the component lifecycle

If you violate these rules, components will behave unpredictably.

Component Lifecycle

Five-stage lifecycle: render → on_render → create → load → ready

Execution Flow

render (top-down)

  • Template executes
  • DOM created
  • First render: this.data = {} (empty)
  • Not overridable

on_render() (top-down, immediately after render)

  • Fires BEFORE children ready
  • Hide uninitialized elements
  • Set initial visual state
  • Prevents flash of uninitialized content

on_create() (bottom-up)

  • Sync initialization
  • Set instance properties
  • Quick setup only

on_load() (bottom-up, siblings in parallel)

  • PRIMARY PURPOSE: Load async data into this.data
  • HARD REQUIREMENT: ONLY modify this.data - NO other properties allowed
  • CRITICAL: NO DOM manipulation - Save for on_ready()
  • NO child component access - Children may not be ready yet
  • Use Ajax endpoint pattern: await Controller.method() NOT $.ajax()
  • NO this.state, NO this.config, NO this.cache - ONLY this.data
  • Siblings execute in parallel

on_ready() (bottom-up)

  • All children guaranteed ready
  • Safe for DOM manipulation
  • Attach event handlers
  • Initialize plugins

Depth-Ordered Execution

  • Top-down: render, on_render (parent before children)
  • Bottom-up: on_create, on_load, on_ready (children before parent)
  • Parallel: Siblings at same depth level process simultaneously during on_load()
class User_Card extends Jqhtml_Component {
    on_render() {
        // Fires immediately after render, before children
        // Hide until data loads to prevent visual glitches
        this.$.css('opacity', '0');
    }

    async on_load() {
        // ✅ CORRECT - Use Ajax endpoint pattern
        this.data = await User_Controller.get_user_data({user_id: this.args.user_id});

        // ❌ FORBIDDEN - No $.ajax() calls
        // this.data = await $.ajax({url: '/api/users/...'});  // WRONG

        // ❌ FORBIDDEN - No fetch() calls to local server
        // this.data = await fetch('/api/users/...').then(r => r.json());  // WRONG

        // ❌ FORBIDDEN - No DOM manipulation
        // this.$id('title').text(this.data.name);  // WRONG
        // this.$.addClass('loaded');               // WRONG

        // ❌ FORBIDDEN - No other properties besides this.data
        // this.state = {loading: false};           // WRONG
        // this.cache = this.data;                  // WRONG
    }

    on_ready() {
        // All children ready, safe for DOM
        this.$id('title').text(this.data.name);
        this.$.animate({opacity: 1}, 300);
    }
}

🔴 CRITICAL: Loading State Pattern

NEVER manually call this.render() in on_load() - The framework handles re-rendering automatically.

The correct pattern for loading states:

  1. Use simple this.data.loaded flag at the END of on_load()
  2. Check !this.data.loaded in template for loading state
  3. Trust automatic re-rendering - Don't call this.render() manually
  4. NO nested state objects - Don't use this.data.state.loading

INCORRECT Pattern (common mistakes):

class Product_List extends Jqhtml_Component {
    async on_load() {
        // ❌ WRONG: Setting loading state at START
        this.data.state = {loading: true};
        this.render();  // ❌ WRONG: Manual render call

        const response = await $.ajax({...});  // ❌ WRONG: $.ajax() instead of controller stub

        if (response.success) {
            this.data = {
                records: response.records,
                state: {loading: false}  // ❌ WRONG: Complex nested state
            };
        }
    }
}

Template for incorrect pattern:

<% if (this.data.state && this.data.state.loading) { %>
    <!-- ❌ WRONG: Complex state check -->
    Loading...
<% } %>

CORRECT Pattern:

class Product_List extends Jqhtml_Component {
    async on_load() {
        // ✅ CORRECT: NO loading flags at start
        // ✅ CORRECT: NO manual this.render() calls

        // ✅ CORRECT: Use Ajax endpoint pattern
        const response = await Product_Controller.list_products({
            page: 1,
            per_page: 25
        });

        // ✅ CORRECT: Populate this.data directly
        this.data.records = response.records;
        this.data.total = response.total;
        this.data.page = response.page;

        // ✅ CORRECT: Simple flag at END
        this.data.loaded = true;

        // ✅ Automatic re-render happens because this.data changed
    }
}

Template for correct pattern:

<Define:Product_List class="product-list">
    <% if (!this.data || !this.data.loaded) { %>
        <!-- ✅ FIRST RENDER: Loading state (this.data is empty {}) -->
        <div class="loading-spinner text-center py-5">
            <div class="spinner-border text-primary mb-3" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
            <p class="text-muted">Loading products...</p>
        </div>
    <% } else if (this.data.records && this.data.records.length > 0) { %>
        <!-- ✅ SECOND RENDER: Data loaded -->
        <div class="table-responsive">
            <table class="table">
                <% for (let record of this.data.records) { %>
                    <tr><td><%= record.name %></td></tr>
                <% } %>
            </table>
        </div>
    <% } else { %>
        <!-- ✅ Empty state -->
        <div class="text-center py-5">
            <p class="text-muted">No records found</p>
        </div>
    <% } %>
</Define:Product_List>

Why this pattern works:

  1. First render: this.data = {} (empty) → Template shows loading state
  2. on_load() executes: Populates this.data.records and sets this.data.loaded = true
  3. Automatic re-render: Framework detects this.data changed and re-renders
  4. Second render: this.data.loaded === true → Template shows data

Key principles:

  • Trust the framework - Automatic re-rendering when this.data changes
  • Simple is better - Use flat this.data.loaded flag, not nested state objects
  • Check at template level - !this.data.loaded in jqhtml, not JS-side logic
  • Never call this.render() manually in on_load() - breaks the lifecycle

Double-Render Pattern

Components may render TWICE if on_load() modifies this.data:

  1. First render: this.data = {} (empty)
  2. on_load() populates this.data
  3. Automatic re-render with populated data
  4. on_ready() fires after second render

Use for loading states:

<Define:Product_List class="product-list">
  <% if (Object.keys(this.data).length === 0) { %>
    <!-- FIRST RENDER: Loading state -->
    <div class="loading-spinner">Loading products...</div>
  <% } else { %>
    <!-- SECOND RENDER: Actual data -->
    <% for (let product of this.data.products) { %>
      <Product_Card $product_id=product.id />
    <% } %>
  <% } %>
</Define:Product_List>
class Product_List extends Jqhtml_Component {
    on_render() {
        // Fires TWICE: before and after data load
        console.log('Rendered, empty?', Object.keys(this.data).length === 0);
    }

    async on_load() {
        // ✅ CORRECT - Use Ajax endpoint pattern
        const result = await Product_Controller.list_products({page: 1, per_page: 25});
        this.data.products = result.products;
        this.data.total = result.total;
        // Automatic re-render happens because this.data changed
    }

    on_ready() {
        // Fires ONCE after second render
        console.log('Ready with', this.data.products.length, 'products');
    }
}

Using in Blade

<User_Card user_id="123" theme="dark" />  {{-- Self-closing only --}}

Attribute Syntax - CRITICAL

$ prefix = passed to this.args

Quoted = Literal Strings

<UserCard $title="User Profile" />
<!-- this.args.title = "User Profile" (string) -->

Unquoted = JavaScript Expressions

<UserCard $user=this.data.user />
<!-- this.args.user = {user object} -->

<UserCard $count=42 />
<!-- this.args.count = 42 (number) -->

<UserCard $active=true />
<!-- this.args.active = true (boolean) -->

Complex Expressions

<UserCard $status=(user.active ? 'online' : 'offline') />
<UserCard $data=({...this.user, modified: true}) />

Implementation Detail: $ attributes also create corresponding data- HTML attributes (vestigial from v1).

Event Binding with @ Prefix

Bind DOM events directly in templates using @event=this.method syntax:

<Define:Button tag="button" @click=this.handle_click @mouseover=this.handle_hover>
  <%= content() %>
</Define:Button>
class Button extends Jqhtml_Component {
    handle_click(event) {
        console.log('Clicked', event);
        this.$.addClass('clicked');
    }

    handle_hover(event) {
        this.$.addClass('hovered');
    }
}

Common events: @click, @change, @submit, @focus, @blur, @keyup, @keydown, @mouseover

⚠️ Note: Verify @event binding functionality in your jqhtml version.

this.args vs this.data

this.args - Input parameters (read-only)

  • Source: Component attributes
  • Purpose: Configuration, IDs, callbacks

this.data - Loaded data (mutable, starts as {})

  • Source: on_load() method
  • Purpose: API data, computed values
  • Starts empty: Check Object.keys(this.data).length === 0 for loading state
// <User_Card user_id="123" theme="dark" />
class User_Card extends Jqhtml_Component {
    async on_load() {
        // ✅ CRITICAL: this.data starts as {} (empty object)
        // ✅ ONLY modify this.data - NO other properties
        this.data = await User_Controller.get_user_data({user_id: this.args.user_id});
        // Now this.data = {name: "John", email: "..."}

        // ❌ FORBIDDEN: Setting other properties
        // this.state = {loading: false};  // WRONG - not allowed
        // this.loaded = true;              // WRONG - not allowed
    }

    on_ready() {
        console.log(this.data.name);   // ✅ Loaded data
        console.log(this.args.theme);  // ✅ Input param (read-only)
    }
}

Components ARE jQuery

this.$ is a genuine jQuery object, not a wrapper. All jQuery methods work directly.

class Dashboard extends Jqhtml_Component {
    on_ready() {
        // All jQuery methods available
        this.$.addClass('active');
        this.$.css('background', '#f0f0f0');
        this.$.fadeIn(300);
        this.$.on('click', () => this.handle_click());

        // Traversal
        this.$.find('.item').addClass('selected');
        this.$.parent().addClass('has-dashboard');

        // Manipulation
        this.$.append('<div>New content</div>');
        this.$.empty();
    }
}

No abstraction layers, no virtual DOM - direct jQuery manipulation.

Scoped IDs with this.$id()

Use $id attribute for component-scoped element IDs.

Template:

<Define:User_Card class="card">
  <h3 $id="title">Name</h3>
  <p $id="email">Email</p>
  <button $id="edit_btn">Edit</button>
</Define:User_Card>

Rendered HTML (automatic scoping):

<div class="User_Card Jqhtml_Component card">
  <h3 id="title:c123">Name</h3>
  <p id="email:c123">Email</p>
  <button id="edit_btn:c123">Edit</button>
</div>

Access with this.$id():

class User_Card extends Jqhtml_Component {
    on_ready() {
        // ✅ CORRECT - Use logical name
        this.$id('title').text('John Doe');
        this.$id('email').text('john@example.com');
        this.$id('edit_btn').on('click', () => this.edit());

        // ❌ WRONG - Don't construct full ID manually
        $('#title:c123').text('John Doe');  // Fragile
    }
}

Why scoped IDs: Multiple component instances need unique IDs without conflicts.

Nesting Components

<Define:User_List class="user-list">
    <% for (let user of this.data.users) { %>
        <User_Card $user=user $theme=this.args.theme />
    <% } %>
</Define:User_List>

Parent-Child Communication

Pass callbacks as component parameters for child-to-parent communication:

<Define:Product_List class="product-grid">
  <% for (let product of this.data.products) { %>
    <Product_Card
      $product=product
      $on_delete=this.handle_delete
      $on_edit=this.handle_edit
    />
  <% } %>
</Define:Product_List>
class Product_List extends Jqhtml_Component {
    async on_load() {
        // ✅ CORRECT - Use Ajax endpoint pattern
        const result = await Product_Controller.list_products({page: 1});
        this.data.products = result.products;
    }

    async handle_delete(product_id) {
        // ✅ CORRECT - Use Ajax endpoint for deletion
        await Product_Controller.delete_product({id: product_id});

        // Update this.data and re-render
        this.data.products = this.data.products.filter(p => p.id !== product_id);
        this.render();
    }

    handle_edit(product_id) {
        Rsx.Route('Product_Edit_Controller').navigate({id: product_id});
    }
}
class Product_Card extends Jqhtml_Component {
    on_ready() {
        this.$id('delete_btn').on('click', () => {
            if (confirm('Delete this product?')) {
                // Call parent's callback
                this.args.on_delete(this.args.product.id);
            }
        });

        this.$id('edit_btn').on('click', () => {
            this.args.on_edit(this.args.product.id);
        });
    }
}

content() - Inner Content and Named Slots

Default content (single content area - 95% of use cases):

<Define:Panel class="panel">
    <div class="panel-header"><%= this.args.title %></div>
    <div class="panel-body"><%= content() %></div>
</Define:Panel>

<!-- Usage: -->
<Panel $title="User Info">
    <p>Content goes here</p>
</Panel>

Named slots (multiple content areas - 5% of use cases):

Child template uses content('slotname') to render named slots:

<Define:Card_Layout>
  <div class="card">
    <div class="card-header"><%= content('header') %></div>
    <div class="card-body"><%= content('body') %></div>
    <div class="card-footer"><%= content('footer') %></div>
  </div>
</Define:Card_Layout>

Parent provides content using <#slotname> tags:

<Card_Layout>
  <#header><h3>User Profile</h3></#header>
  <#body>
    <p>Name: <%= this.data.name %></p>
    <p>Email: <%= this.data.email %></p>
  </#body>
  <#footer>
    <button class="btn btn-primary">Save</button>
  </#footer>
</Card_Layout>

Critical: Cannot mix content() with named slots. If using named slots, ALL content must be in <#slotname> tags.

Slot-Based Template Inheritance (v2.2.108+)

When a component template contains ONLY slots (no HTML), it automatically inherits the parent class template structure.

This enables abstract base components with customizable slots, allowing child classes to extend parent templates without duplicating HTML structure.

Parent Template (DataGrid_Abstract.jqhtml):

<Define:DataGrid_Abstract class="card">
  <table class="table">
    <thead><tr><%= content('header') %></tr></thead>
    <tbody>
      <% for (let record of this.data.records) { %>
        <tr><%= content('row', record) %></tr>
      <% } %>
    </tbody>
  </table>
</Define:DataGrid_Abstract>

Parent Class (Users_DataGrid.js):

class Users_DataGrid extends DataGrid_Abstract {
  async on_load() {
    const result = await User_Controller.list_users({page: 1});
    this.data.records = result.users;
    this.data.loaded = true;
  }
}

Child Template - Slot-Only (Users_DataGrid.jqhtml):

<Define:Users_DataGrid>
  <#header>
    <th>ID</th>
    <th>Name</th>
    <th>Email</th>
  </#header>

  <#row>
    <td><%= row.id %></td>
    <td><%= row.name %></td>
    <td><%= row.email %></td>
  </#row>
</Define:Users_DataGrid>

Result: Users_DataGrid renders using DataGrid_Abstract structure with customized slot content.

Data Passing to Slots: Parents pass data to slots via second parameter: content('slotname', data)

<!-- Parent passes data -->
<% for (let record of this.data.records) { %>
  <tr><%= content('row', record) %></tr>
<% } %>

<!-- Child receives data via slot parameter -->
<#row>
  <td><%= row.id %></td>
  <td><%= row.name %></td>
</#row>

Reserved Word Validation: Slot names cannot be JavaScript reserved words. Parser rejects with fatal error:

<#function>Content</#function>  <!-- ❌ "function" is reserved -->
<#if>Content</#if>                <!-- ❌ "if" is reserved -->
<#header>Content</#header>        <!-- ✅ Valid -->

Requirements:

  • Child template contains ONLY slot definitions (no HTML wrapper)
  • JavaScript class extends parent class: class Child extends Parent
  • Parent template defines slots using content('slotname') or content('slotname', data)

Use Cases:

  • Abstract data tables with customizable columns
  • Page layouts with variable content sections
  • Card variations with different headers/bodies/footers
  • Form patterns with customizable field sets

DOM Class Convention

All components have Jqhtml_Component class automatically:

const components = $('.Jqhtml_Component');
if ($element.hasClass('Jqhtml_Component')) {
    const component = $element.component();
}

Component names also added as classes for CSS targeting.

Getting Component Instance from DOM

Use .component() method on jQuery objects to get component instance:

// From element reference
const card = $('#user-card-123').component();
card.data.name;  // Access component data
card.reload_data();  // Call component methods

// From any parent
$('.container').find('.User_Card').each(function() {
    const component = $(this).component();
    console.log(component.args.user_id);
});

// Check if element is a component
if ($element.hasClass('Jqhtml_Component')) {
    const component = $element.component();
    // Access component properties and methods
}

Lifecycle Event Callbacks

External code can register callbacks:

$('#my-component').component().on('ready', (component) => {
    console.log('Ready:', component);
});

// Chain events
component
    .on('render', () => console.log('Rendered'))
    .on('ready', () => console.log('Ready'));

Supported events: render, create, load, ready

Lifecycle Manipulation Methods

Components provide methods to control lifecycle and state after initial render.

render()

Synchronous template re-render - Updates DOM with current this.data.

class Product_Card extends Jqhtml_Component {
    update_price(new_price) {
        this.data.price = new_price;
        this.render();  // Template re-renders immediately
    }
}
  • Re-executes template with current this.data
  • Calls on_render() hook after render
  • Does NOT trigger on_load() or on_ready()
  • Synchronous - completes immediately
  • Use when: Data changed, UI needs updating

reload_data()

Re-fetch data and update - Re-runs on_load(), re-renders, calls on_ready().

class User_Card extends Jqhtml_Component {
    async refresh_user_data() {
        await this.reload_data();
        console.log('Updated');
    }
}
  • Re-runs on_load() to fetch fresh data
  • Automatically re-renders template
  • Calls on_ready() after re-render
  • Returns promise - await for completion
  • Use when: Need fresh data from API

reinitialize()

Full component reset - Restarts entire lifecycle from stage 0.

class Dashboard extends Jqhtml_Component {
    async switch_user(new_user_id) {
        this.args.user_id = new_user_id;
        await this.reinitialize();
    }
}
  • Destroys current component state
  • Re-runs full lifecycle: render → on_render → create → load → ready
  • Use when: Component needs complete rebuild
  • Rare use case - usually reload_data() or render() sufficient

destroy()

Component destruction - Removes component and all children from DOM.

class Modal extends Jqhtml_Component {
    close() {
        this.destroy();
    }
}
  • Calls on_destroy() hook if defined
  • Recursively destroys all child components
  • Removes DOM element completely
  • Adds _Component_Destroyed class before removal
  • Synchronous - completes immediately

on_destroy() hook:

class Chat_Widget extends Jqhtml_Component {
    on_destroy() {
        this.socket.disconnect();
        console.log('Chat destroyed');
    }
}

Synchronous Requirements

CRITICAL: These lifecycle methods MUST be synchronous (no async, no await):

Method Synchronous Required
on_create() YES
on_render() YES
on_destroy() YES
on_load() NO (async allowed)
on_ready() NO (async allowed)

Framework needs predictable execution order for lifecycle coordination.

// ✅ CORRECT
class MyComponent extends Jqhtml_Component {
    on_create() {
        this.counter = 0;
    }

    on_destroy() {
        console.log('Destroyed');
    }
}

// ❌ WRONG
class MyComponent extends Jqhtml_Component {
    async on_create() {  // DON'T DO THIS
        await this.setup();
    }
}

Common Pitfalls and Non-Intuitive Behaviors

Most Important:

  • <Define> IS the element itself, not a wrapper - Use tag="" to specify element type (defaults to <div>)
  • Component name and Jqhtml_Component classes added automatically - Never add these manually to <Define> tag

Data and Lifecycle:

  • this.data is the ONLY property allowed in on_load() - NO this.state, this.config, etc.
  • this.data starts as empty object {} - Check !this.data.loaded or Object.keys(this.data).length === 0 for loading state
  • Components render twice if on_load() modifies this.data - First with empty data, second after load
  • Only modify this.data in on_load(), NEVER DOM - DOM manipulation belongs in on_ready()
  • Use Ajax endpoint pattern in on_load() - await Controller.method() NOT $.ajax() or fetch()
  • on_render() fires before children ready - Use to hide uninitialized UI (prevent flash)

Loading Pattern Anti-Patterns:

  • NEVER call this.render() manually in on_load() - Framework re-renders automatically when this.data changes
  • NEVER use nested state objects - Use flat this.data.loaded = true NOT this.data.state.loading = false
  • NEVER set loading flags at START of on_load() - Only set this.data.loaded = true at END
  • Check loading in template - Use <% if (!this.data.loaded) %> for loading state, trust automatic re-rendering

Attributes and Syntax:

  • Quoted vs unquoted $ attributes behave completely differently - Quoted = string literal, unquoted = JS expression
  • Component names must be PascalCase - User_Card not user_card in component definitions
  • Filenames should be snake_case - user_card.jqhtml not UserCard.jqhtml
  • Blade tags are self-closing only - <User_Card /> works, <User_Card></User_Card> doesn't

Synchronous Requirements:

  • on_create(), on_render(), on_destroy() MUST be synchronous - No async/await allowed
  • render() and destroy() are synchronous - Complete before next line executes
  • reload_data() and reinitialize() are async - Must await for completion

RSpade Integration

⚠️ CRITICAL: Components require bundles to function

JavaScript classes and lifecycle methods only execute when included in a bundle rendered in the HTML. Without bundles:

  • No JavaScript execution
  • No on_load(), on_ready(), or other lifecycle methods
  • Components render as static HTML only

Bundle inclusion:

class Frontend_Bundle extends Rsx_Bundle_Abstract
{
    public static function define(): array
    {
        return [
            'include' => [
                'jquery',                    // Required for jqhtml
                'jqhtml',                    // jqhtml runtime
                'rsx/theme/components',      // All .jqhtml components in directory
                'rsx/app/frontend',          // Frontend-specific components
            ],
        ];
    }
}

Automatic discovery: Components in bundle-included directories are auto-discovered and compiled.

Rendering bundles (required for JavaScript to execute):

return rsx_view('Frontend_Index', [
    'bundle' => Frontend_Bundle::render()
]);
<!DOCTYPE html>
<html>
  <head>
    {!! Frontend_Bundle::render() !!}
  </head>
  <body>
    <User_Card user_id="123" />  {{-- JS will execute --}}
  </body>
</html>

Blade usage: Components self-closing only, attributes with $ prefix passed to this.args.

Build-time compilation: Templates compile to JavaScript functions with working sourcemaps. Bundles recompile automatically when you reload the page during development.

$redrawable Attribute - Lightweight Components

Convert any HTML element into a re-renderable component using the $redrawable attribute. This parser-level transformation enables selective re-rendering without creating separate component classes.

How it works:

<!-- Write this: -->
<div $redrawable $id="counter">
    Count: <%= this.data.count %>
</div>

<!-- Parser transforms to: -->
<Redrawable data-tag="div" $id="counter">
    Count: <%= this.data.count %>
</Redrawable>

The Redrawable component renders as the specified tag type (default: div) while providing full component functionality including lifecycle hooks, data management, and selective re-rendering.

Selective re-rendering from parent:

class Dashboard extends Jqhtml_Component {
    async increment_counter() {
        this.data.count++;
        // Re-render only the counter element, not entire dashboard
        this.render('counter');  // Finds child with $id="counter"
    }
}

render(id) delegation syntax:

  • Finds child element with matching $id
  • Verifies element is a component (has $redrawable or is proper component class)
  • Calls its render() method
  • Perfect for live data displays, counters, status indicators, real-time feeds

Error handling:

  • Clear error if ID doesn't exist
  • Clear error if element isn't configured as component
  • Guides developers to correct usage

Use cases:

  • Live counters and metrics
  • Status indicators that update independently
  • Real-time data feeds
  • Dynamic lists that change frequently
  • Any element needing selective updates without full page re-render

Component Documentation Standard

Document jqhtml components using HTML comments at top of .jqhtml files. Treat <Define> like a function signature - document inputs (arguments), outputs (data/methods), and extension points (content blocks).

Basic format:

<!--
Component_Name

$required_arg - Description of required argument
$optional="default" - Optional argument with default value

this.data.field - Data available after on_load()
this.args.param - Input parameters from component attributes

method_name() - Public methods for external interaction

CONTENT BLOCKS:
<Block:Custom_Block>
    Extension point for customization
    Variables: Custom_Block.variable_name
-->

Simple components need minimal docs, complex ones need more detail. Only document what's relevant. Complete documentation standard: php artisan rsx:man jqhtmldoc

Examples:

Simple component:

<!--
Text_Input

$name="field_name" - Form field name
$value="initial" - Initial value
$placeholder="" - Input placeholder (optional)
-->
<Define:Text_Input tag="input" type="text" class="form-control"
    name="<%= this.args.name %>"
    value="<%= this.args.value %>"
    placeholder="<%= this.args.placeholder || '' %>"
/>

Complex component:

<!--
DataGrid

$api="Controller" - Controller with datagrid_fetch() endpoint
$per_page="25" - Rows per page (default: 25)

this.data.records - Array of records from API
this.data.total - Total record count

reload_data() - Refresh grid data
goto_page(n) - Navigate to page
sort_by(column) - Sort by column

CONTENT BLOCKS:
<Block:Datagrid_Row_Block>
    Custom row rendering
    Variables: Datagrid_Row_Block.record, Datagrid_Row_Block.index
-->

MODELS & DATABASE

Model Definition

class User_Model extends Rsx_Model_Abstract
{
    protected $table = 'users';
    protected $fillable = [];  // Always empty

    public static $enums = [
        'status_id' => [
            1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active'],
            2 => ['constant' => 'STATUS_INACTIVE', 'label' => 'Inactive'],
        ],
    ];
}

No Mass Assignment

Always explicit:

// ✅ CORRECT
$user = new User_Model();
$user->email = $email;
$user->status_id = User_Model::STATUS_ACTIVE;
$user->save();

// ❌ WRONG
$user = User_Model::create(['email' => $email]);

Model Enums

Type-safe field values with constants, labels, custom properties.

Defining

public static $enums = [
    'status_id' => [
        1 => [
            'constant' => 'STATUS_PUBLISHED',
            'label' => 'Published',
            'badge' => 'bg-success',      // Custom
            'selectable' => true,         // Show in dropdowns
        ],
    ],
];

PHP Constants (Auto-Generated)

php artisan rsx:migrate:document_models
const STATUS_PUBLISHED = 1;
$article->status_id = Article_Model::STATUS_PUBLISHED;

Magic Properties

$article->status_id = 1;
echo $article->status_label;        // "Published"
echo $article->status_badge;        // "bg-success"

Static Methods

Article_Model::status_id_enum();        // All definitions
Article_Model::status_id_enum_select(); // Dropdown options
Article_Model::status_id_enum_ids();    // All IDs

JavaScript Integration

const article = await Article_Model.fetch(1);
console.log(article.status_label);
if (article.status_id === Article_Model.STATUS_PUBLISHED) { }

Migrations

Forward-only - No rollbacks, no down().

php artisan make:migration:safe create_articles_table

Never create manually - always use make:migration:safe.

Migration Example

public function up()
{
    DB::statement("
        CREATE TABLE articles (
            id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
            title VARCHAR(255) NOT NULL,
            status_id TINYINT(1) NOT NULL DEFAULT 1,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            INDEX idx_status_id (status_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    ");
}

Commands

php artisan migrate:begin     # Start session
php artisan migrate           # Run
php artisan migrate:commit    # Commit
php artisan migrate:rollback  # Rollback to snapshot (within session)

INTERNAL API (AJAX ENDPOINTS)

Creating Endpoints

class Demo_Controller extends Rsx_Controller_Abstract
{
    #[Auth('Permission::anybody()')]
    #[Ajax_Endpoint]
    public static function get_user_data(Request $request, array $params = [])
    {
        $user = User_Model::find($params['user_id']);
        return $user ? ['success' => true, 'user' => $user] : ['success' => false];
    }
}

Calling from JavaScript

Auto-generated stubs:

const result = await Demo_Controller.get_user_data({user_id: 123});
if (result.success) {
    console.log('User:', result.user);
}

Model Fetch System

Enable secure fetching:

class Product_Model extends Rsx_Model_Abstract
{
    #[Ajax_Endpoint_Model_Fetch]
    public static function fetch($id)
    {
        if (!RsxAuth::check()) return false;
        $product = static::find($id);
        return $product && Permission::can_view($product) ? $product : false;
    }
}

JavaScript:

const product = await Product_Model.fetch(1);
const products = await Product_Model.fetch([1, 2, 3]); // Auto-splits
console.log(product.status_label); // Enum properties available

Security: Each ID authorized individually. Models opt-in with attribute.


JAVASCRIPT DECORATORS

Creating Decorators

Mark with @decorator:

/**
 * @decorator
 */
function logCalls(target, key, descriptor) {
    const original = descriptor.value;
    descriptor.value = function(...args) {
        console.log(`${key} called:`, args);
        return original.apply(this, args);
    };
    return descriptor;
}

Using Decorators

class UserService {
    @logCalls
    static async fetchUser(id) {
        return await fetch(`/api/users/${id}`).then(r => r.json());
    }
}

Built-in @mutex Decorator

Ensures exclusive method execution:

class DataService {
    // Per-instance locking (each instance has its own lock)
    @mutex
    async save_data() {
        // Only one save_data() call per instance at a time
    }

    // Global locking by ID (all instances share the lock)
    @mutex('critical_operation')
    async update_shared_resource() {
        // Only one update across all instances
    }
}

Rules

  1. Static and instance methods supported
  2. Whitelist required - Must have @decorator in JSDoc
  3. Build-time validation

MODULE CREATION COMMANDS

Hierarchy

Module > Submodule > Feature > Subfeature

Command Creates Route
rsx:app:module:create <name> Module + index /name
rsx:app:module:feature:create <m> <f> Feature /m/f
rsx:app:submodule:create <m> <s> Submodule + layout /m/s
rsx:app:subfeature:create <m> <f> <s> Subfeature /m/f/s
rsx:app:component:create --name=x jqhtml component N/A

When to Use

  • Module - Major sections (frontend, backend, dashboard)
  • Feature - Distinct functionality (users, products, settings)
  • Submodule - Semi-independent with own layout
  • Subfeature - Break complex features into pages
  • Component - Reusable UI (cards, forms, modals)

CODE QUALITY & STANDARDS

rsx:check

php artisan rsx:check

Blocks git commits until violations resolved.

Key Rules

  • NoAnimationsRule - No CSS animations/hover on non-actionable elements
  • ThisUsageRule - Enforce that = this in ES6 classes
  • DuplicateCaseFilesRule - Detect same-name different-case files
  • MassAssignmentRule - Prohibit $fillable
  • VarUsageRule - Prohibit var, require let/const

Professional UI Philosophy

RSX applications are serious business tools, not interactive toys.

Hover effects ONLY on actionable elements:

  • Buttons, links, form fields, images, table rows

All other elements remain static. No hover on cards, containers, panels.


ERROR HANDLING

shouldnt_happen()

For "impossible" conditions:

if (!class_exists($expected)) {
    shouldnt_happen("Class {$expected} should be loaded");
}
if (!element) {
    shouldnt_happen(`Element #${id} not found`);
}

Use for: Code paths that should never execute if system works correctly.


CRITICAL: RSPADE CACHE SYSTEM - AUTOMATIC INVALIDATION

DO NOT RUN rsx:clean DURING DEVELOPMENT

STOP: You almost certainly don't need to run this command.

RSpade's caching system automatically invalidates when files change. You do NOT need to clear caches after editing code.

Traditional frameworks (Laravel, Symfony) require manual cache clearing:

  • Laravel: php artisan cache:clear
  • Symfony: php bin/console cache:clear

RSpade is different. The cache system:

  • Auto-detects file changes
  • Invalidates cache entries automatically
  • Provides instant feedback (< 1 second)
  • Rebuilds only what's needed

The Cost of Running rsx:clean

When you run php artisan rsx:clean:

  • Entire cache is destroyed (not just invalidated)
  • ⏱️ 30-60 second rebuild on next request
  • ⚠️ Timeouts during development
  • 🔄 No benefit (changes already visible without clearing)

Correct Development Workflow

# ✅ CORRECT - Instant feedback
1. Edit file (component, controller, view)
2. Save file
3. Reload page in browser
4. See changes immediately (< 1 second)

# ❌ WRONG - 30-60 second delay
1. Edit file
2. php artisan rsx:clean  # DON'T DO THIS
3. Reload page
4. Wait 30-60 seconds for cache rebuild
5. Finally see changes

When to Actually Use rsx:clean

ONLY run this command when:

  • Catastrophic cache corruption (extremely rare)
  • Framework itself was updated (php artisan rsx:framework:pull runs this automatically)
  • Explicitly instructed by error message or RSpade documentation

NEVER use during normal development:

  • After editing a component
  • After fixing an error
  • After adding a route
  • After modifying a bundle
  • "Just to be safe"

Mental Model: Trust the Automation

Think of RSpade cache like:

  • Modern browsers - auto-refresh on file changes (webpack/vite)
  • VS Code intellisense - auto-updates on file save
  • Linux kernel modules - auto-loaded when needed

NOT like:

  • Laravel cache (requires manual clearing)
  • Compiled languages (require rebuild)
  • Docker containers (require restart)

Error Handling

If you get an error after making changes:

# ❌ WRONG RESPONSE
php artisan rsx:clean  # Won't fix code errors!

# ✅ CORRECT RESPONSE
1. Read the error message
2. Fix the error in your code
3. Save the file
4. Reload the page

The error will still be there after clearing cache because it's a code error, not a cache error.

Performance Impact

Typical development session WITHOUT unnecessary rsx:clean:

  • 100 page reloads × 0.2 seconds = 20 seconds total
  • Fast iteration, high productivity

Typical session WITH unnecessary rsx:clean (3 times):

  • 3 cache rebuilds × 40 seconds = 120 seconds wasted
  • 100 page reloads × 0.2 seconds = 20 seconds
  • ⏱️ Total: 140 seconds (2.3 minutes wasted waiting)

Summary for AI Assistants

Core principle: RSpade's cache system is automatic and transparent. Treat it like the Linux kernel - it just works, don't touch it.

Default assumption: If you're thinking about running rsx:clean, don't. The cache doesn't need clearing.

Exception handling: Errors are in YOUR code, not the cache. Fix the code, don't clear the cache.

DEBUGGING TOOLS

rsx:clean - Emergency Cache Clear

⚠️ RARE USE ONLY - Read the section above before using this command.

Clears all caches and forces complete rebuild (30-60 seconds):

php artisan rsx:clean

This is NOT a routine debugging tool. Only use when specifically instructed or after framework updates.

rsx:debug

Use instead of curl. Headless browser with full JS:

php artisan rsx:debug /dashboard
php artisan rsx:debug /dashboard --user=1       # Test as user
php artisan rsx:debug /page --expect-element="#btn"
php artisan rsx:debug /page --full              # Max info

console_debug()

Channel-based logging:

console_debug("AUTH", "Login attempt", $user->id);
console_debug('AJAX', 'Request sent', url);
CONSOLE_DEBUG_FILTER=AUTH php artisan serve

db:query

php artisan db:query "SELECT * FROM users" --json

Always use --json for compact output.


REFACTORING COMMANDS

php artisan rsx:refactor:rename_php_class Old_Name New_Name
php artisan rsx:refactor:rename_php_class_function Class old new
php artisan rsx:refactor:sort_php_class_functions rsx/path/to/class.php

RSX:MAN DOCUMENTATION

php artisan rsx:man <topic>

Topics: ast_sourcecode_parsers, bundle_api, code_quality, coding_standards, console_debug, controller, enums, error_handling, jqhtml, js_decorators, migrations, model_fetch, routing, session


MAIN_ABSTRACT MIDDLEWARE

/rsx/main.php for app-wide hooks:

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
}

Execution Order

  1. Main::init() - Once at bootstrap
  2. Main::pre_dispatch() - Before every route
  3. Controller::pre_dispatch() - Before controller
  4. Route method - If both return null

LARAVEL AVAILABILITY

Base Laravel available but discouraged:

  • Hidden from IDE by default
  • Use only for specific circumstances
  • Everything should be built in /rsx/

Prefer RSX equivalents:

  • RsxAuth instead of Auth
  • Rsx::Route() instead of route()
  • RSX models instead of direct Eloquent

DEPLOYMENT

Storage Directories

Directory Action Purpose
system/storage/rsx-build/ INCLUDE Compiled assets and build cache
system/storage/rsx-tmp/ EXCLUDE Temporary caches (auto-recreates)
system/storage/rsx-locks/ CLEAR Process locks

Checklist

  1. php artisan rsx:check - Fix all violations
  2. Include system/storage/rsx-build/ in deployment
  3. Exclude system/storage/rsx-tmp/ from deployment
  4. Clear system/storage/rsx-locks/ before starting services

Note: Bundles and framework indexes compile automatically on first page load in production.


GATEKEEPER

Password protection for dev/preview:

GATEKEEPER_ENABLED=true
GATEKEEPER_PASSWORD=your_password
GATEKEEPER_TITLE="Development Preview"

Disable in production: GATEKEEPER_ENABLED=false


VS CODE EXTENSION

  • LLMDIRECTIVE folding - Auto-collapse generated code
  • RSX:USE protection - Warnings for auto-generated sections
  • Smart namespace updates - Auto-update on file moves
  • Integrated formatting - Docker-based

Install from .vscode/extensions/.


CRITICAL REMINDERS

  1. Fail loud - No fallback, no silent failures
  2. Static by default - Use static unless instances needed
  3. No defensive coding - Trust core classes exist
  4. One way to do things - No alternative paths
  5. Path-agnostic - Reference by name, not path
  6. Bundles required - JS won't execute without
  7. Run rsx:check - Before committing
  8. Use RsxAuth - Never Laravel Auth or $_SESSION
  9. No mass assignment - Explicit only
  10. Forward-only migrations - No rollbacks

GETTING HELP

Documentation:

php artisan rsx:man <topic>
php artisan rsx:routes
php artisan list rsx

Debugging:

php artisan rsx:debug /page
php artisan rsx:check
php artisan db:query "SQL" --json

Development:

php artisan rsx:app:module:create name
php artisan rsx:app:component:create
php artisan rsx:clean                      # Clear caches (troubleshooting)