🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1707 lines
62 KiB
Plaintext
Executable File
1707 lines
62 KiB
Plaintext
Executable File
JQHTML(3) RSX Framework Manual JQHTML(3)
|
|
|
|
NAME
|
|
JQHTML - jQuery-based HTML component system for semantic UI development
|
|
|
|
SYNOPSIS
|
|
// Blade directive
|
|
<User_Card $name="John" $email="john@example.com" />
|
|
|
|
// PHP helper
|
|
{!! Jqhtml::component('User_Card', ['name' => 'John']) !!}
|
|
|
|
// JavaScript lifecycle class
|
|
class User_Card extends Jqhtml_Component {
|
|
async on_load() {
|
|
// Load async data - ONLY modify this.data
|
|
this.data = await fetch('/api/user/' + this.args.id)
|
|
.then(r => r.json());
|
|
}
|
|
|
|
on_ready() {
|
|
// All children ready, safe for DOM manipulation
|
|
this.$sid('edit').on('click', () => this.edit());
|
|
}
|
|
}
|
|
|
|
DESCRIPTION
|
|
JQHTML is a jQuery-based component system for building semantic,
|
|
reusable UI elements. Unlike complex frameworks requiring state
|
|
management and virtual DOM, JQHTML uses simple HTML templates with
|
|
optional jQuery-based JavaScript classes.
|
|
|
|
Components compile to JavaScript functions at build time (similar to
|
|
PHP templating). The system handles unique IDs, DOM updates, and
|
|
initialization automatically through the RSX manifest system.
|
|
|
|
Key characteristics:
|
|
- Semantic-first design (name what things ARE, not how they LOOK)
|
|
- Templates compile to JavaScript (not JSX, not runtime parsing)
|
|
- Components ARE jQuery objects (this.$ is genuine jQuery)
|
|
- Undefined components work immediately (incremental scaffolding)
|
|
- Five-stage deterministic lifecycle (render → on_render → create → load → ready)
|
|
- Double-render pattern for loading states
|
|
- No build configuration required (auto-discovered)
|
|
|
|
SEMANTIC-FIRST DESIGN
|
|
JQHTML is designed for mechanical thinkers who think structurally
|
|
rather than visually. Components represent logical concepts, not
|
|
visual elements.
|
|
|
|
From the creator:
|
|
"I think mechanically, not visually. JQHTML lets me composite
|
|
concepts in HTML, rather than elements with cryptic class names."
|
|
|
|
Example semantic approach:
|
|
<User_Profile_Card /> # What it IS
|
|
<Product_List /> # Logical concept
|
|
<Stats_Dashboard /> # Semantic name
|
|
|
|
Not visual approach:
|
|
<div class="card"> # How it LOOKS
|
|
<ul class="list"> # Visual element
|
|
<div class="dashboard"> # Generic container
|
|
|
|
Undefined components render as <div> with the component name as a
|
|
class. This enables incremental scaffolding: build page structure
|
|
with semantic names first, define component templates later.
|
|
|
|
<!-- Works immediately without definitions -->
|
|
<Dashboard>
|
|
<Stats_Panel />
|
|
<Activity_Feed />
|
|
</Dashboard>
|
|
|
|
<!-- Renders as: -->
|
|
<div class="Dashboard">
|
|
<div class="Stats_Panel"></div>
|
|
<div class="Activity_Feed"></div>
|
|
</div>
|
|
|
|
TEMPLATE SYNTAX
|
|
Templates stored as .jqhtml files compile to JavaScript functions:
|
|
|
|
<Define:User_Card>
|
|
<div class="card">
|
|
<img $sid="avatar" src="<%= this.data.avatar %>" />
|
|
<h3 $sid="name"><%= this.data.name %></h3>
|
|
<p $sid="email"><%= this.data.email %></p>
|
|
<button $sid="edit">Edit</button>
|
|
</div>
|
|
</Define:User_Card>
|
|
|
|
Template expressions:
|
|
<%= expression %> - Escaped HTML output (safe, default)
|
|
<%!= expression %> - Unescaped raw output (pre-sanitized content only)
|
|
<% statement; %> - JavaScript statements (loops, conditionals)
|
|
<%-- comment --%> - JQHTML comments (not HTML <!-- --> comments)
|
|
|
|
Attributes:
|
|
$sid="name" - Scoped ID (becomes id="name:component_id")
|
|
$attr=value - Component parameter (becomes this.args.attr)
|
|
Note: Also creates data-attr HTML attribute
|
|
@event=this.method - Event binding (⚠️ verify functionality)
|
|
data-attr="value" - HTML data attributes
|
|
class="my-class" - CSS classes (merged with component name)
|
|
|
|
Conditional Attributes (v2.2.162+):
|
|
Place if statements directly in attribute context to conditionally apply
|
|
attributes based on component arguments:
|
|
|
|
<input type="text"
|
|
<% if (this.args.required) { %>required="required"<% } %>
|
|
<% if (this.args.min !== undefined) { %>min="<%= this.args.min %>"<% } %> />
|
|
|
|
Works with static values, interpolated expressions, and multiple conditionals
|
|
per element. Compiles to Object.assign() with ternary operators at build time.
|
|
No nested conditionals or else clauses supported - use separate if statements.
|
|
|
|
DEFINE TAG CONFIGURATION
|
|
The <Define> tag supports three types of attributes:
|
|
|
|
1. extends="Parent_Component"
|
|
Explicit template inheritance (alternative to class-based)
|
|
|
|
2. $property=value
|
|
Set default this.args values in template
|
|
|
|
3. Regular HTML attributes
|
|
Standard attributes (class, id, tag, data-*, etc.)
|
|
|
|
Setting Default Args with $ Attributes:
|
|
Use $ prefix on <Define> tag to configure default this.args
|
|
values without requiring a JavaScript class:
|
|
|
|
<Define:Data_Grid
|
|
$api=Product_Controller.list_products
|
|
$per_page=25
|
|
$theme="dark"
|
|
>
|
|
<table class="table">
|
|
<% for (let item of this.data.items) { %>
|
|
<tr><td><%= item.name %></td></tr>
|
|
<% } %>
|
|
</table>
|
|
</Define:Data_Grid>
|
|
|
|
Component automatically has:
|
|
this.args.api = Product_Controller.list_products (function)
|
|
this.args.per_page = 25 (number)
|
|
this.args.theme = "dark" (string literal)
|
|
|
|
Quoted vs Unquoted $ Attributes:
|
|
Unquoted - Raw JavaScript expressions (function refs, identifiers):
|
|
$handler=User_Controller.fetch_user (function reference)
|
|
$count=42 (number)
|
|
$enabled=true (boolean)
|
|
|
|
Quoted - String literals:
|
|
$theme="light" (string)
|
|
$user_id="123" (string "123", not number)
|
|
|
|
Template-Only Components:
|
|
Components can be fully configured in templates without
|
|
requiring JavaScript classes. The parent class can access
|
|
this.args values set in child template Define tags.
|
|
|
|
Example - Reusable data grid configured in template:
|
|
|
|
<Define:Users_Grid
|
|
$api=User_Controller.list_users
|
|
$columns="ID,Name,Email"
|
|
>
|
|
<%= content() %>
|
|
</Define:Users_Grid>
|
|
|
|
No JavaScript class needed - template provides all configuration.
|
|
Parent component accesses this.args.api and this.args.columns.
|
|
|
|
Use Cases:
|
|
- Template-only components (no JavaScript class required)
|
|
- Reusable components with configurable controllers
|
|
- Default values overridable at instantiation
|
|
- Declarative component configuration
|
|
|
|
Template compilation:
|
|
- Compiles to JavaScript render functions at build time
|
|
- Not JSX, not runtime parsing
|
|
- Generates working sourcemaps for debugging
|
|
- Similar to PHP templating philosophy
|
|
- All code in <% %> must be SYNCHRONOUS (no async/await)
|
|
|
|
COMPONENT ARGUMENTS
|
|
The $ prefix passes arguments to child components as this.args.
|
|
Critical distinction between quoted and unquoted values:
|
|
|
|
Quoted Values = Literal Strings:
|
|
<UserCard $title="User Profile" />
|
|
<!-- Result: this.args.title = "User Profile" -->
|
|
|
|
<UserCard $expr="this.user" />
|
|
<!-- Result: this.args.expr = "this.user" (string, NOT object!) -->
|
|
|
|
Unquoted Values = JavaScript Expressions:
|
|
<UserCard $user=this.data.user />
|
|
<!-- Result: this.args.user = {actual user object} -->
|
|
|
|
<UserCard $count=42 />
|
|
<!-- Result: this.args.count = 42 (number) -->
|
|
|
|
<UserCard $enabled=true />
|
|
<!-- Result: this.args.enabled = true (boolean) -->
|
|
|
|
Complex Expressions (use parentheses):
|
|
<UserCard $status=(user.active ? 'online' : 'offline') />
|
|
<UserCard $data=({...this.user, modified: true}) />
|
|
|
|
Implementation detail: $attr=value also creates data-attr HTML
|
|
attribute. This is vestigial from v1. For practical purposes,
|
|
focus on: $attr=value sets this.args.attr
|
|
|
|
EVENT BINDING (⚠️ VERIFY FUNCTIONALITY)
|
|
Event binding with @ prefix:
|
|
|
|
<Define:Button>
|
|
<button @click=this.handle_click @mouseover=this.handle_hover>
|
|
<%= content() %>
|
|
</button>
|
|
</Define:Button>
|
|
|
|
Common events:
|
|
@click, @change, @submit, @focus, @blur, @mouseover, @mouseout
|
|
|
|
Syntax: @eventname=this.method_name
|
|
|
|
⚠️ TODO: Verify @event binding syntax is functional in current jqhtml version.
|
|
|
|
THIS.ARGS VS THIS.DATA
|
|
Two distinct data sources with strict lifecycle rules:
|
|
|
|
this.args - Component State (what to load):
|
|
- Source: Passed from parent via $attr=value
|
|
- Purpose: Component state that determines what on_load() fetches
|
|
- Mutability: Modifiable in all methods EXCEPT on_load()
|
|
- Usage: Page numbers, filters, sort order, configuration
|
|
- Examples: this.args.page, this.args.filter, this.args.user_id
|
|
|
|
this.data - Loaded Data (from APIs):
|
|
- Source: Set in on_load() lifecycle method
|
|
- Purpose: Data fetched from APIs based on this.args
|
|
- Mutability: ONLY modifiable in on_create() and on_load()
|
|
- Freeze Cycle: Frozen after on_create(), unfrozen during on_load(),
|
|
frozen again after on_load() completes
|
|
- Initial State: Empty object {} on first render
|
|
- Examples: API responses, fetched records, computed results
|
|
|
|
Lifecycle Restrictions (ENFORCED):
|
|
- on_create(): Can modify this.data (set defaults)
|
|
- on_load(): Can ONLY access this.args and this.data
|
|
Cannot access this.$, this.$sid(), or any other properties
|
|
Can modify this.data freely
|
|
- on_ready() / event handlers: Can modify this.args, read this.data
|
|
CANNOT modify this.data (frozen)
|
|
|
|
State Management Pattern:
|
|
class Product_List extends Jqhtml_Component {
|
|
on_create() {
|
|
// Set default state for on_load()
|
|
this.args.filter = this.args.filter || 'all';
|
|
this.args.page = this.args.page || 1;
|
|
|
|
// Set default data for template
|
|
this.data.products = [];
|
|
this.data.loading = false;
|
|
}
|
|
|
|
async on_load() {
|
|
// Read state from this.args
|
|
const filter = this.args.filter;
|
|
const page = this.args.page;
|
|
|
|
// Fetch and set this.data
|
|
this.data = await Product_Controller.list({
|
|
filter: filter,
|
|
page: page
|
|
});
|
|
}
|
|
|
|
on_ready() {
|
|
// Modify state, then reload
|
|
this.$sid('filter_btn').on('click', () => {
|
|
this.args.filter = 'active'; // Change state
|
|
this.reload(); // Re-fetch with new state
|
|
});
|
|
}
|
|
}
|
|
|
|
Error Messages:
|
|
Violations throw runtime errors with detailed fix suggestions.
|
|
Example: "Cannot modify this.data outside of on_create() or on_load()."
|
|
|
|
COMMON PATTERNS AND PITFALLS
|
|
Correct usage examples:
|
|
<!-- Pass object reference -->
|
|
<UserList $users=this.data.users />
|
|
|
|
<!-- Pass literal string -->
|
|
<Header $title="Dashboard" />
|
|
|
|
<!-- Pass computed value -->
|
|
<Badge $count=this.data.items.length />
|
|
|
|
<!-- Pass callback -->
|
|
<Button $onClick=this.handleClick />
|
|
|
|
Common mistakes to avoid:
|
|
<!-- WRONG: Passes string "this.data.users" not the array -->
|
|
<UserList $users="this.data.users" />
|
|
|
|
<!-- WRONG: Missing quotes on literal string -->
|
|
<Header $title=Dashboard /> <!-- Tries to evaluate variable -->
|
|
|
|
<!-- WRONG: Using this.data for input parameters -->
|
|
class Component {
|
|
on_create() {
|
|
const userId = this.data.userId; // WRONG - use this.args
|
|
const userId = this.args.userId; // CORRECT
|
|
}
|
|
}
|
|
|
|
CONTROL FLOW AND LOOPS
|
|
Templates support full JavaScript control flow using brace syntax:
|
|
|
|
<% if (this.data.show) { %>
|
|
<div>Visible content</div>
|
|
<% } else { %>
|
|
<div>Hidden content</div>
|
|
<% } %>
|
|
|
|
<% for (let i = 0; i < 10; i++) { %>
|
|
<div>Item <%= i %></div>
|
|
<% } %>
|
|
|
|
Common Loop Patterns:
|
|
// Array iteration
|
|
<% for (let user of this.data.users) { %>
|
|
<p><%= user.name %> - <%= user.email %></p>
|
|
<% } %>
|
|
|
|
// With index
|
|
<% for (let [idx, user] of this.data.users.entries()) { %>
|
|
<div>#<%= idx + 1 %>: <%= user.name %></div>
|
|
<% } %>
|
|
|
|
// Object properties
|
|
<% for (let key in this.data.settings) { %>
|
|
<p><%= key %>: <%= this.data.settings[key] %></p>
|
|
<% } %>
|
|
|
|
// Object.entries
|
|
<% for (let [key, val] of Object.entries(this.data.config)) { %>
|
|
<span><%= key %> = <%= val %></span>
|
|
<% } %>
|
|
|
|
JavaScript in Templates:
|
|
Template JavaScript must be SYNCHRONOUS - no async/await:
|
|
- Templates render entire HTML at once
|
|
- All code in <% %> must complete immediately
|
|
- Async operations belong in on_load() lifecycle method
|
|
|
|
<%
|
|
// ALLOWED: Synchronous calculations
|
|
let total = 0;
|
|
for (let item of this.data.items) {
|
|
total += item.price;
|
|
}
|
|
%>
|
|
<p>Total: $<%= total.toFixed(2) %></p>
|
|
|
|
COMMENTS IN TEMPLATES
|
|
JQHTML uses its own comment syntax, NOT HTML comments:
|
|
|
|
Correct - JQHTML comments (parser removes, never in output):
|
|
<%--
|
|
This is a JQHTML comment
|
|
Completely removed during compilation
|
|
Perfect for component documentation
|
|
--%>
|
|
|
|
Incorrect - HTML comments (parser DOES NOT remove):
|
|
<!--
|
|
This is an HTML comment
|
|
Parser treats this as literal HTML
|
|
Will appear in rendered output
|
|
Still processes JQHTML directives inside!
|
|
-->
|
|
|
|
Critical difference:
|
|
HTML comments <!-- --> do NOT block JQHTML directive execution.
|
|
Code inside HTML comments will still execute, just like PHP code
|
|
inside HTML comments in .php files still executes.
|
|
|
|
WRONG - This WILL execute:
|
|
<!-- <% dangerous_code(); %> -->
|
|
|
|
CORRECT - This will NOT execute:
|
|
<%-- <% safe_code(); %> --%>
|
|
|
|
Component docblocks:
|
|
Use JQHTML comments at the top of component templates:
|
|
|
|
<%--
|
|
User_Card_Component
|
|
|
|
Displays user profile information in a card layout.
|
|
|
|
$user_id - ID of user to display
|
|
$show_avatar - Whether to show profile photo (default: true)
|
|
--%>
|
|
<Define:User_Card_Component>
|
|
<!-- Component template here -->
|
|
</Define:User_Card_Component>
|
|
|
|
COMPONENT LIFECYCLE
|
|
Five-stage deterministic lifecycle:
|
|
|
|
on_create → render → on_render → on_load → on_ready
|
|
|
|
1. on_create() (synchronous, runs BEFORE first render)
|
|
- Setup default state BEFORE template executes
|
|
- Initialize this.data properties so template can reference them
|
|
- Must be synchronous (no async/await)
|
|
- Perfect for abstract component base classes
|
|
- Example: this.data.rows = this.data.rows || [];
|
|
|
|
2. render (automatic, top-down)
|
|
- Template executes, DOM created
|
|
- First render: can safely reference properties set in on_create()
|
|
- Parent completes before children
|
|
- Not overridable
|
|
|
|
3. on_render() (top-down)
|
|
- Fires immediately after render, BEFORE children ready
|
|
- Hide uninitialized elements
|
|
- Set initial visual state
|
|
- Prevents flash of uninitialized content
|
|
- Parent completes before children
|
|
|
|
4. on_load() (bottom-up, siblings in parallel, CAN be async)
|
|
- Load async data based on this.args
|
|
- ONLY access this.args and this.data (RESTRICTED)
|
|
- CANNOT access this.$, this.$sid(), or any other properties
|
|
- ONLY modify this.data - NEVER DOM
|
|
- NO child component access
|
|
- Siblings at same depth execute in parallel
|
|
- Children complete before parent
|
|
- If this.data changes, triggers automatic re-render
|
|
- Runtime enforces access restrictions with clear errors
|
|
|
|
5. on_ready() (bottom-up)
|
|
- All children guaranteed ready
|
|
- Safe for DOM manipulation
|
|
- Attach event handlers
|
|
- Initialize plugins
|
|
- Children complete before parent
|
|
|
|
Depth-Ordered Execution:
|
|
- First: on_create runs before anything else (setup state)
|
|
- Top-down: render, on_render (parent before children)
|
|
- Bottom-up: on_load, on_ready (children before parent)
|
|
- Parallel: Siblings at same depth during on_load()
|
|
|
|
Critical rules:
|
|
- Use on_create() to initialize default state before template runs
|
|
- Never modify DOM in on_load() - only update this.data
|
|
- on_load() runs in parallel for siblings (DOM unpredictable)
|
|
- Data changes during load trigger automatic re-render
|
|
- on_create(), on_render(), on_stop() must be synchronous
|
|
- on_load() and on_ready() can be async
|
|
|
|
ON_CREATE() USE CASES
|
|
The on_create() method runs BEFORE the first render, making it perfect
|
|
for initializing default state that templates will reference:
|
|
|
|
Example: Preventing "not iterable" errors
|
|
class DataGrid_Abstract extends Jqhtml_Component {
|
|
on_create() {
|
|
// Initialize defaults BEFORE template renders
|
|
this.data.rows = [];
|
|
this.data.loading = true;
|
|
this.data.is_empty = false;
|
|
}
|
|
|
|
async on_ready() {
|
|
// Later: load actual data
|
|
await this.load_page(1);
|
|
}
|
|
}
|
|
|
|
Template can now safely iterate:
|
|
<% for(let row of this.data.rows) { %>
|
|
<%= content('row', row); %>
|
|
<% } %>
|
|
|
|
Without on_create(), template would fail with "this.data.rows is not
|
|
iterable" because this.data starts as {} before on_load() runs.
|
|
|
|
Abstract Component Pattern:
|
|
Use on_create() in abstract base classes to ensure child templates
|
|
have required properties initialized:
|
|
|
|
class Form_Abstract extends Jqhtml_Component {
|
|
on_create() {
|
|
// Set defaults that all forms need
|
|
this.data.fields = this.data.fields || [];
|
|
this.data.errors = this.data.errors || {};
|
|
this.data.submitting = false;
|
|
}
|
|
}
|
|
|
|
DOUBLE-RENDER PATTERN
|
|
Components may render TWICE if on_load() modifies this.data:
|
|
|
|
1. on_create() sets defaults: this.data.rows = []
|
|
2. First render: template uses empty rows array
|
|
3. on_load() populates this.data.rows with actual data
|
|
4. Automatic re-render with populated data
|
|
5. on_ready() fires after second render (only once)
|
|
|
|
Use for loading states:
|
|
<Define:Product_List>
|
|
<% if (Object.keys(this.data).length === 0) { %>
|
|
<!-- FIRST RENDER: Loading state -->
|
|
<div class="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>
|
|
|
|
Example class:
|
|
class Product_List extends Jqhtml_Component {
|
|
on_render() {
|
|
// Fires TWICE: before and after data load
|
|
const is_loading = Object.keys(this.data).length === 0;
|
|
console.log('Rendered, loading?', is_loading);
|
|
}
|
|
|
|
async on_load() {
|
|
this.data.products = await fetch('/api/products')
|
|
.then(r => r.json());
|
|
// Automatic re-render happens after this completes
|
|
}
|
|
|
|
on_ready() {
|
|
// Fires ONCE after second render
|
|
console.log('Ready with', this.data.products.length, 'products');
|
|
}
|
|
}
|
|
|
|
LOADING STATE PATTERN (CRITICAL)
|
|
NEVER manually call this.render() in on_load() - The framework handles
|
|
re-rendering automatically when this.data changes.
|
|
|
|
The correct pattern for loading states:
|
|
1. Use simple this.data.loaded flag at END of on_load()
|
|
2. Check !this.data.loaded in template for loading state
|
|
3. Trust automatic re-rendering - don't call this.render()
|
|
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()
|
|
|
|
if (response.success) {
|
|
this.data = {
|
|
records: response.records,
|
|
state: {loading: false} // WRONG: 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>
|
|
<% if (!this.data || !this.data.loaded) { %>
|
|
<!-- FIRST RENDER: Loading state -->
|
|
<div class="loading-spinner text-center">
|
|
<div class="spinner-border"></div>
|
|
<p>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">
|
|
<p>No records found</p>
|
|
</div>
|
|
<% } %>
|
|
</Define:Product_List>
|
|
|
|
Why this pattern works:
|
|
1. First render: this.data = {} (empty) - Template shows loading
|
|
2. on_load() executes: Populates this.data and sets loaded = true
|
|
3. Automatic re-render: Framework detects this.data changed
|
|
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 objects
|
|
- Check at template level - !this.data.loaded in template, not JS
|
|
- Never call this.render() manually in on_load() - breaks lifecycle
|
|
|
|
Anti-Patterns to Avoid:
|
|
- NEVER call this.render() manually in on_load()
|
|
- NEVER use nested state objects (this.data.state.loading)
|
|
- NEVER set loading flags at START of on_load()
|
|
- ONLY set this.data.loaded = true at END
|
|
|
|
JAVASCRIPT COMPONENT CLASS
|
|
Optional JavaScript class for behavior:
|
|
|
|
class Product_Grid extends Jqhtml_Component {
|
|
on_render() {
|
|
// Fires immediately after render, before children ready
|
|
// Hide uninitialized grid to prevent visual glitches
|
|
this.$.css('opacity', '0');
|
|
}
|
|
|
|
on_create() {
|
|
// Quick synchronous setup
|
|
// Set instance properties
|
|
this.selected = [];
|
|
}
|
|
|
|
async on_load() {
|
|
// Fetch data - NO DOM manipulation allowed!
|
|
// ONLY update this.data
|
|
this.data.products = await fetch('/api/products')
|
|
.then(r => r.json());
|
|
// Template re-renders automatically with new data
|
|
}
|
|
|
|
on_ready() {
|
|
// All children ready, safe for DOM
|
|
// Attach event handlers
|
|
this.$sid('select_all').on('click', () => this.select_all());
|
|
this.$.animate({opacity: 1}, 300);
|
|
}
|
|
|
|
select_all() {
|
|
// Custom method called from event handler
|
|
this.$.find('.product').addClass('selected');
|
|
}
|
|
}
|
|
|
|
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.
|
|
The DOM is the state. Update it directly.
|
|
|
|
CLASS-TEMPLATE ASSOCIATION
|
|
Automatic by naming convention:
|
|
- Template ID in .jqhtml file: <Define:User_Card>
|
|
- JavaScript class with same name: class User_Card extends Jqhtml_Component
|
|
- Both named "User_Card" = automatically associated
|
|
|
|
The JavaScript class is optional. Components work with template only.
|
|
|
|
NAMING CONVENTIONS
|
|
Component Names (PascalCase):
|
|
- Component names MUST use PascalCase
|
|
- Examples: UserCard, TestComponent1, ProductList
|
|
|
|
Filenames (snake_case - RSpade convention):
|
|
- Filenames SHOULD use lowercase with underscores (snake_case)
|
|
- Framework allows PascalCase or snake_case variations
|
|
- snake_case is the official RSpade convention
|
|
|
|
Examples:
|
|
Component: TestComponent1
|
|
Files: test_component_1.jqhtml, test_component_1.js
|
|
|
|
Component: UserCard
|
|
Files: user_card.jqhtml, user_card.js
|
|
|
|
Component: ProductList
|
|
Files: product_list.jqhtml, product_list.js
|
|
|
|
The framework converts PascalCase to snake_case by:
|
|
- Inserting underscore before uppercase letters (except first)
|
|
- Inserting underscore before first digit in number sequences
|
|
- Converting entire result to lowercase
|
|
|
|
Allowed variations (case-insensitive in rsx/):
|
|
TestComponent1 → test_component_1.jqhtml (recommended)
|
|
TestComponent1 → testcomponent1.jqhtml (allowed)
|
|
TestComponent1 → TestComponent1.jqhtml (allowed, not convention)
|
|
|
|
INSTANTIATION METHODS
|
|
Server-side (Blade uppercase tags):
|
|
<User_Card $name="John" $email="john@example.com" />
|
|
|
|
Server-side (PHP helper):
|
|
{!! Jqhtml::component('User_Card', ['name' => 'John']) !!}
|
|
|
|
Client-side (jQuery):
|
|
$('#target').component('User_Card', {
|
|
data: {name: 'John'},
|
|
template: 'override_template.jqhtml', // Optional
|
|
append: true, // Append instead of replace
|
|
});
|
|
|
|
Replacing Existing Components:
|
|
When called on an element with an existing component, .component()
|
|
destroys the old component and creates a new one. Class preservation:
|
|
- Removed: PascalCase component names (capital start, no __)
|
|
- Preserved: Utility classes (text-muted), BEM child classes (Parent__child)
|
|
- Preserved: All HTML attributes
|
|
|
|
This allows parent components to add BEM-style classes for targeting
|
|
(e.g., Parent__slot) that survive child component replacement.
|
|
|
|
Client-side (HTML attributes):
|
|
<div data-component="User_Card"
|
|
data-component-data='{"name": "John"}'>
|
|
</div>
|
|
|
|
BLADE SYNTAX (UPPERCASE TAGS)
|
|
RSX includes a Blade precompiler for uppercase component tags:
|
|
|
|
Self-closing uppercase tags (recommended):
|
|
<User_Card $name="John" $email="john@example.com" />
|
|
<Counter_Widget $title="My Counter" $initial_value=0 />
|
|
|
|
These transform to Jqhtml::component() calls automatically.
|
|
|
|
Important rules:
|
|
- Component names MUST start with uppercase letter
|
|
- Only self-closing tags allowed in Blade (no innerHTML/slot content)
|
|
- Use $ prefix for component args: $name="value"
|
|
- Use content() method pattern (95% of cases), not slots (5%)
|
|
|
|
Regular PHP syntax also works:
|
|
{!! Jqhtml::component('User_Card', ['name' => 'John']) !!}
|
|
|
|
DOM CLASS CONVENTION
|
|
All instantiated jqhtml components receive CSS classes on their
|
|
DOM elements:
|
|
|
|
- 'Component' - All components
|
|
- Component name (e.g., 'User_Card') - For targeting
|
|
|
|
// Select all jqhtml components on the page
|
|
const components = $('.Component');
|
|
|
|
// Check if an element is a jqhtml component
|
|
if ($element.hasClass('Component')) {
|
|
// This is a jqhtml component
|
|
}
|
|
|
|
// Access component instance
|
|
const component = $element.data('component');
|
|
const component = $('#my-comp').component();
|
|
|
|
CREATING COMPONENTS
|
|
Generate new components with the RSX command:
|
|
|
|
php artisan rsx:app:component:create --name=my_widget --path=rsx/theme/components
|
|
|
|
Creates:
|
|
- my_widget.jqhtml - Template with <%= content() %> pattern
|
|
- My_Widget.js - Class extending Jqhtml_Component
|
|
|
|
Components automatically discovered by manifest system.
|
|
|
|
LIFECYCLE EVENT CALLBACKS
|
|
External code can register callbacks for lifecycle events using
|
|
the .on() method. Useful when you need to know when a component
|
|
reaches a certain state.
|
|
|
|
Supported Events:
|
|
'render' - Fires after render phase completes
|
|
'create' - Fires after create phase completes
|
|
'load' - Fires after load phase completes (data available)
|
|
'ready' - Fires after ready phase completes (fully initialized)
|
|
|
|
Awaiting Data in Async Methods:
|
|
When a method needs data that may not be loaded yet, await the 'load'
|
|
event. If the event already fired, the callback executes immediately:
|
|
|
|
async get_display_name() {
|
|
// Wait for on_load() to complete if not already
|
|
await new Promise(resolve => this.on('load', resolve));
|
|
return `${this.data.first_name} ${this.data.last_name}`;
|
|
}
|
|
|
|
This pattern is useful for breadcrumb methods or other async accessors
|
|
that need loaded data but may be called before on_load() completes.
|
|
|
|
Basic usage:
|
|
// Get component instance and register callback
|
|
const component = $('#my-component').component();
|
|
component.on('ready', (comp) => {
|
|
console.log('Component ready!', comp);
|
|
// Access component data, DOM, etc.
|
|
});
|
|
|
|
// Chain directly
|
|
$('#my-component').component().on('ready', (component) => {
|
|
console.log('User data:', component.data.user);
|
|
});
|
|
|
|
// Multiple callbacks for same event
|
|
component
|
|
.on('render', () => console.log('Dashboard rendered'))
|
|
.on('create', () => console.log('Dashboard created'))
|
|
.on('load', () => console.log('Dashboard data loaded'))
|
|
.on('ready', () => console.log('Dashboard ready'));
|
|
|
|
Key behaviors:
|
|
- Immediate execution: If lifecycle event already occurred,
|
|
callback fires immediately
|
|
- Future events: Callback also registers for future occurrences
|
|
(useful for re-renders)
|
|
- Multiple callbacks: Can register multiple for same event
|
|
- Chaining: Returns this so you can chain .on() calls
|
|
- Custom events also supported (see CUSTOM COMPONENT EVENTS)
|
|
|
|
Example - Wait for component initialization:
|
|
// Initialize nested components after parent ready
|
|
$('#parent').component('Dashboard').on('ready', function() {
|
|
Jqhtml_Integration._on_framework_modules_init($(this));
|
|
});
|
|
|
|
// Process component data after load
|
|
$('#data-grid').component().on('load', (comp) => {
|
|
const total = comp.data.items.reduce((sum, i) => sum + i.value, 0);
|
|
$('#total').text(total);
|
|
});
|
|
|
|
Available in JQHTML v2.2.81+
|
|
|
|
CUSTOM COMPONENT EVENTS
|
|
Components can fire and listen to custom events using the jqhtml event
|
|
bus. Unlike jQuery's .trigger()/.on(), the jqhtml event bus guarantees
|
|
callback execution even if the event fired before the handler was
|
|
registered. This is critical for component lifecycle coordination.
|
|
|
|
Firing Events:
|
|
// Fire event from within a component
|
|
this.trigger('my_event');
|
|
this.trigger('my_event', { key: 'value' });
|
|
|
|
Listening to Events:
|
|
|
|
From parent component (using $sid):
|
|
// In parent's on_ready()
|
|
this.sid('child_component').on('my_event', (component, data) => {
|
|
console.log('Event from:', component);
|
|
console.log('Event data:', data);
|
|
});
|
|
|
|
From external code:
|
|
$('#element').component().on('my_event', (component, data) => {
|
|
// Handle event
|
|
});
|
|
|
|
Callback Signature:
|
|
.on('event_name', (component, data) => { ... })
|
|
|
|
- component: The component instance that fired the event
|
|
- data: Optional data passed as second argument to trigger()
|
|
|
|
Key Difference from jQuery Events:
|
|
jQuery events are lost if fired before handler registration:
|
|
|
|
// jQuery - BROKEN: Event fires before handler exists
|
|
child.$.trigger('initialized'); // Lost!
|
|
parent.$sid('child').on('initialized', handler); // Never called
|
|
|
|
JQHTML events work regardless of timing:
|
|
|
|
// JQHTML - WORKS: Event queued, fires when handler registers
|
|
child.trigger('initialized'); // Queued
|
|
parent.sid('child').on('initialized', handler); // Called immediately!
|
|
|
|
This enables reliable parent-child communication during component
|
|
initialization, when event timing is unpredictable.
|
|
|
|
Example - Child notifies parent of state change:
|
|
|
|
// Child component (Tab_Bar.js)
|
|
set_active_tab(key) {
|
|
this.state.active_tab = key;
|
|
this.render();
|
|
this.trigger('tab:change', { key: key });
|
|
}
|
|
|
|
// Parent component (Settings_Page.js)
|
|
on_ready() {
|
|
this.sid('tabs').on('tab:change', (component, data) => {
|
|
this.show_panel(data.key);
|
|
});
|
|
}
|
|
|
|
IMPORTANT: Do not use jQuery's .trigger() for custom events in jqhtml
|
|
components. The code quality rule JQHTML-EVENT-01 enforces this.
|
|
|
|
$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:
|
|
The $redrawable attribute triggers a parser transformation that
|
|
converts the element into a <Redrawable> component:
|
|
|
|
<!-- Write this: -->
|
|
<div $redrawable $sid="counter">
|
|
Count: <%= this.data.count %>
|
|
</div>
|
|
|
|
<!-- Parser transforms to: -->
|
|
<Redrawable data-tag="div" $sid="counter">
|
|
Count: <%= this.data.count %>
|
|
</Redrawable>
|
|
|
|
The Redrawable component renders as the specified tag type (default
|
|
is div) while providing full component functionality including
|
|
lifecycle hooks, data management, and selective re-rendering.
|
|
|
|
Selective Re-rendering from Parent:
|
|
Use the render(id) delegation syntax to re-render only specific
|
|
child components:
|
|
|
|
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 $sid="counter"
|
|
}
|
|
}
|
|
|
|
render(id) Delegation Syntax:
|
|
- this.render('counter') finds child with $sid="counter"
|
|
- Verifies element is a component (has $redrawable or is proper
|
|
component class)
|
|
- Calls its render() method to update only that element
|
|
- Perfect for live data displays, counters, status indicators,
|
|
real-time feeds
|
|
- Parent component's DOM remains unchanged
|
|
|
|
Error Handling:
|
|
- Clear error if $sid doesn't exist in children
|
|
- Clear error if element isn't configured as component
|
|
- Guides developers to correct usage patterns
|
|
|
|
Use Cases:
|
|
- Live counters and metrics that update frequently
|
|
- Status indicators that change independently
|
|
- Real-time data feeds (notifications, chat messages)
|
|
- Dynamic lists that need incremental updates
|
|
- Any element needing selective updates without full page re-render
|
|
- Avoiding unnecessary re-renders of static sibling elements
|
|
|
|
Benefits:
|
|
- No need to create separate component classes
|
|
- Lightweight - just add $redrawable attribute
|
|
- Full component lifecycle (on_load, on_ready, etc.)
|
|
- Efficient - only updates what changed
|
|
- Clear errors guide correct usage
|
|
|
|
LIFECYCLE MANIPULATION METHODS
|
|
Components provide methods to control their lifecycle and state after
|
|
initial render.
|
|
|
|
Method Summary (jqhtml v2.2.182+):
|
|
|
|
reload() - Restore this.data to defaults, call on_load(), then render()
|
|
render() - Re-renders with full lifecycle (waits for children, calls on_ready)
|
|
redraw() - Alias for render()
|
|
ready() - Returns promise that resolves when full lifecycle complete
|
|
rendered() - Returns promise that resolves when render complete (not children)
|
|
|
|
reload()
|
|
Re-fetch data and re-render - Restores this.data to on_create() state,
|
|
calls on_load() to fetch fresh data, then renders.
|
|
|
|
Usage:
|
|
class User_Card extends Jqhtml_Component {
|
|
async refresh_user_data() {
|
|
await this.reload(); // Fetches fresh data
|
|
console.log('User data updated');
|
|
}
|
|
|
|
on_ready() {
|
|
this.$sid('filter_btn').on('click', async () => {
|
|
this.args.filter = 'active'; // Update state
|
|
await this.reload(); // Re-fetch with new state
|
|
});
|
|
}
|
|
}
|
|
|
|
Behavior:
|
|
- Restores this.data to snapshot from on_create()
|
|
- Calls on_load() to fetch fresh data based on current this.args
|
|
- Always re-renders (calls render() with full lifecycle)
|
|
- Waits for children and calls on_ready()
|
|
- Returns promise - await for completion
|
|
- Use when: this.args changed and need to re-fetch data
|
|
|
|
Lifecycle: restore this.data → on_load() → render() → on_ready() → trigger('ready')
|
|
|
|
render()
|
|
Re-render component - Re-executes template with full lifecycle,
|
|
waits for children, calls on_ready().
|
|
|
|
Usage:
|
|
class Counter extends Jqhtml_Component {
|
|
increment() {
|
|
this.data.count++;
|
|
this.render(); // Update UI
|
|
}
|
|
}
|
|
|
|
Behavior:
|
|
- Re-executes template with current this.data
|
|
- Waits for all child components to be ready
|
|
- Calls on_ready() after children ready
|
|
- Triggers 'ready' event when complete
|
|
- Returns promise - await for completion
|
|
- Use when: Data changed and UI needs updating
|
|
|
|
Lifecycle: _render() → wait for children → on_ready() → trigger('ready')
|
|
|
|
Note: redraw() is an alias for render()
|
|
|
|
NOTE: reinitialize() method removed in v2.2.201.
|
|
Use reload() for re-fetching data (99% of cases).
|
|
For complete reset, remove component and create new instance.
|
|
|
|
stop()
|
|
Component stopping - Removes component and all children from DOM.
|
|
|
|
Usage:
|
|
class Modal extends Jqhtml_Component {
|
|
close() {
|
|
this.stop(); // Remove modal from DOM
|
|
}
|
|
}
|
|
|
|
Behavior:
|
|
- Calls on_stop() hook if defined
|
|
- Recursively stops all child components
|
|
- Removes DOM element completely
|
|
- Adds _Component_Stopped class before removal
|
|
- Synchronous - completes immediately
|
|
- Use when: Component no longer needed
|
|
|
|
on_stop() hook example:
|
|
class Chat_Widget extends Jqhtml_Component {
|
|
on_stop() {
|
|
// Clean up websocket connection
|
|
this.socket.disconnect();
|
|
console.log('Chat widget stopped');
|
|
}
|
|
}
|
|
|
|
ready()
|
|
Wait for component to complete full lifecycle including all children.
|
|
|
|
Usage:
|
|
const component = $(element).component();
|
|
await component.ready();
|
|
// Component and all children are now fully loaded
|
|
|
|
Behavior:
|
|
- Returns promise that resolves when full lifecycle is complete
|
|
- Waits for on_load() of this component AND all descendants
|
|
- Waits for on_ready() of this component AND all descendants
|
|
- Use when: You need component to be fully interactive
|
|
|
|
rendered()
|
|
Wait for component's render to complete (does not wait for children's data).
|
|
|
|
Usage:
|
|
const component = $(element).component();
|
|
await component.rendered();
|
|
// DOM is rendered, but children may still be loading data
|
|
|
|
Behavior:
|
|
- Returns promise that resolves after render phase completes
|
|
- Does NOT wait for child components' on_load() or on_ready()
|
|
- Use when: You need DOM structure but don't need child data
|
|
- Faster than ready() when child data loading is unnecessary
|
|
|
|
Example - SPA layout navigation:
|
|
// Update nav active state without waiting for action to load data
|
|
await layout.rendered();
|
|
layout.$sid('nav').find('.active').removeClass('active');
|
|
layout.$sid('nav').find(`[href="${url}"]`).addClass('active');
|
|
|
|
SYNCHRONOUS REQUIREMENTS
|
|
CRITICAL: These lifecycle methods MUST be synchronous (no async, no await):
|
|
|
|
Method Synchronous Required Can Use await
|
|
------------ -------------------- -------------
|
|
on_create() YES NO
|
|
on_render() YES NO
|
|
on_stop() YES NO
|
|
on_load() NO (async allowed) YES
|
|
on_ready() NO (async allowed) YES
|
|
|
|
Framework needs predictable execution order for lifecycle coordination.
|
|
|
|
Example - Correct synchronous hooks:
|
|
class MyComponent extends Jqhtml_Component {
|
|
on_create() {
|
|
this.counter = 0; // Sync setup
|
|
}
|
|
|
|
on_render() {
|
|
this.counter++; // Sync update
|
|
}
|
|
|
|
on_stop() {
|
|
console.log('Stopped'); // Sync cleanup
|
|
}
|
|
}
|
|
|
|
Example - WRONG (async hooks not allowed):
|
|
class MyComponent extends Jqhtml_Component {
|
|
async on_create() { // DON'T DO THIS
|
|
await this.setup();
|
|
}
|
|
}
|
|
|
|
ACCESSING COMPONENT INSTANCE
|
|
From element:
|
|
const component = $('#my-component').component();
|
|
component.data.name = 'New Name';
|
|
|
|
From class:
|
|
// All instances tracked in Jqhtml_Component.instances
|
|
User_Card.instances.forEach(instance => {
|
|
instance.data.status = 'active';
|
|
});
|
|
|
|
DOM UTILITIES
|
|
Component instance methods and properties:
|
|
|
|
this.$
|
|
jQuery wrapped component root element
|
|
This is genuine jQuery - all methods work directly
|
|
|
|
this.$sid(name)
|
|
Get scoped element as jQuery object
|
|
Example: this.$sid('edit') gets element with $sid="edit"
|
|
Returns jQuery object, NOT component instance
|
|
|
|
this.sid(name)
|
|
Get scoped child component instance directly
|
|
Example: this.sid('my_component') gets component instance
|
|
Returns component instance, NOT jQuery object
|
|
|
|
CRITICAL: this.$sid() vs this.sid() distinction
|
|
- this.$sid('foo') → jQuery object (for DOM manipulation)
|
|
- this.sid('foo') → Component instance (for calling methods)
|
|
|
|
Common mistake:
|
|
const comp = this.sid('foo').component(); // ❌ WRONG
|
|
const comp = this.sid('foo'); // ✅ CORRECT
|
|
|
|
Getting component from jQuery:
|
|
const $elem = this.$sid('foo');
|
|
const comp = $elem.component(); // ✅ CORRECT (jQuery → component)
|
|
|
|
this.data
|
|
Component data object
|
|
Starts as empty object {}
|
|
Changes trigger re-render
|
|
Load data in on_load() method
|
|
|
|
this.args
|
|
Component input parameters from attributes
|
|
Set during construction from $attr=value
|
|
Treat as read-only
|
|
|
|
this._cid
|
|
Unique component instance ID for scoping
|
|
Used internally for ID generation
|
|
|
|
this.$child(name)
|
|
Get child component by name
|
|
|
|
this.$parent()
|
|
Get parent component instance
|
|
|
|
NESTING COMPONENTS
|
|
Components can contain other components:
|
|
|
|
<Define:Dashboard>
|
|
<div class="dashboard">
|
|
<% for (let widget of this.data.widgets) { %>
|
|
<Widget_Card $widget_id=widget.id $data=widget />
|
|
<% } %>
|
|
</div>
|
|
</Define:Dashboard>
|
|
|
|
Nested components initialize after parent (bottom-up for on_create,
|
|
on_load, on_ready).
|
|
|
|
SCOPED IDS
|
|
Use $sid attribute for component-scoped element IDs:
|
|
|
|
Template:
|
|
<Define:User_Card>
|
|
<h3 $sid="title">Name</h3>
|
|
<p $sid="email">Email</p>
|
|
<button $sid="edit_btn">Edit</button>
|
|
</Define:User_Card>
|
|
|
|
Rendered HTML (automatic scoping):
|
|
<div class="User_Card Jqhtml_Component">
|
|
<h3 id="title:c123">Name</h3>
|
|
<p id="email:c123">Email</p>
|
|
<button id="edit_btn:c123">Edit</button>
|
|
</div>
|
|
|
|
Access with this.$sid():
|
|
class User_Card extends Jqhtml_Component {
|
|
on_ready() {
|
|
// Use logical name
|
|
this.$sid('title').text('John Doe');
|
|
this.$sid('email').text('john@example.com');
|
|
this.$sid('edit_btn').on('click', () => this.edit());
|
|
}
|
|
}
|
|
|
|
Why scoped IDs: Multiple component instances need unique IDs
|
|
without conflicts. Framework automatically appends :component_id
|
|
to prevent collisions.
|
|
|
|
EXAMPLES
|
|
Card with async data loading:
|
|
class Product_Card extends Jqhtml_Component {
|
|
async on_load() {
|
|
// Load product data
|
|
const id = this.args.product_id;
|
|
this.data = await fetch(`/api/products/${id}`)
|
|
.then(r => r.json());
|
|
// Template re-renders automatically with data
|
|
}
|
|
|
|
on_ready() {
|
|
// Attach event handlers after data loaded
|
|
this.$sid('buy').on('click', async () => {
|
|
await Cart.add(this.data.id);
|
|
this.$sid('buy').text('Added!').prop('disabled', true);
|
|
});
|
|
|
|
this.$sid('favorite').on('click', () => {
|
|
this.$.toggleClass('favorited');
|
|
});
|
|
}
|
|
}
|
|
|
|
List with loading state:
|
|
<!-- todo_list.jqhtml -->
|
|
<Define:Todo_List>
|
|
<div>
|
|
<% if (Object.keys(this.data).length === 0) { %>
|
|
<div class="loading">Loading todos...</div>
|
|
<% } else { %>
|
|
<% for (let todo of this.data.todos) { %>
|
|
<div class="todo">
|
|
<input type="checkbox"
|
|
<%= todo.done ? 'checked' : '' %>>
|
|
<span><%= todo.text %></span>
|
|
</div>
|
|
<% } %>
|
|
<% } %>
|
|
</div>
|
|
</Define:Todo_List>
|
|
|
|
class Todo_List extends Jqhtml_Component {
|
|
async on_load() {
|
|
this.data.todos = await fetch('/api/todos')
|
|
.then(r => r.json());
|
|
}
|
|
|
|
on_ready() {
|
|
this.$.on('change', 'input', (e) => {
|
|
const index = $(e.target).closest('.todo').index();
|
|
this.toggle_todo(index);
|
|
});
|
|
}
|
|
|
|
toggle_todo(index) {
|
|
this.data.todos[index].done = !this.data.todos[index].done;
|
|
// Update server...
|
|
}
|
|
}
|
|
|
|
Form with validation:
|
|
class Contact_Form extends Jqhtml_Component {
|
|
on_ready() {
|
|
this.$.on('submit', (e) => {
|
|
e.preventDefault();
|
|
if (this.validate()) {
|
|
this.submit();
|
|
}
|
|
});
|
|
}
|
|
|
|
validate() {
|
|
const email = this.$sid('email').val();
|
|
if (!email.includes('@')) {
|
|
this.$sid('error').text('Invalid email');
|
|
return false;
|
|
}
|
|
this.$sid('error').text('');
|
|
return true;
|
|
}
|
|
|
|
async submit() {
|
|
const data = {
|
|
email: this.$sid('email').val(),
|
|
message: this.$sid('message').val(),
|
|
};
|
|
|
|
await fetch('/contact', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
this.$.html('<p>Thank you!</p>');
|
|
}
|
|
}
|
|
|
|
CONTENT AND SLOTS
|
|
JQHTML provides two patterns for passing content from parent to child:
|
|
|
|
1. content() Function (Primary Pattern - 95% of Use Cases)
|
|
The content() function is the standard way to accept innerHTML:
|
|
|
|
<!-- Define component -->
|
|
<Define:Card>
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<%= content() %> <!-- Outputs passed innerHTML -->
|
|
</div>
|
|
</div>
|
|
</Define:Card>
|
|
|
|
<!-- Use component -->
|
|
<Card>
|
|
<h3>Card Title</h3>
|
|
<p>This content goes where content() is called</p>
|
|
</Card>
|
|
|
|
Works exactly like regular HTML - intuitive and simple.
|
|
Perfect for: wrappers, containers, cards, modals.
|
|
|
|
2. Named Slots (Advanced Pattern - 5% of Use Cases)
|
|
Named slots for components needing multiple content areas.
|
|
|
|
Child templates use content('slotname') to render named slots:
|
|
|
|
<!-- Define with 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 templates provide content using <Slot:slotname> tags:
|
|
|
|
<!-- Use with named slots -->
|
|
<Card_Layout>
|
|
<Slot:header><h3>User Profile</h3></Slot:header>
|
|
<Slot:body>
|
|
<p>Name: <%= this.data.name %></p>
|
|
<p>Email: <%= this.data.email %></p>
|
|
</Slot:body>
|
|
<Slot:footer>
|
|
<button class="btn">Save</button>
|
|
</Slot:footer>
|
|
</Card_Layout>
|
|
|
|
Critical rules:
|
|
- Cannot mix regular content with named slots
|
|
- If ANY named slots present, ALL content must be in slots
|
|
- Child template syntax: <%= content('slotname') %>
|
|
- Parent template syntax: <Slot:slotname>content</Slot:slotname>
|
|
|
|
Decision Guide:
|
|
Use content() when:
|
|
- Single content area (most components)
|
|
- Building wrappers or containers
|
|
- Enhancing child content
|
|
|
|
Use slots when:
|
|
- Multiple distinct content regions
|
|
- Building complex layouts (DataGrid, Wizard)
|
|
- Different templates for different states
|
|
|
|
3. Slot-Based Template Inheritance (v2.2.108+)
|
|
When a component template contains ONLY slots (no HTML wrapper),
|
|
it automatically inherits the parent class template structure.
|
|
|
|
This enables abstract base components with customizable content,
|
|
allowing child classes to extend parent templates without
|
|
duplicating HTML structure.
|
|
|
|
Parent template (DataGrid_Abstract.jqhtml):
|
|
<Define:DataGrid_Abstract>
|
|
<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 JavaScript 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>
|
|
<Slot:header>
|
|
<th>ID</th>
|
|
<th>Name</th>
|
|
<th>Email</th>
|
|
</Slot:header>
|
|
|
|
<Slot:row>
|
|
<td><%= row.id %></td>
|
|
<td><%= row.name %></td>
|
|
<td><%= row.email %></td>
|
|
</Slot:row>
|
|
</Define:Users_DataGrid>
|
|
|
|
Result: Users_DataGrid renders using DataGrid_Abstract HTML
|
|
structure with customized slot content.
|
|
|
|
Data Passing to Slots:
|
|
Parents pass data to slots via second parameter:
|
|
<%= content('slotname', data) %>
|
|
|
|
Child templates receive data via slot parameter:
|
|
<Slot:row>
|
|
<td><%= row.id %></td>
|
|
</Slot:row>
|
|
|
|
The slot parameter name matches the slot name automatically.
|
|
|
|
Reserved Word Validation:
|
|
Slot names cannot be JavaScript reserved words.
|
|
Parser rejects with fatal error:
|
|
|
|
<Slot:function>Content</Slot:function> <!-- ERROR: reserved word -->
|
|
<Slot:if>Content</Slot:if> <!-- ERROR: reserved word -->
|
|
<Slot:header>Content</Slot:header> <!-- Valid -->
|
|
|
|
Reserved words include: function, if, for, class, const, let,
|
|
var, while, switch, return, try, catch, and others.
|
|
|
|
Requirements:
|
|
- Child template contains ONLY slot definitions (no HTML)
|
|
- JavaScript class extends parent: class Child extends Parent
|
|
- Parent template defines slots using content('slotname')
|
|
- Runtime walks prototype chain via Object.getPrototypeOf()
|
|
|
|
Implementation:
|
|
- Parser detects slot-only templates
|
|
- Generates {_slots: {...}} format instead of HTML instructions
|
|
- Runtime finds parent template by walking prototype chain
|
|
- Parent template structure inherited automatically
|
|
- All compilation happens at build time
|
|
|
|
Use Cases:
|
|
- Abstract data tables with customizable column layouts
|
|
- Page layouts with variable content sections
|
|
- Card variations with different headers/bodies/footers
|
|
- Form patterns with customizable field sets
|
|
- Any component hierarchy with shared structure
|
|
|
|
TEMPLATE-ONLY COMPONENTS
|
|
Components can exist as .jqhtml files without a companion .js file.
|
|
This is fine for simple display-only components that just render
|
|
input data with conditionals - no lifecycle hooks or event handlers
|
|
needed beyond what's possible inline.
|
|
|
|
When to use template-only:
|
|
- Component just displays data passed via arguments
|
|
- Only needs simple conditionals in the template
|
|
- No complex event handling beyond simple button clicks
|
|
- Mentally easier than creating a separate .js file
|
|
|
|
Inline Event Handlers:
|
|
Define handlers in template code, reference with @event syntax:
|
|
|
|
<Define:Retry_Button>
|
|
<% this.handle_click = () => window.location.reload(); %>
|
|
<button class="btn btn-primary" @click=this.handle_click>
|
|
Retry
|
|
</button>
|
|
</Define:Retry_Button>
|
|
|
|
Note: @event values must be UNQUOTED (not @click="this.method").
|
|
|
|
Inline Argument Validation:
|
|
Throw errors early if required arguments are missing:
|
|
|
|
<Define:User_Badge>
|
|
<%
|
|
if (!this.args.user_id) {
|
|
throw new Error('User_Badge: $user_id is required');
|
|
}
|
|
if (!this.args.name) {
|
|
throw new Error('User_Badge: $name is required');
|
|
}
|
|
%>
|
|
<span class="badge"><%= this.args.name %></span>
|
|
</Define:User_Badge>
|
|
|
|
Complete Example (error page component):
|
|
<%--
|
|
Not_Found_Error_Page_Component
|
|
Displays when a record cannot be found.
|
|
--%>
|
|
<Define:Not_Found_Error_Page_Component class="text-center py-5">
|
|
<div class="mb-4">
|
|
<i class="bi bi-exclamation-triangle-fill text-warning"
|
|
style="font-size: 4rem;"></i>
|
|
</div>
|
|
<h3 class="mb-3"><%= this.args.record_type %> Not Found</h3>
|
|
<p class="text-muted mb-4">
|
|
The <%= this.args.record_type.toLowerCase() %> you are
|
|
looking for does not exist or has been deleted.
|
|
</p>
|
|
<a href="<%= this.args.back_url %>" class="btn btn-primary">
|
|
<%= this.args.back_label %>
|
|
</a>
|
|
</Define:Not_Found_Error_Page_Component>
|
|
|
|
This pattern is not necessarily "best practice" for complex components,
|
|
but it works well and is pragmatic for simple display components. If
|
|
the component needs lifecycle hooks, state management, or complex
|
|
event handling, create a companion .js file instead.
|
|
|
|
INTEGRATION WITH RSX
|
|
JQHTML automatically integrates with the RSX framework:
|
|
- Templates discovered by manifest system during build
|
|
- Processor converts .jqhtml to JavaScript render functions
|
|
- Bundle system includes generated code automatically
|
|
- No manual registration or configuration required
|
|
- Templates compile at build time with working sourcemaps
|
|
|
|
MANIFEST INTEGRATION
|
|
The manifest system handles automatic discovery:
|
|
- Caches component names during manifest build
|
|
- Enables Blade precompiler validation
|
|
- Supports component auto-discovery
|
|
- Generates JavaScript class stubs for components
|
|
- Tracks all .jqhtml files in included directories
|
|
|
|
Components automatically found in directories scanned by
|
|
manifest system (typically /rsx/ and included paths).
|
|
|
|
Build process flow:
|
|
1. Manifest build scans included directories
|
|
2. Finds all .jqhtml template files
|
|
3. Finds matching .js class files (optional)
|
|
4. Registers component names for path-agnostic access
|
|
5. Templates compile to JavaScript during bundle compilation
|
|
|
|
BUNDLE INCLUSION
|
|
Components included via bundle system:
|
|
|
|
class Frontend_Bundle extends Rsx_Bundle_Abstract
|
|
{
|
|
public static function define(): array
|
|
{
|
|
return [
|
|
'include' => [
|
|
'jqhtml', // Framework library
|
|
'rsx/theme/components', // All .jqhtml files
|
|
'rsx/app/frontend', // Module directory
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
All .jqhtml and .js files in included directories are
|
|
automatically discovered, compiled, and bundled.
|
|
|
|
Generated JavaScript added to bundle automatically.
|
|
|
|
SERVER-SIDE RENDERING
|
|
Jqhtml::component() renders initial HTML server-side.
|
|
JavaScript hydrates on client for interactivity.
|
|
|
|
Benefits:
|
|
- SEO friendly
|
|
- Fast initial render
|
|
- Progressive enhancement
|
|
- Works without JavaScript (degraded)
|
|
|
|
DEBUGGING
|
|
Enable debug mode:
|
|
window.Jqhtml_Debug = true;
|
|
|
|
Logs:
|
|
- Component initialization
|
|
- Template compilation
|
|
- Data binding
|
|
- Lifecycle events
|
|
|
|
PERFORMANCE
|
|
- Templates cached after first compilation
|
|
- Use data-component for server-side initial render
|
|
- Minimize DOM manipulation in lifecycle methods
|
|
- Leverage automatic re-render via this.data changes
|
|
- Sibling components load in parallel during on_load()
|
|
|
|
SEE ALSO
|
|
bundle_api(3), controller(3), manifest_api(3)
|
|
|
|
RSX Framework 2025-10-07 JQHTML(3)
|