Fix bin/publish: use correct .env path for rspade_system Fix bin/publish script: prevent grep exit code 1 from terminating script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
16 KiB
Executable File
@jqhtml/core
Core runtime library for JQHTML v2 - jQuery-first component framework.
Installation
npm install @jqhtml/core jquery
Module Structure
The core package exports two separate entry points:
Main Runtime (Production)
import { Component, jqhtml } from '@jqhtml/core';
Debug Utilities (Development Only)
import { showDebugOverlay, hideDebugOverlay } from '@jqhtml/core/debug';
The debug module is kept separate to avoid including debug code in production bundles. It provides:
- Visual debug overlay with performance profiling
- Component flash visualization
- Lifecycle logging
- Slow render detection
Overview
The core package provides:
- Component base class with 5-stage lifecycle
- Lifecycle manager for phase coordination
- Component registry for dynamic instantiation
- Instruction processor for template rendering
- jQuery plugin integration
- Template rendering with data bindings
Quick Start
Basic Component
import { Component } from '@jqhtml/core';
class UserCard extends Component {
async init() {
// Quick setup, hide elements
this.$.addClass('loading');
}
async load() {
// Fetch data (runs in parallel with siblings)
this.data.user = await fetch('/api/user').then(r => r.json());
}
async render() {
// Create DOM structure
this.$.html(`
<div class="card">
<h2>${this.data.user.name}</h2>
<p>${this.data.user.email}</p>
</div>
`);
}
async ready() {
// Component fully initialized
this.$.removeClass('loading');
}
}
Using jQuery Plugin
// Create component on existing element
$('#user-container').component(UserCard, { userId: 123 });
// Get component instance
const card = $('#user-container').component();
Component Lifecycle
Components follow a multi-stage lifecycle with automatic re-rendering:
- constructor - Instance creation, jQuery element setup (
this.datais{}) - render - Create initial DOM structure (top-down, atomic/synchronous operation)
- create - Quick setup after DOM creation (bottom-up, siblings parallel)
- load - Fetch data (bottom-up, fully parallel, ABSOLUTELY NO DOM MODIFICATIONS)
- render (automatic) - Re-render if data changed during load (empties DOM first)
- create (automatic) - Re-setup if component was re-rendered
- ready - Fully initialized (bottom-up, siblings parallel)
- destroy - Cleanup when removed
Critical: NO DOM Modifications in load()
The requirement that on_load() must not modify the DOM is ABSOLUTE. This is not a guideline but a strict architectural requirement. Violating this will cause rendering issues and race conditions.
Data Initialization Timeline:
- Before
load():this.data = {}(empty object) - After
load():this.datacontains fetched data (remains{}if no data loaded)
Correct Patterns for Loading States
If you need to show different DOM states before and after loading, use these patterns:
Pattern 1: Conditional Rendering in Template
<Define:UserProfile>
<div class="user-profile">
<% if (Object.keys(this.data).length === 0): %>
<!-- Show loading state (no data loaded yet) -->
<div class="loading">
<div class="spinner"></div>
<p>Loading user profile...</p>
</div>
<% else: %>
<!-- Show loaded content -->
<h2><%= this.data.user?.name || 'Unknown User' %></h2>
<p><%= this.data.user?.bio || 'No bio available' %></p>
<% endif; %>
</div>
</Define:UserProfile>
Pattern 2: DOM Changes in create() and ready()
class DataComponent extends Component {
async on_create() {
// Set loading state in the DOM during create phase
this.$id('status').addClass('loading').text('Loading...');
}
async on_load() {
// ONLY fetch data - NO DOM modifications here!
this.data.user = await fetch('/api/user').then(r => r.json());
}
async on_ready() {
// Update DOM after data is loaded
this.$id('status').removeClass('loading').text('Loaded');
this.$id('username').text(this.data.user.name);
}
}
NEVER do this:
class BadComponent extends Component {
async on_load() {
// ❌ WRONG - DOM modification in load()
this.$id('status').text('Loading...'); // VIOLATION!
this.data.user = await fetch('/api/user').then(r => r.json());
// ❌ WRONG - More DOM modification
this.$id('status').text('Loaded'); // VIOLATION!
}
}
Important: The render() method is atomic and essentially synchronous - it empties the DOM and rebuilds it in a single operation. While marked async for technical reasons, it completes immediately without yielding control.
Phase Batching
All components complete each phase before moving to the next:
- Parents render before children
- Children create/load/ready before parents
- Siblings execute in parallel where safe
Automatic Re-rendering
After the load phase, if this.data has changed, the component automatically:
- Calls
render()again (which empties the DOM with$.empty()first) - Calls
create()again to re-setup the new DOM - Then proceeds to
ready()
This ensures components can fetch data and re-render with that data seamlessly.
Controlling Re-rendering
By default, components automatically re-render if this.data changes during load():
class DataComponent extends Component {
async on_load() {
// If this modifies this.data, component will re-render
this.data.user = await fetch('/api/user').then(r => r.json());
}
// Override to customize re-render logic
should_rerender() {
// Default: returns true if JSON.stringify(this.data) changed
// Override for custom logic:
return this.data.user && !this.data.cached;
}
}
Manual Re-rendering
You can manually re-render a component at any time:
class InteractiveComponent extends Component {
async handleUpdate(newData) {
// Update data
this.data = { ...this.data, ...newData };
// Manually re-render (atomic operation)
await this.render();
// Optionally re-initialize (call create/ready as needed)
await this.on_create(); // Re-setup event handlers
await this.on_ready(); // Re-initialize component state
}
// Lifecycle manipulation methods
redraw() {
// Synchronously re-renders with current data
}
async reload_data() {
// Re-fetches data via on_load(), then redraws
}
async reinitialize() {
// Full lifecycle reset from stage 0
}
destroy() {
// Cleanup component and children
}
}
Note: The re-render process:
- Empties the component's DOM with
this.$.empty() - Calls
render()to rebuild the DOM atomically - For automatic re-renders, calls
create()again for the new DOM - Finally calls
ready() - For manual re-renders, you control which lifecycle methods to call
Scoped IDs
Components use _cid for scoped element selection:
class TabsComponent extends Component {
async render() {
this.$.html(`
<button id="tab1:${this._cid}">Tab 1</button>
<button id="tab2:${this._cid}">Tab 2</button>
<div id="content:${this._cid}"></div>
`);
}
selectTab(tabId) {
// Use $id() for scoped selection
this.$id('tab1').removeClass('active');
this.$id('tab2').removeClass('active');
this.$id(tabId).addClass('active');
}
}
Template Integration
The core runtime processes templates compiled by @jqhtml/parser:
import { render_template, with_template } from '@jqhtml/core';
// Template function (usually generated by parser)
const template = function(data, args, content) {
const _output = [];
_output.push({tag: ["h1", {"data-bind-text": "title"}, false]});
_output.push({tag: ["h1", {}, true]});
return [_output, this];
};
// Component with template
class Article extends Component {
async on_render() {
await render_template(this, template);
}
}
// Or use mixin
const Article = with_template(template)(Component);
Attribute Rules
JQHTML has specific rules for attribute quoting and value passing:
- @ Event attributes: MUST be unquoted (pass function references)
Example:
@click=this.handleClick - $ Data attributes: Can be quoted OR unquoted (flexible)
Example:
$id="my-id"or$data=this.complexObject - Regular HTML attributes: MUST be quoted (strings only)
Example:
class="container <%= this.args.theme %>"
See ATTRIBUTE_RULES.md for comprehensive documentation.
Data Bindings
Templates support reactive data bindings:
:text="expression"- Update text content:value="expression"- Form input values:class="{active: isActive}"- Dynamic classes:style="{color: textColor}"- Dynamic styles@click=handler- Event handlers (unquoted)
Browser Bundle
For browser usage without a build tool:
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="jqhtml-bundle.js"></script>
<script>
const { Component } = JQHTML;
class MyComponent extends Component {
// ...
}
</script>
Lifecycle Event Callbacks
JQHTML provides a .on() method for registering callbacks that fire after lifecycle events complete. This is useful for external code that needs to know when a component reaches a certain state.
Supported Events
'render'- Fires after the render phase completes'create'- Fires after the create phase completes'load'- Fires after the load phase completes'ready'- Fires after the ready phase completes (component fully initialized)
Usage
// Get component instance and register callback
const component = $('#my-component').component();
component.on('ready', (comp) => {
console.log('Component is ready!', comp);
// Access component data, DOM, etc.
});
// If the event already occurred, callback fires immediately
// AND still registers for future occurrences (e.g., on re-render)
Behavior
- Immediate Execution: If the lifecycle event has already occurred when you register the callback, it fires immediately
- Future Events: The callback also registers for future occurrences of the same event (useful for re-renders)
- Multiple Callbacks: You can register multiple callbacks for the same event
- Error Safety: Errors in callbacks are caught and logged without breaking the component
- Validation: Only lifecycle events are allowed; other event names trigger a console error
Example: Wait for Component to be Ready
// Register callback before or after component initialization
$('#user-profile').component().on('ready', (component) => {
// Component is fully initialized, data is loaded
console.log('User data:', component.data.user);
// Safe to access all DOM elements
component.$id('email').addClass('verified');
});
Example: Track Multiple Lifecycle Events
const component = $('#dashboard').component();
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'));
Example: Invalid Event (Error)
// This will log an error to console
$('#my-component').component().on('click', callback);
// Error: Component.on() only supports lifecycle events: render, create, load, ready
Note on Re-renders
When a component re-renders (manually or automatically after load()), lifecycle events fire again:
const component = $('#widget').component();
component.on('render', () => {
console.log('Widget rendered');
// Fires on initial render AND every re-render
});
// Later, trigger re-render
await component.render(); // "Widget rendered" logs again
API Reference
Component Class
constructor(element, args)- Create component instanceasync render()- Create DOM structureasync init()- Quick setup phaseasync load()- Data fetching phaseasync ready()- Final initializationdestroy()- Cleanupshould_rerender()- Control re-rendering after loademit(event, data)- Emit jQuery eventson(event, callback)- Register lifecycle event callback$id(localId)- Get scoped jQuery elementid(localId)- Get scoped component instanceparent()- Get parent componentchildren()- Get direct child componentsfind(selector)- Find descendant components
jQuery Plugin
$(el).component()- Get component instance$(el).component(Class, args)- Create component$.jqhtml.register(name, Class)- Register component$.jqhtml.create(name, args)- Create by name
Template Functions
render_template(component, template)- Render templatewith_template(template)- Component mixinprocess_instructions(instructions, target, context)- Process instruction array
Component and Template Registration Flexibility
JQHTML provides complete flexibility in how you define components - you can use JavaScript classes, .jqhtml template files, both, or neither. The framework automatically handles all combinations:
All Possible Combinations
-
Both JS Class and .jqhtml Template (Standard approach)
// UserCard.js class UserCard extends Jqhtml_Component { async load() { this.data.user = await fetch('/api/user').then(r => r.json()); } } jqhtml.register_component('UserCard', UserCard);<!-- UserCard.jqhtml --> <Define:UserCard> <div class="card"> <h2><%= this.data.user.name %></h2> </div> </Define:UserCard>Result: Uses your custom class logic + your custom template
-
.jqhtml Template Only (No JS class needed)
<!-- SimpleWidget.jqhtml --> <Define:SimpleWidget> <div class="widget"> Hello, <%= this.args.name %>! </div> </Define:SimpleWidget>Result: Uses default
Jqhtml_Componentclass + your template. Perfect for presentational components that don't need custom logic. -
JS Class Only (No .jqhtml template)
class DynamicComponent extends Jqhtml_Component { async render() { this.$.html(`<div>Rendered at ${Date.now()}</div>`); } } jqhtml.register_component('DynamicComponent', DynamicComponent);Result: Uses your class + default passthrough template. Useful when you want full programmatic control.
-
Neither Class nor Template (Passthrough)
<!-- In your JQHTML file --> <FooBar> <p>This content just passes through!</p> </FooBar>Result: Uses default
Jqhtml_Componentclass + default passthrough template. Acts as a simple wrapper that renders its inner content.
How It Works
When you use <ComponentName>:
- JS Class Resolution: Looks for registered class, falls back to
Jqhtml_Component - Template Resolution: Looks for registered template by class, then by name, then uses default passthrough
- Default Passthrough Template: Simply renders the component's inner HTML as-is
This means you can start simple and progressively add complexity:
<!-- Start with just markup -->
<UserBadge>Basic HTML content</UserBadge>
<!-- Later, add a .jqhtml template for consistent styling -->
<Define:UserBadge>
<div class="badge"><%= this.args.username %></div>
</Define:UserBadge>
<!-- Finally, add JS class for complex behavior -->
class UserBadge extends Jqhtml_Component {
async load() { /* fetch user data */ }
}
Laravel Integration
This package includes a Laravel bridge for error handling and source map support. After installing via npm, you can load it directly from node_modules in your Laravel application:
// In app/Providers/AppServiceProvider.php
public function register()
{
$jqhtmlBridge = base_path('node_modules/@jqhtml/core/laravel-bridge/autoload.php');
if (file_exists($jqhtmlBridge)) {
require_once $jqhtmlBridge;
}
}
See laravel-bridge/LARAVEL_INTEGRATION.md for complete integration instructions.
Development
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run tests
npm test
# Watch mode
npm run watch
License
MIT