Document .once() method and 'loaded' event, update npm packages

Update npm packages including @jqhtml/core

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-03-06 23:50:01 +00:00
parent 198cd42ce1
commit 3294fc7337
22 changed files with 292 additions and 82 deletions

View File

@@ -974,22 +974,36 @@ CREATING COMPONENTS
LIFECYCLE EVENT CALLBACKS LIFECYCLE EVENT CALLBACKS
External code can register callbacks for lifecycle events using External code can register callbacks for lifecycle events using
the .on() method. Useful when you need to know when a component the .on() and .once() methods. Useful when you need to know when
reaches a certain state. a component reaches a certain state.
Supported Events: Supported Events:
'render' - Fires after render phase completes 'render' - Fires after render phase completes
'create' - Fires after create phase completes 'create' - Fires after create phase completes
'load' - Fires after load phase completes (data available) 'load' - Fires after load phase completes (data available)
'loaded' - Fires after on_loaded() completes (this.data set and frozen)
'ready' - Fires after ready phase completes (fully initialized) 'ready' - Fires after ready phase completes (fully initialized)
Methods:
.on(event, callback) - Register callback, fires on every occurrence
.once(event, callback) - Register callback, fires only once
Both methods share the same behavior:
- If the event already fired, callback executes immediately
- Returns this for chaining
- Custom events also supported (see CUSTOM COMPONENT EVENTS)
The difference: .on() fires on every future occurrence (useful for
re-renders), while .once() fires at most once and then removes itself.
Awaiting Data in Async Methods: Awaiting Data in Async Methods:
When a method needs data that may not be loaded yet, await the 'load' When a method needs data that may not be loaded yet, await the
event. If the event already fired, the callback executes immediately: 'loaded' event. If the event already fired, the callback executes
immediately. Use .once() since you only need notification once:
async get_display_name() { async get_display_name() {
// Wait for on_load() to complete if not already // Wait for on_load() to complete if not already
await new Promise(resolve => this.on('load', resolve)); await new Promise(resolve => this.once('loaded', resolve));
return `${this.data.first_name} ${this.data.last_name}`; return `${this.data.first_name} ${this.data.last_name}`;
} }
@@ -1004,6 +1018,11 @@ LIFECYCLE EVENT CALLBACKS
// Access component data, DOM, etc. // Access component data, DOM, etc.
}); });
// Fire only once (even across re-renders)
component.once('ready', (comp) => {
console.log('First ready!', comp);
});
// Chain directly // Chain directly
$('#my-component').component().on('ready', (component) => { $('#my-component').component().on('ready', (component) => {
console.log('User data:', component.data.user); console.log('User data:', component.data.user);
@@ -1013,17 +1032,16 @@ LIFECYCLE EVENT CALLBACKS
component component
.on('render', () => console.log('Dashboard rendered')) .on('render', () => console.log('Dashboard rendered'))
.on('create', () => console.log('Dashboard created')) .on('create', () => console.log('Dashboard created'))
.on('load', () => console.log('Dashboard data loaded')) .on('loaded', () => console.log('Dashboard data loaded'))
.on('ready', () => console.log('Dashboard ready')); .on('ready', () => console.log('Dashboard ready'));
Key behaviors: Key behaviors:
- Immediate execution: If lifecycle event already occurred, - Immediate execution: If lifecycle event already occurred,
callback fires immediately callback fires immediately (both .on() and .once())
- Future events: Callback also registers for future occurrences - .on(): Callback persists for future occurrences (re-renders)
(useful for re-renders) - .once(): Callback fires once then auto-removes
- Multiple callbacks: Can register multiple for same event - Multiple callbacks: Can register multiple for same event
- Chaining: Returns this so you can chain .on() calls - Chaining: Both return this so you can chain calls
- Custom events also supported (see CUSTOM COMPONENT EVENTS)
Example - Wait for component initialization: Example - Wait for component initialization:
// React when dashboard component is ready // React when dashboard component is ready
@@ -1031,14 +1049,12 @@ LIFECYCLE EVENT CALLBACKS
console.log('Dashboard loaded:', this.data); console.log('Dashboard loaded:', this.data);
}); });
// Process component data after load // One-time notification when data loads
$('#data-grid').component().on('load', (comp) => { $('#data-grid').component().once('loaded', (comp) => {
const total = comp.data.items.reduce((sum, i) => sum + i.value, 0); const total = comp.data.items.reduce((sum, i) => sum + i.value, 0);
$('#total').text(total); $('#total').text(total);
}); });
Available in JQHTML v2.2.81+
CUSTOM COMPONENT EVENTS CUSTOM COMPONENT EVENTS
Components can fire and listen to custom events using the jqhtml event Components can fire and listen to custom events using the jqhtml event
bus. Unlike jQuery's .trigger()/.on(), the jqhtml event bus guarantees bus. Unlike jQuery's .trigger()/.on(), the jqhtml event bus guarantees
@@ -1059,6 +1075,11 @@ CUSTOM COMPONENT EVENTS
console.log('Event data:', data); console.log('Event data:', data);
}); });
// Listen only once
this.sid('child_component').once('my_event', (component, data) => {
console.log('First occurrence only');
});
From external code: From external code:
$('#element').component().on('my_event', (component, data) => { $('#element').component().on('my_event', (component, data) => {
// Handle event // Handle event
@@ -1066,6 +1087,7 @@ CUSTOM COMPONENT EVENTS
Callback Signature: Callback Signature:
.on('event_name', (component, data) => { ... }) .on('event_name', (component, data) => { ... })
.once('event_name', (component, data) => { ... })
- component: The component instance that fired the event - component: The component instance that fired the event
- data: Optional data passed as second argument to trigger() - data: Optional data passed as second argument to trigger()
@@ -1109,14 +1131,14 @@ EVENT HANDLER PLACEMENT
Where to register event handlers depends on what you're attaching to: Where to register event handlers depends on what you're attaching to:
Component Events (this.on()) - Register in on_create(): Component Events (this.on()/this.once()) - Register in on_create():
Component events attach to this.$ which persists across re-renders. Component events attach to this.$ which persists across re-renders.
Register once in on_create() to avoid infinite loops. Register once in on_create() to avoid infinite loops.
on_create() { on_create() {
// Component event - register once // Component event - register once
this.on('file-drop', (_, data) => this._handle(data)); this.on('file-drop', (_, data) => this._handle(data));
this.on('custom-event', (_, data) => this._process(data)); this.once('initialized', () => this._setup());
} }
DANGER: If registered in on_ready() and the handler triggers DANGER: If registered in on_ready() and the handler triggers
@@ -1146,9 +1168,9 @@ EVENT HANDLER PLACEMENT
} }
Summary: Summary:
this.on('event', ...) → on_create() (component events) this.on/once('event', ...) → on_create() (component events)
this.sid('child').on('event') → on_ready() (child component events) this.sid('child').on/once('event') → on_ready() (child component events)
this.$sid('elem').on('click') → on_render() (child DOM events) this.$sid('elem').on('click') → on_render() (child DOM events)
$REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
Convert any HTML element into a re-renderable component using the Convert any HTML element into a re-renderable component using the

View File

@@ -222,7 +222,7 @@ BREADCRUMB SYSTEM
// Helper to await loaded data // Helper to await loaded data
async _await_loaded() { async _await_loaded() {
if (this.data.contact && this.data.contact.id) return; if (this.data.contact && this.data.contact.id) return;
await new Promise(resolve => this.on('load', resolve)); await new Promise(resolve => this.once('loaded', resolve));
} }
async page_title() { async page_title() {
@@ -248,17 +248,17 @@ BREADCRUMB SYSTEM
Awaiting Loaded Data: Awaiting Loaded Data:
Breadcrumb methods are called BEFORE on_load() completes. If a method Breadcrumb methods are called BEFORE on_load() completes. If a method
needs loaded data (e.g., contact name), it must await the 'load' event: needs loaded data (e.g., contact name), it must await the 'loaded' event:
async _await_loaded() { async _await_loaded() {
// Check if data is already loaded // Check if data is already loaded
if (this.data.contact && this.data.contact.id) return; if (this.data.contact && this.data.contact.id) return;
// Otherwise wait for 'load' event // Otherwise wait for 'loaded' event
await new Promise(resolve => this.on('load', resolve)); await new Promise(resolve => this.once('loaded', resolve));
} }
The 'load' event fires immediately if already past that lifecycle The 'loaded' event fires after on_load() completes and this.data is
phase, so this pattern is safe to call multiple times. set. Using once() is appropriate since it only needs to fire once.
RSX_BREADCRUMB_RESOLVER RSX_BREADCRUMB_RESOLVER
Framework class that handles breadcrumb resolution with caching. Framework class that handles breadcrumb resolution with caching.

View File

@@ -641,8 +641,8 @@ async add_item() {
| What | Where | Why | | What | Where | Why |
|------|-------|-----| |------|-------|-----|
| `this.on('event', ...)` | `on_create()` | Persists across renders; on_ready() risks infinite loops from event replay | | `this.on/once('event', ...)` | `on_create()` | Persists across renders; on_ready() risks infinite loops from event replay |
| `this.sid('child').on('event')` | `on_ready()` | Child component events | | `this.sid('child').on/once('event')` | `on_ready()` | Child component events |
| `this.$sid('elem').on('click')` | `on_render()` or `on_ready()` | Child DOM recreated on render, must re-attach | | `this.$sid('elem').on('click')` | `on_render()` or `on_ready()` | Child DOM recreated on render, must re-attach |
### Loading Pattern ### Loading Pattern
@@ -692,6 +692,8 @@ From within component methods:
Fire: `this.trigger('event_name', data)` | Listen: `this.sid('child').on('event_name', (component, data) => {})` Fire: `this.trigger('event_name', data)` | Listen: `this.sid('child').on('event_name', (component, data) => {})`
**Methods**: `.on(event, cb)` fires on every occurrence. `.once(event, cb)` fires once then auto-removes. Both fire immediately if event already happened and return `this` for chaining.
**Key difference from jQuery**: Events fired BEFORE handler registration still trigger the callback when registered. This solves component lifecycle timing issues where child events fire before parent registers handlers. Never use `this.$.trigger()` for custom events (enforced by JQHTML-EVENT-01). **Key difference from jQuery**: Events fired BEFORE handler registration still trigger the callback when registered. This solves component lifecycle timing issues where child events fire before parent registers handlers. Never use `this.$.trigger()` for custom events (enforced by JQHTML-EVENT-01).
### Dynamic Component Creation ### Dynamic Component Creation

24
node_modules/.package-lock.json generated vendored
View File

@@ -2224,9 +2224,9 @@
} }
}, },
"node_modules/@jqhtml/core": { "node_modules/@jqhtml/core": {
"version": "2.3.39", "version": "2.3.41",
"resolved": "http://npm.internal.hanson.xyz/@jqhtml/core/-/core-2.3.39.tgz", "resolved": "http://npm.internal.hanson.xyz/@jqhtml/core/-/core-2.3.41.tgz",
"integrity": "sha512-qyxOBcoFCaf35etqvNOSJppqT4WQLfD9O2b8bAv5la4oSpRUmXSjVJFdv3cSMIK8qClXbupN8bm4FLbAalJqog==", "integrity": "sha512-Owf8Rf7yjG+WSRCPTXtTg+pFpWbTB+MnB/g2Clo6rVWZ5JxEqFZfmKIDx6lSX30pz16ph3RShe9Ijjc8V89S3w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.1",
@@ -2250,9 +2250,9 @@
} }
}, },
"node_modules/@jqhtml/parser": { "node_modules/@jqhtml/parser": {
"version": "2.3.39", "version": "2.3.41",
"resolved": "http://npm.internal.hanson.xyz/@jqhtml/parser/-/parser-2.3.39.tgz", "resolved": "http://npm.internal.hanson.xyz/@jqhtml/parser/-/parser-2.3.41.tgz",
"integrity": "sha512-DLPwZf1X7enf2lVOaFaIWlu8vQYMgk/+Lioup2w4F07oXFx2+MnFgcJ/Ie9Pf6VUnMT1IOIZQxOd/5QugwFFDA==", "integrity": "sha512-q6pT+eqWQf0qEgxzb61nERro5NkIeBnu/DQPUqRNZdywAqam8AHYlwzA5n54BlghJ6m/61DVeRMSHoVu1UV6lA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
@@ -2290,9 +2290,9 @@
} }
}, },
"node_modules/@jqhtml/ssr": { "node_modules/@jqhtml/ssr": {
"version": "2.3.39", "version": "2.3.41",
"resolved": "http://npm.internal.hanson.xyz/@jqhtml/ssr/-/ssr-2.3.39.tgz", "resolved": "http://npm.internal.hanson.xyz/@jqhtml/ssr/-/ssr-2.3.41.tgz",
"integrity": "sha512-//MaIub8tel8w6l3AiqvoW021Aj9JR8BlVrZsezAO7svAIgsMFTeFdLKUud1+rg8I5Nxe4DE8CiGHz+f3Ts0kA==", "integrity": "sha512-9uNQ7QaaBBU49ncEKxv9uoajfxe3/vt1wLOMrex81oqKB1PHFIkfQbQ1QcNakYgDTXMFkXKinH0O3qEROH9Lxw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jquery": "^3.7.1", "jquery": "^3.7.1",
@@ -2386,9 +2386,9 @@
} }
}, },
"node_modules/@jqhtml/vscode-extension": { "node_modules/@jqhtml/vscode-extension": {
"version": "2.3.39", "version": "2.3.41",
"resolved": "http://npm.internal.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.39.tgz", "resolved": "http://npm.internal.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.41.tgz",
"integrity": "sha512-Zi0iS5/t+5IhQoZP54J1/OOFB2OdoM6TM3g37SMJmPKjIDBUt883M3POszKFJwfj8+lrBV5OeJPOmPu3m9RYOQ==", "integrity": "sha512-CB3tIppMT3cVLiOIAAxymMtLAae2FJfkf6aFSkQOiONK47h10k2/QkkXFJwXyRRnzbw+ijuhBCDodiLlJtt8aw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"vscode": "^1.74.0" "vscode": "^1.74.0"

View File

@@ -21,6 +21,17 @@
* @param callback - Callback: (component, data?) => void * @param callback - Callback: (component, data?) => void
*/ */
export declare function event_on(component: any, event_name: string, callback: (comp: any, data?: any) => void): any; export declare function event_on(component: any, event_name: string, callback: (comp: any, data?: any) => void): any;
/**
* Register a callback that fires exactly once.
*
* - If the event already occurred (sticky), fires immediately and does NOT register.
* - If the event has not occurred, registers and auto-deregisters after first fire.
*
* @param component - The component instance
* @param event_name - Name of the event
* @param callback - Callback: (component, data?) => void
*/
export declare function event_once(component: any, event_name: string, callback: (comp: any, data?: any) => void): any;
/** /**
* Trigger an event - fires all registered callbacks. * Trigger an event - fires all registered callbacks.
* Marks event as occurred so future .on() calls fire immediately. * Marks event as occurred so future .on() calls fire immediately.

View File

@@ -1 +1 @@
{"version":3,"file":"component-events.d.ts","sourceRoot":"","sources":["../src/component-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,GAAG,CAqB3G;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,CAelF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAG/E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAEzE"} {"version":3,"file":"component-events.d.ts","sourceRoot":"","sources":["../src/component-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,GAAG,CAqB3G;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,GAAG,CA+B7G;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,CAelF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAG/E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAEzE"}

View File

@@ -53,6 +53,7 @@ export declare class Jqhtml_Component {
private _load_only; private _load_only;
private _load_render_only; private _load_render_only;
_is_detached: boolean; _is_detached: boolean;
private _force_initial_render;
private _has_rendered; private _has_rendered;
private _load_queue; private _load_queue;
private __has_custom_on_load; private __has_custom_on_load;
@@ -344,6 +345,12 @@ export declare class Jqhtml_Component {
* @see component-events.ts for full documentation * @see component-events.ts for full documentation
*/ */
on(event_name: string, callback: (component: Jqhtml_Component, data?: any) => void): this; on(event_name: string, callback: (component: Jqhtml_Component, data?: any) => void): this;
/**
* Register a callback that fires exactly once - delegates to component-events.ts
* If the event already occurred, fires immediately and does not register.
* @see component-events.ts for full documentation
*/
once(event_name: string, callback: (component: Jqhtml_Component, data?: any) => void): this;
/** /**
* Trigger a lifecycle event - delegates to component-events.ts * Trigger a lifecycle event - delegates to component-events.ts
* @see component-events.ts for full documentation * @see component-events.ts for full documentation

View File

@@ -1 +1 @@
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"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"} {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"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"}

View File

@@ -2199,6 +2199,46 @@ function event_on(component, event_name, callback) {
} }
return component; return component;
} }
/**
* Register a callback that fires exactly once.
*
* - If the event already occurred (sticky), fires immediately and does NOT register.
* - If the event has not occurred, registers and auto-deregisters after first fire.
*
* @param component - The component instance
* @param event_name - Name of the event
* @param callback - Callback: (component, data?) => void
*/
function event_once(component, event_name, callback) {
// If event already occurred, fire immediately and we're done
if (component._lifecycle_states.has(event_name)) {
try {
const stored_data = component._lifecycle_states.get(event_name);
callback(component, stored_data);
}
catch (error) {
console.error(`[JQHTML] Error in ${event_name} once callback:`, error);
}
return component;
}
// Wrap callback to auto-deregister after first fire
const wrapper = (comp, data) => {
// Remove ourselves from the callback list
const callbacks = component._lifecycle_callbacks.get(event_name);
if (callbacks) {
const idx = callbacks.indexOf(wrapper);
if (idx !== -1)
callbacks.splice(idx, 1);
}
callback(comp, data);
};
// Initialize callback array for this event if needed
if (!component._lifecycle_callbacks.has(event_name)) {
component._lifecycle_callbacks.set(event_name, []);
}
component._lifecycle_callbacks.get(event_name).push(wrapper);
return component;
}
/** /**
* Trigger an event - fires all registered callbacks. * Trigger an event - fires all registered callbacks.
* Marks event as occurred so future .on() calls fire immediately. * Marks event as occurred so future .on() calls fire immediately.
@@ -2847,7 +2887,11 @@ class Jqhtml_Component {
this._load_render_only = false; this._load_render_only = false;
// Detached optimization: true when element is not in the DOM at boot time. // Detached optimization: true when element is not in the DOM at boot time.
// Skips initial render and cache read — just on_load then render. // Skips initial render and cache read — just on_load then render.
// Override with _force_initial_render to keep normal double-render behavior.
this._is_detached = false; this._is_detached = false;
// _force_initial_render: override detached optimization — render even when not in DOM.
// Use when you need the loading spinner visible immediately after appending.
this._force_initial_render = false;
// rendered event - fires once after the synchronous render chain completes // rendered event - fires once after the synchronous render chain completes
// (after on_load's re-render if applicable, or after first render if no on_load) // (after on_load's re-render if applicable, or after first render if no on_load)
this._has_rendered = false; this._has_rendered = false;
@@ -2906,6 +2950,9 @@ class Jqhtml_Component {
if (this.args._load_render_only === true) { if (this.args._load_render_only === true) {
this._load_render_only = true; this._load_render_only = true;
} }
if (this.args._force_initial_render === true) {
this._force_initial_render = true;
}
// Attach component to element // Attach component to element
this.$.data('_component', this); this.$.data('_component', this);
// Apply CSS classes and attributes // Apply CSS classes and attributes
@@ -3396,7 +3443,7 @@ class Jqhtml_Component {
// Suppressed by _load_only and _load_render_only flags (preloading mode) // Suppressed by _load_only and _load_render_only flags (preloading mode)
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
}); });
return data_changed; return data_changed;
@@ -3417,7 +3464,8 @@ class Jqhtml_Component {
// Don't await - on_create MUST be sync. The warning is enough. // Don't await - on_create MUST be sync. The warning is enough.
} }
// Detect detached elements — skip cache and initial render for elements not in DOM // Detect detached elements — skip cache and initial render for elements not in DOM
this._is_detached = !this.$[0].isConnected; // _force_initial_render overrides this optimization
this._is_detached = !this.$[0].isConnected && !this._force_initial_render;
// OPTIMIZATION: Skip cache operations and snapshot if no custom on_load() // OPTIMIZATION: Skip cache operations and snapshot if no custom on_load()
// Components without on_load() don't fetch data, so nothing to cache or restore // Components without on_load() don't fetch data, so nothing to cache or restore
if (this.__has_custom_on_load) { if (this.__has_custom_on_load) {
@@ -3463,7 +3511,7 @@ class Jqhtml_Component {
this.trigger('load'); this.trigger('load');
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
return; return;
} }
@@ -3543,7 +3591,7 @@ class Jqhtml_Component {
this.trigger('load'); this.trigger('load');
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
return; return;
} }
@@ -3623,7 +3671,7 @@ class Jqhtml_Component {
// Suppressed by _load_only and _load_render_only flags (preloading mode) // Suppressed by _load_only and _load_render_only flags (preloading mode)
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
} }
finally { finally {
@@ -4080,6 +4128,14 @@ class Jqhtml_Component {
on(event_name, callback) { on(event_name, callback) {
return event_on(this, event_name, callback); return event_on(this, event_name, callback);
} }
/**
* Register a callback that fires exactly once - delegates to component-events.ts
* If the event already occurred, fires immediately and does not register.
* @see component-events.ts for full documentation
*/
once(event_name, callback) {
return event_once(this, event_name, callback);
}
/** /**
* Trigger a lifecycle event - delegates to component-events.ts * Trigger a lifecycle event - delegates to component-events.ts
* @see component-events.ts for full documentation * @see component-events.ts for full documentation
@@ -5280,7 +5336,7 @@ function init(jQuery) {
} }
} }
// Version - will be replaced during build with actual version from package.json // Version - will be replaced during build with actual version from package.json
const version = '2.3.39'; const version = '2.3.41';
// Default export with all functionality // Default export with all functionality
const jqhtml = { const jqhtml = {
// Core // Core

File diff suppressed because one or more lines are too long

View File

@@ -2195,6 +2195,46 @@ function event_on(component, event_name, callback) {
} }
return component; return component;
} }
/**
* Register a callback that fires exactly once.
*
* - If the event already occurred (sticky), fires immediately and does NOT register.
* - If the event has not occurred, registers and auto-deregisters after first fire.
*
* @param component - The component instance
* @param event_name - Name of the event
* @param callback - Callback: (component, data?) => void
*/
function event_once(component, event_name, callback) {
// If event already occurred, fire immediately and we're done
if (component._lifecycle_states.has(event_name)) {
try {
const stored_data = component._lifecycle_states.get(event_name);
callback(component, stored_data);
}
catch (error) {
console.error(`[JQHTML] Error in ${event_name} once callback:`, error);
}
return component;
}
// Wrap callback to auto-deregister after first fire
const wrapper = (comp, data) => {
// Remove ourselves from the callback list
const callbacks = component._lifecycle_callbacks.get(event_name);
if (callbacks) {
const idx = callbacks.indexOf(wrapper);
if (idx !== -1)
callbacks.splice(idx, 1);
}
callback(comp, data);
};
// Initialize callback array for this event if needed
if (!component._lifecycle_callbacks.has(event_name)) {
component._lifecycle_callbacks.set(event_name, []);
}
component._lifecycle_callbacks.get(event_name).push(wrapper);
return component;
}
/** /**
* Trigger an event - fires all registered callbacks. * Trigger an event - fires all registered callbacks.
* Marks event as occurred so future .on() calls fire immediately. * Marks event as occurred so future .on() calls fire immediately.
@@ -2843,7 +2883,11 @@ class Jqhtml_Component {
this._load_render_only = false; this._load_render_only = false;
// Detached optimization: true when element is not in the DOM at boot time. // Detached optimization: true when element is not in the DOM at boot time.
// Skips initial render and cache read — just on_load then render. // Skips initial render and cache read — just on_load then render.
// Override with _force_initial_render to keep normal double-render behavior.
this._is_detached = false; this._is_detached = false;
// _force_initial_render: override detached optimization — render even when not in DOM.
// Use when you need the loading spinner visible immediately after appending.
this._force_initial_render = false;
// rendered event - fires once after the synchronous render chain completes // rendered event - fires once after the synchronous render chain completes
// (after on_load's re-render if applicable, or after first render if no on_load) // (after on_load's re-render if applicable, or after first render if no on_load)
this._has_rendered = false; this._has_rendered = false;
@@ -2902,6 +2946,9 @@ class Jqhtml_Component {
if (this.args._load_render_only === true) { if (this.args._load_render_only === true) {
this._load_render_only = true; this._load_render_only = true;
} }
if (this.args._force_initial_render === true) {
this._force_initial_render = true;
}
// Attach component to element // Attach component to element
this.$.data('_component', this); this.$.data('_component', this);
// Apply CSS classes and attributes // Apply CSS classes and attributes
@@ -3392,7 +3439,7 @@ class Jqhtml_Component {
// Suppressed by _load_only and _load_render_only flags (preloading mode) // Suppressed by _load_only and _load_render_only flags (preloading mode)
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
}); });
return data_changed; return data_changed;
@@ -3413,7 +3460,8 @@ class Jqhtml_Component {
// Don't await - on_create MUST be sync. The warning is enough. // Don't await - on_create MUST be sync. The warning is enough.
} }
// Detect detached elements — skip cache and initial render for elements not in DOM // Detect detached elements — skip cache and initial render for elements not in DOM
this._is_detached = !this.$[0].isConnected; // _force_initial_render overrides this optimization
this._is_detached = !this.$[0].isConnected && !this._force_initial_render;
// OPTIMIZATION: Skip cache operations and snapshot if no custom on_load() // OPTIMIZATION: Skip cache operations and snapshot if no custom on_load()
// Components without on_load() don't fetch data, so nothing to cache or restore // Components without on_load() don't fetch data, so nothing to cache or restore
if (this.__has_custom_on_load) { if (this.__has_custom_on_load) {
@@ -3459,7 +3507,7 @@ class Jqhtml_Component {
this.trigger('load'); this.trigger('load');
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
return; return;
} }
@@ -3539,7 +3587,7 @@ class Jqhtml_Component {
this.trigger('load'); this.trigger('load');
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
return; return;
} }
@@ -3619,7 +3667,7 @@ class Jqhtml_Component {
// Suppressed by _load_only and _load_render_only flags (preloading mode) // Suppressed by _load_only and _load_render_only flags (preloading mode)
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
} }
finally { finally {
@@ -4076,6 +4124,14 @@ class Jqhtml_Component {
on(event_name, callback) { on(event_name, callback) {
return event_on(this, event_name, callback); return event_on(this, event_name, callback);
} }
/**
* Register a callback that fires exactly once - delegates to component-events.ts
* If the event already occurred, fires immediately and does not register.
* @see component-events.ts for full documentation
*/
once(event_name, callback) {
return event_once(this, event_name, callback);
}
/** /**
* Trigger a lifecycle event - delegates to component-events.ts * Trigger a lifecycle event - delegates to component-events.ts
* @see component-events.ts for full documentation * @see component-events.ts for full documentation
@@ -5276,7 +5332,7 @@ function init(jQuery) {
} }
} }
// Version - will be replaced during build with actual version from package.json // Version - will be replaced during build with actual version from package.json
const version = '2.3.39'; const version = '2.3.41';
// Default export with all functionality // Default export with all functionality
const jqhtml = { const jqhtml = {
// Core // Core

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
/** /**
* JQHTML Core v2.3.39 * JQHTML Core v2.3.41
* (c) 2025 JQHTML Team * (c) 2025 JQHTML Team
* Released under the MIT License * Released under the MIT License
*/ */
@@ -2200,6 +2200,46 @@ function event_on(component, event_name, callback) {
} }
return component; return component;
} }
/**
* Register a callback that fires exactly once.
*
* - If the event already occurred (sticky), fires immediately and does NOT register.
* - If the event has not occurred, registers and auto-deregisters after first fire.
*
* @param component - The component instance
* @param event_name - Name of the event
* @param callback - Callback: (component, data?) => void
*/
function event_once(component, event_name, callback) {
// If event already occurred, fire immediately and we're done
if (component._lifecycle_states.has(event_name)) {
try {
const stored_data = component._lifecycle_states.get(event_name);
callback(component, stored_data);
}
catch (error) {
console.error(`[JQHTML] Error in ${event_name} once callback:`, error);
}
return component;
}
// Wrap callback to auto-deregister after first fire
const wrapper = (comp, data) => {
// Remove ourselves from the callback list
const callbacks = component._lifecycle_callbacks.get(event_name);
if (callbacks) {
const idx = callbacks.indexOf(wrapper);
if (idx !== -1)
callbacks.splice(idx, 1);
}
callback(comp, data);
};
// Initialize callback array for this event if needed
if (!component._lifecycle_callbacks.has(event_name)) {
component._lifecycle_callbacks.set(event_name, []);
}
component._lifecycle_callbacks.get(event_name).push(wrapper);
return component;
}
/** /**
* Trigger an event - fires all registered callbacks. * Trigger an event - fires all registered callbacks.
* Marks event as occurred so future .on() calls fire immediately. * Marks event as occurred so future .on() calls fire immediately.
@@ -2848,7 +2888,11 @@ class Jqhtml_Component {
this._load_render_only = false; this._load_render_only = false;
// Detached optimization: true when element is not in the DOM at boot time. // Detached optimization: true when element is not in the DOM at boot time.
// Skips initial render and cache read — just on_load then render. // Skips initial render and cache read — just on_load then render.
// Override with _force_initial_render to keep normal double-render behavior.
this._is_detached = false; this._is_detached = false;
// _force_initial_render: override detached optimization — render even when not in DOM.
// Use when you need the loading spinner visible immediately after appending.
this._force_initial_render = false;
// rendered event - fires once after the synchronous render chain completes // rendered event - fires once after the synchronous render chain completes
// (after on_load's re-render if applicable, or after first render if no on_load) // (after on_load's re-render if applicable, or after first render if no on_load)
this._has_rendered = false; this._has_rendered = false;
@@ -2907,6 +2951,9 @@ class Jqhtml_Component {
if (this.args._load_render_only === true) { if (this.args._load_render_only === true) {
this._load_render_only = true; this._load_render_only = true;
} }
if (this.args._force_initial_render === true) {
this._force_initial_render = true;
}
// Attach component to element // Attach component to element
this.$.data('_component', this); this.$.data('_component', this);
// Apply CSS classes and attributes // Apply CSS classes and attributes
@@ -3397,7 +3444,7 @@ class Jqhtml_Component {
// Suppressed by _load_only and _load_render_only flags (preloading mode) // Suppressed by _load_only and _load_render_only flags (preloading mode)
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
}); });
return data_changed; return data_changed;
@@ -3418,7 +3465,8 @@ class Jqhtml_Component {
// Don't await - on_create MUST be sync. The warning is enough. // Don't await - on_create MUST be sync. The warning is enough.
} }
// Detect detached elements — skip cache and initial render for elements not in DOM // Detect detached elements — skip cache and initial render for elements not in DOM
this._is_detached = !this.$[0].isConnected; // _force_initial_render overrides this optimization
this._is_detached = !this.$[0].isConnected && !this._force_initial_render;
// OPTIMIZATION: Skip cache operations and snapshot if no custom on_load() // OPTIMIZATION: Skip cache operations and snapshot if no custom on_load()
// Components without on_load() don't fetch data, so nothing to cache or restore // Components without on_load() don't fetch data, so nothing to cache or restore
if (this.__has_custom_on_load) { if (this.__has_custom_on_load) {
@@ -3464,7 +3512,7 @@ class Jqhtml_Component {
this.trigger('load'); this.trigger('load');
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
return; return;
} }
@@ -3544,7 +3592,7 @@ class Jqhtml_Component {
this.trigger('load'); this.trigger('load');
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
return; return;
} }
@@ -3624,7 +3672,7 @@ class Jqhtml_Component {
// Suppressed by _load_only and _load_render_only flags (preloading mode) // Suppressed by _load_only and _load_render_only flags (preloading mode)
if (!this._load_only && !this._load_render_only) { if (!this._load_only && !this._load_render_only) {
await this._call_lifecycle('on_loaded'); await this._call_lifecycle('on_loaded');
this.trigger('on_loaded'); this.trigger('loaded');
} }
} }
finally { finally {
@@ -4081,6 +4129,14 @@ class Jqhtml_Component {
on(event_name, callback) { on(event_name, callback) {
return event_on(this, event_name, callback); return event_on(this, event_name, callback);
} }
/**
* Register a callback that fires exactly once - delegates to component-events.ts
* If the event already occurred, fires immediately and does not register.
* @see component-events.ts for full documentation
*/
once(event_name, callback) {
return event_once(this, event_name, callback);
}
/** /**
* Trigger a lifecycle event - delegates to component-events.ts * Trigger a lifecycle event - delegates to component-events.ts
* @see component-events.ts for full documentation * @see component-events.ts for full documentation
@@ -5281,7 +5337,7 @@ function init(jQuery) {
} }
} }
// Version - will be replaced during build with actual version from package.json // Version - will be replaced during build with actual version from package.json
const version = '2.3.39'; const version = '2.3.41';
// Default export with all functionality // Default export with all functionality
const jqhtml = { const jqhtml = {
// Core // Core

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{ {
"name": "@jqhtml/core", "name": "@jqhtml/core",
"version": "2.3.39", "version": "2.3.41",
"description": "Core runtime library for JQHTML", "description": "Core runtime library for JQHTML",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1385,7 +1385,7 @@ export class CodeGenerator {
for (const [name, component] of this.components) { for (const [name, component] of this.components) {
code += `// Component: ${name}\n`; code += `// Component: ${name}\n`;
code += `jqhtml_components.set('${name}', {\n`; code += `jqhtml_components.set('${name}', {\n`;
code += ` _jqhtml_version: '2.3.39',\n`; // Version will be replaced during build code += ` _jqhtml_version: '2.3.41',\n`; // Version will be replaced during build
code += ` name: '${name}',\n`; code += ` name: '${name}',\n`;
code += ` tag: '${component.tagName}',\n`; code += ` tag: '${component.tagName}',\n`;
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`; code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`;

View File

@@ -1,6 +1,6 @@
{ {
"name": "@jqhtml/parser", "name": "@jqhtml/parser",
"version": "2.3.39", "version": "2.3.41",
"description": "JQHTML template parser - converts templates to JavaScript", "description": "JQHTML template parser - converts templates to JavaScript",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@jqhtml/ssr", "name": "@jqhtml/ssr",
"version": "2.3.39", "version": "2.3.41",
"description": "Server-Side Rendering for JQHTML components - renders components to HTML for SEO", "description": "Server-Side Rendering for JQHTML components - renders components to HTML for SEO",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@@ -1 +1 @@
2.3.39 2.3.41

View File

@@ -2,7 +2,7 @@
"name": "@jqhtml/vscode-extension", "name": "@jqhtml/vscode-extension",
"displayName": "JQHTML", "displayName": "JQHTML",
"description": "Syntax highlighting and language support for JQHTML template files", "description": "Syntax highlighting and language support for JQHTML template files",
"version": "2.3.39", "version": "2.3.41",
"publisher": "jqhtml", "publisher": "jqhtml",
"license": "MIT", "license": "MIT",
"publishConfig": { "publishConfig": {

24
package-lock.json generated
View File

@@ -2676,9 +2676,9 @@
} }
}, },
"node_modules/@jqhtml/core": { "node_modules/@jqhtml/core": {
"version": "2.3.39", "version": "2.3.41",
"resolved": "http://npm.internal.hanson.xyz/@jqhtml/core/-/core-2.3.39.tgz", "resolved": "http://npm.internal.hanson.xyz/@jqhtml/core/-/core-2.3.41.tgz",
"integrity": "sha512-qyxOBcoFCaf35etqvNOSJppqT4WQLfD9O2b8bAv5la4oSpRUmXSjVJFdv3cSMIK8qClXbupN8bm4FLbAalJqog==", "integrity": "sha512-Owf8Rf7yjG+WSRCPTXtTg+pFpWbTB+MnB/g2Clo6rVWZ5JxEqFZfmKIDx6lSX30pz16ph3RShe9Ijjc8V89S3w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.1",
@@ -2702,9 +2702,9 @@
} }
}, },
"node_modules/@jqhtml/parser": { "node_modules/@jqhtml/parser": {
"version": "2.3.39", "version": "2.3.41",
"resolved": "http://npm.internal.hanson.xyz/@jqhtml/parser/-/parser-2.3.39.tgz", "resolved": "http://npm.internal.hanson.xyz/@jqhtml/parser/-/parser-2.3.41.tgz",
"integrity": "sha512-DLPwZf1X7enf2lVOaFaIWlu8vQYMgk/+Lioup2w4F07oXFx2+MnFgcJ/Ie9Pf6VUnMT1IOIZQxOd/5QugwFFDA==", "integrity": "sha512-q6pT+eqWQf0qEgxzb61nERro5NkIeBnu/DQPUqRNZdywAqam8AHYlwzA5n54BlghJ6m/61DVeRMSHoVu1UV6lA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
@@ -2742,9 +2742,9 @@
} }
}, },
"node_modules/@jqhtml/ssr": { "node_modules/@jqhtml/ssr": {
"version": "2.3.39", "version": "2.3.41",
"resolved": "http://npm.internal.hanson.xyz/@jqhtml/ssr/-/ssr-2.3.39.tgz", "resolved": "http://npm.internal.hanson.xyz/@jqhtml/ssr/-/ssr-2.3.41.tgz",
"integrity": "sha512-//MaIub8tel8w6l3AiqvoW021Aj9JR8BlVrZsezAO7svAIgsMFTeFdLKUud1+rg8I5Nxe4DE8CiGHz+f3Ts0kA==", "integrity": "sha512-9uNQ7QaaBBU49ncEKxv9uoajfxe3/vt1wLOMrex81oqKB1PHFIkfQbQ1QcNakYgDTXMFkXKinH0O3qEROH9Lxw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jquery": "^3.7.1", "jquery": "^3.7.1",
@@ -2838,9 +2838,9 @@
} }
}, },
"node_modules/@jqhtml/vscode-extension": { "node_modules/@jqhtml/vscode-extension": {
"version": "2.3.39", "version": "2.3.41",
"resolved": "http://npm.internal.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.39.tgz", "resolved": "http://npm.internal.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.41.tgz",
"integrity": "sha512-Zi0iS5/t+5IhQoZP54J1/OOFB2OdoM6TM3g37SMJmPKjIDBUt883M3POszKFJwfj8+lrBV5OeJPOmPu3m9RYOQ==", "integrity": "sha512-CB3tIppMT3cVLiOIAAxymMtLAae2FJfkf6aFSkQOiONK47h10k2/QkkXFJwXyRRnzbw+ijuhBCDodiLlJtt8aw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"vscode": "^1.74.0" "vscode": "^1.74.0"