# @jqhtml/core
Core runtime library for JQHTML v2 - jQuery-first component framework.
## Installation
```bash
npm install @jqhtml/core jquery
```
## Module Structure
The core package exports two separate entry points:
### Main Runtime (Production)
```javascript
import { Component, jqhtml } from '@jqhtml/core';
```
### Debug Utilities (Development Only)
```javascript
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
```javascript
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(`
${this.data.user.name}
${this.data.user.email}
`);
}
async ready() {
// Component fully initialized
this.$.removeClass('loading');
}
}
```
### Using jQuery Plugin
```javascript
// 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:
1. **constructor** - Instance creation, jQuery element setup (`this.data` is `{}`)
2. **render** - Create initial DOM structure (top-down, atomic/synchronous operation)
3. **create** - Quick setup after DOM creation (bottom-up, siblings parallel)
4. **load** - Fetch data (bottom-up, fully parallel, **ABSOLUTELY NO DOM MODIFICATIONS**)
5. **render** (automatic) - Re-render if data changed during load (empties DOM first)
6. **create** (automatic) - Re-setup if component was re-rendered
7. **ready** - Fully initialized (bottom-up, siblings parallel)
8. **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.data` contains 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**
```jqhtml
<% if (Object.keys(this.data).length === 0): %>
Loading user profile...
<% else: %>
<%= this.data.user?.name || 'Unknown User' %>
<%= this.data.user?.bio || 'No bio available' %>
<% endif; %>
```
**Pattern 2: DOM Changes in create() and ready()**
```javascript
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:**
```javascript
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:
1. Calls `render()` again (which empties the DOM with `$.empty()` first)
2. Calls `create()` again to re-setup the new DOM
3. 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()`:
```javascript
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:
```javascript
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:
1. Empties the component's DOM with `this.$.empty()`
2. Calls `render()` to rebuild the DOM atomically
3. For automatic re-renders, calls `create()` again for the new DOM
4. Finally calls `ready()`
5. For manual re-renders, you control which lifecycle methods to call
## Scoped IDs
Components use `_cid` for scoped element selection:
```javascript
class TabsComponent extends Component {
async render() {
this.$.html(`
`);
}
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`:
```javascript
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](../../docs/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:
```html
```
## 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
```javascript
// 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
1. **Immediate Execution**: If the lifecycle event has already occurred when you register the callback, it fires immediately
2. **Future Events**: The callback also registers for future occurrences of the same event (useful for re-renders)
3. **Multiple Callbacks**: You can register multiple callbacks for the same event
4. **Error Safety**: Errors in callbacks are caught and logged without breaking the component
5. **Validation**: Only lifecycle events are allowed; other event names trigger a console error
### Example: Wait for Component to be Ready
```javascript
// 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
```javascript
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)
```javascript
// 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:
```javascript
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 instance
- `async render()` - Create DOM structure
- `async init()` - Quick setup phase
- `async load()` - Data fetching phase
- `async ready()` - Final initialization
- `destroy()` - Cleanup
- `should_rerender()` - Control re-rendering after load
- `emit(event, data)` - Emit jQuery events
- `on(event, callback)` - Register lifecycle event callback
- `$id(localId)` - Get scoped jQuery element
- `id(localId)` - Get scoped component instance
- `parent()` - Get parent component
- `children()` - Get direct child components
- `find(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 template
- `with_template(template)` - Component mixin
- `process_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
1. **Both JS Class and .jqhtml Template** (Standard approach)
```javascript
// 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);
```
```html
<%= this.data.user.name %>
```
**Result**: Uses your custom class logic + your custom template
2. **.jqhtml Template Only** (No JS class needed)
```html
Hello, <%= this.args.name %>!
```
**Result**: Uses default `Jqhtml_Component` class + your template. Perfect for presentational components that don't need custom logic.
3. **JS Class Only** (No .jqhtml template)
```javascript
class DynamicComponent extends Jqhtml_Component {
async render() {
this.$.html(`
Rendered at ${Date.now()}
`);
}
}
jqhtml.register_component('DynamicComponent', DynamicComponent);
```
**Result**: Uses your class + default passthrough template. Useful when you want full programmatic control.
4. **Neither Class nor Template** (Passthrough)
```html
This content just passes through!
```
**Result**: Uses default `Jqhtml_Component` class + default passthrough template. Acts as a simple wrapper that renders its inner content.
### How It Works
When you use ``:
- **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:
```html
Basic HTML content
<%= this.args.username %>
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:
```php
// 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](./laravel-bridge/LARAVEL_INTEGRATION.md) for complete integration instructions.
## Development
```bash
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run tests
npm test
# Watch mode
npm run watch
```
## License
MIT