Files
rspade_system/app/RSpade/man/jqhtml.txt
2025-12-26 02:17:31 +00:00

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)