Add JQHTML-EVENT-01 rule, document custom component events, update jqhtml

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-12 03:42:24 +00:00
parent c4ba2b743f
commit 5d5bd85e42
19 changed files with 364 additions and 79 deletions

View File

@@ -0,0 +1,197 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\Core\Manifest\Manifest;
/**
* Detects use of jQuery .trigger() for custom events in jqhtml components.
*
* Custom events should use the jqhtml event bus (this.trigger()) instead of
* jQuery's trigger() because the jqhtml bus supports firing events that
* occurred before handlers were registered - critical for component lifecycle.
*/
class JqhtmlCustomEvent_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Standard jQuery/DOM events that are legitimate to trigger via jQuery
*/
private const ALLOWED_EVENTS = [
// Mouse events
'click',
'dblclick',
'mousedown',
'mouseup',
'mouseover',
'mouseout',
'mouseenter',
'mouseleave',
'mousemove',
'contextmenu',
// Keyboard events
'keydown',
'keyup',
'keypress',
// Form events
'submit',
'change',
'input',
'focus',
'blur',
'focusin',
'focusout',
'select',
'reset',
// Touch events
'touchstart',
'touchend',
'touchmove',
'touchcancel',
// Drag events
'drag',
'dragstart',
'dragend',
'dragover',
'dragenter',
'dragleave',
'drop',
// Scroll/resize
'scroll',
'resize',
// Load events
'load',
'unload',
'error',
// Clipboard events
'copy',
'cut',
'paste',
];
public function get_id(): string
{
return 'JQHTML-EVENT-01';
}
public function get_name(): string
{
return 'JQHTML Custom Event Check';
}
public function get_description(): string
{
return 'Custom events in jqhtml components should use this.trigger() instead of jQuery .trigger()';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
public function is_called_during_manifest_scan(): bool
{
return true;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get JavaScript class from manifest metadata
$js_classes = [];
if (isset($metadata['class']) && isset($metadata['extension']) && $metadata['extension'] === 'js') {
$js_classes = [$metadata['class']];
}
// If no classes in metadata, nothing to check
if (empty($js_classes)) {
return;
}
// Check each class to see if it's a JQHTML component
foreach ($js_classes as $class_name) {
// Use Manifest to check inheritance
if (!Manifest::js_is_subclass_of($class_name, 'Component')) {
continue;
}
// The $contents passed to check() is already sanitized (strings removed).
// We need the original file content to extract event names from string literals.
$original_contents = file_get_contents($file_path);
$original_lines = explode("\n", $original_contents);
// Sanitized lines are used to skip comments
$sanitized_lines = explode("\n", $contents);
// Look for .trigger() calls on this.$ or that.$
foreach ($original_lines as $line_number => $line) {
$actual_line_number = $line_number + 1;
// Use sanitized line to check if this is a comment
$sanitized_line = $sanitized_lines[$line_number] ?? '';
$trimmed = trim($sanitized_line);
// Skip comments (check sanitized version which preserves comment markers)
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*')) {
continue;
}
// Check for exception comment on this line or previous line (use original)
if (str_contains($line, '@' . $this->get_id() . '-EXCEPTION')) {
continue;
}
if ($line_number > 0 && str_contains($original_lines[$line_number - 1], '@' . $this->get_id() . '-EXCEPTION')) {
continue;
}
// Match patterns like: this.$.trigger( or that.$.trigger(
// Also match: this.$sid('x').trigger( or that.$sid('x').trigger(
if (preg_match('/(this|that)\.\$(?:sid\s*\([^)]+\))?\s*\.\s*trigger\s*\(\s*[\'"]([^\'"]+)[\'"]/', $line, $matches)) {
$event_name = $matches[2];
// Check if it's an allowed standard event
if (in_array(strtolower($event_name), self::ALLOWED_EVENTS, true)) {
continue;
}
$this->add_violation(
$file_path,
$actual_line_number,
"Custom event '{$event_name}' triggered via jQuery .trigger() in Component class '{$class_name}'. " .
'Use the jqhtml event bus instead.',
trim($line),
"Use jqhtml event system for custom events:\n\n" .
"FIRING EVENTS:\n" .
" Instead of: this.\$.trigger('{$event_name}', data)\n" .
" Use: this.trigger('{$event_name}', data)\n\n" .
"LISTENING FROM PARENT (using \$sid):\n" .
" Instead of: this.\$sid('child').on('{$event_name}', handler)\n" .
" Use: this.sid('child').on('{$event_name}', handler)\n\n" .
"LISTENING FROM EXTERNAL CODE:\n" .
" Instead of: \$(element).on('{$event_name}', handler)\n" .
" Use: \$(element).component().on('{$event_name}', handler)\n\n" .
"WHY: The jqhtml event bus fires callbacks for events that occurred before the handler " .
"was registered. This is critical for component lifecycle - parent components may " .
"register handlers after child components have already fired their events during initialization.",
'high'
);
}
}
}
}
}

View File

@@ -865,7 +865,7 @@ LIFECYCLE EVENT CALLBACKS
(useful for re-renders)
- Multiple callbacks: Can register multiple for same event
- Chaining: Returns this so you can chain .on() calls
- Only lifecycle events allowed: Other event names log error
- Custom events also supported (see CUSTOM COMPONENT EVENTS)
Example - Wait for component initialization:
// Initialize nested components after parent ready
@@ -881,6 +881,72 @@ LIFECYCLE EVENT CALLBACKS
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

View File

@@ -686,6 +686,12 @@ From within component methods:
- **$(selector).component()** → Get component instance from jQuery element
- **`await $(selector).component().ready()`** → Await component initialization. Rarely needed - `on_ready()` auto-waits for children created during render. Use for dynamically created components or Blade page JS interaction.
### Custom Component Events
Fire: `this.trigger('event_name', data)` | Listen: `this.sid('child').on('event_name', (component, data) => {})`
**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
To dynamically create/replace a component in JavaScript:

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

@@ -2211,9 +2211,9 @@
}
},
"node_modules/@jqhtml/core": {
"version": "2.3.16",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.16.tgz",
"integrity": "sha512-kAV24i6JqgmN3hJlRyv0dDgAyXmQUebp7WMwBTrtKJZJK+m59O0n4Uf5GeFEdfuxShMeWuz64Uh/BdrOCIB9uw==",
"version": "2.3.17",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.17.tgz",
"integrity": "sha512-v4M9kX1Z/NH5BNiMO9FrqdhYYT3zWehCYz7AVIZVfcOL0PHJskmoa2e+yUplgyexQ7ufQ31XzVefocdmSX3DHA==",
"license": "MIT",
"dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
@@ -2237,9 +2237,9 @@
}
},
"node_modules/@jqhtml/parser": {
"version": "2.3.16",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.16.tgz",
"integrity": "sha512-9qKR+hH+y6JsaSquq2AGZDOvS1nFH3U4GUodrdCEIpw959XivAi5BEBPr6uJ5dN4beYmonMge1SF4XZnaoiaEw==",
"version": "2.3.17",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.17.tgz",
"integrity": "sha512-Jr7vKjqmL/JIyGa6ojmnG3xcvWLaprLqG5BLPnZHXH8hC1AG3ReVuNZs1P3dT6GCp6dYjQ3ly30sl+PbZ32IQQ==",
"license": "MIT",
"dependencies": {
"@types/jest": "^29.5.11",
@@ -2277,9 +2277,9 @@
}
},
"node_modules/@jqhtml/vscode-extension": {
"version": "2.3.16",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.16.tgz",
"integrity": "sha512-QS+soQLhZquTQ5V5nCo3J13ztKW8cXacNaW05vJSvaYQnKFmSvmiZrGfnaF/7UToIYfs5+tpjDRGkt72ENTQ/A==",
"version": "2.3.17",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.17.tgz",
"integrity": "sha512-6pyOuM5G3hlJx45fRwrTZZfyE/t7IvWK3P/+lsqOIk2uoQcNn8SZjGMnSJyYEdCaPJmA5rHZRok5izGStiEUNg==",
"license": "MIT",
"engines": {
"vscode": "^1.74.0"

View File

@@ -276,13 +276,20 @@ export declare class Jqhtml_Component {
* Lifecycle event callbacks fire after the lifecycle method completes
* If a lifecycle event has already occurred, the callback fires immediately AND registers for future occurrences
* Custom events only fire when explicitly triggered via .trigger()
*
* Callback signature: (component, data?) => void
* - component: The component instance that triggered the event
* - data: Optional data passed as second parameter to trigger()
*/
on(event_name: string, callback: (component: Jqhtml_Component) => void): this;
on(event_name: string, callback: (component: Jqhtml_Component, data?: any) => void): this;
/**
* Trigger a lifecycle event - fires all registered callbacks
* Marks event as occurred so future .on() calls fire immediately
*
* @param event_name - Name of the event to trigger
* @param data - Optional data to pass to callbacks as second parameter
*/
trigger(event_name: string): void;
trigger(event_name: string, data?: any): void;
/**
* Check if any callbacks are registered for a given event
* Used to determine if cleanup logic needs to run

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

@@ -1955,7 +1955,7 @@ class Jqhtml_Component {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) is the leader`, { args: this.args });
}
// Capture state before on_load() for validation
// Capture args state before on_load() for validation
let argsBeforeLoad = null;
try {
argsBeforeLoad = JSON.stringify(this.args);
@@ -1963,7 +1963,6 @@ class Jqhtml_Component {
catch (error) {
// Args contain circular references - skip validation
}
const propertiesBeforeLoad = new Set(Object.keys(this));
// Set loading flag to prevent render() calls during on_load()
this.__loading = true;
// Create restricted proxy to prevent DOM access during on_load()
@@ -2052,7 +2051,6 @@ class Jqhtml_Component {
catch (error) {
// Args contain circular references - skip validation
}
const propertiesAfterLoad = Object.keys(this);
// Check if args were modified (skip if args are non-serializable)
if (argsBeforeLoad !== null && argsAfterLoad !== null && argsBeforeLoad !== argsAfterLoad) {
console.error(`[JQHTML] WARNING: Component "${this.component_name()}" modified this.args in on_load().\n` +
@@ -2062,15 +2060,12 @@ class Jqhtml_Component {
`Fix: Modify this.args in on_create() or other lifecycle methods, not in on_load().\n` +
`this.args stores state that on_load() depends on. Modifying it inside on_load() creates circular dependencies.`);
}
// Check if new properties were added to the component instance
const newProperties = propertiesAfterLoad.filter(prop => !propertiesBeforeLoad.has(prop) && prop !== 'data');
if (newProperties.length > 0) {
console.error(`[JQHTML] WARNING: Component "${this.component_name()}" added new properties in on_load().\n` +
`on_load() should ONLY modify this.data. New properties detected: ${newProperties.join(', ')}\n\n` +
`Fix: Store your data in this.data instead:\n` +
` ❌ this.${newProperties[0]} = value;\n` +
` ✅ this.data.${newProperties[0]} = value;`);
}
// NOTE: We previously checked for new properties being added to the component instance
// during on_load() and warned about them. However, this validation was removed because:
// 1. The restricted proxy already throws errors if code tries to set properties during on_load()
// 2. The validation was producing false positives for properties set during template render
// (templates can legitimately set this.* properties in code blocks like <% this._tabs = [...] %>)
// 3. Properties set via the restricted proxy throw immediately, making post-hoc validation redundant
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
// This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
// (restored from cache). Unregistered class instances become plain objects immediately,
@@ -2649,6 +2644,10 @@ class Jqhtml_Component {
* Lifecycle event callbacks fire after the lifecycle method completes
* If a lifecycle event has already occurred, the callback fires immediately AND registers for future occurrences
* Custom events only fire when explicitly triggered via .trigger()
*
* Callback signature: (component, data?) => void
* - component: The component instance that triggered the event
* - data: Optional data passed as second parameter to trigger()
*/
on(event_name, callback) {
// Initialize callback array for this event if needed
@@ -2659,9 +2658,10 @@ class Jqhtml_Component {
this._lifecycle_callbacks.get(event_name).push(callback);
// If this lifecycle event has already occurred, fire the callback immediately
// (only for lifecycle events - custom events don't have this behavior)
// Note: For already-occurred events, data is undefined since we don't store it
if (this._lifecycle_states.has(event_name)) {
try {
callback(this);
callback(this, undefined);
}
catch (error) {
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
@@ -2672,8 +2672,11 @@ class Jqhtml_Component {
/**
* Trigger a lifecycle event - fires all registered callbacks
* Marks event as occurred so future .on() calls fire immediately
*
* @param event_name - Name of the event to trigger
* @param data - Optional data to pass to callbacks as second parameter
*/
trigger(event_name) {
trigger(event_name, data) {
// Mark this event as occurred
this._lifecycle_states.add(event_name);
// Fire all registered callbacks for this event
@@ -2681,7 +2684,7 @@ class Jqhtml_Component {
if (callbacks) {
for (const callback of callbacks) {
try {
callback.bind(this)(this);
callback.bind(this)(this, data);
}
catch (error) {
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
@@ -4781,7 +4784,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.3.16';
const version = '2.3.17';
// Default export with all functionality
const jqhtml = {
// Core

File diff suppressed because one or more lines are too long

View File

@@ -1951,7 +1951,7 @@ class Jqhtml_Component {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) is the leader`, { args: this.args });
}
// Capture state before on_load() for validation
// Capture args state before on_load() for validation
let argsBeforeLoad = null;
try {
argsBeforeLoad = JSON.stringify(this.args);
@@ -1959,7 +1959,6 @@ class Jqhtml_Component {
catch (error) {
// Args contain circular references - skip validation
}
const propertiesBeforeLoad = new Set(Object.keys(this));
// Set loading flag to prevent render() calls during on_load()
this.__loading = true;
// Create restricted proxy to prevent DOM access during on_load()
@@ -2048,7 +2047,6 @@ class Jqhtml_Component {
catch (error) {
// Args contain circular references - skip validation
}
const propertiesAfterLoad = Object.keys(this);
// Check if args were modified (skip if args are non-serializable)
if (argsBeforeLoad !== null && argsAfterLoad !== null && argsBeforeLoad !== argsAfterLoad) {
console.error(`[JQHTML] WARNING: Component "${this.component_name()}" modified this.args in on_load().\n` +
@@ -2058,15 +2056,12 @@ class Jqhtml_Component {
`Fix: Modify this.args in on_create() or other lifecycle methods, not in on_load().\n` +
`this.args stores state that on_load() depends on. Modifying it inside on_load() creates circular dependencies.`);
}
// Check if new properties were added to the component instance
const newProperties = propertiesAfterLoad.filter(prop => !propertiesBeforeLoad.has(prop) && prop !== 'data');
if (newProperties.length > 0) {
console.error(`[JQHTML] WARNING: Component "${this.component_name()}" added new properties in on_load().\n` +
`on_load() should ONLY modify this.data. New properties detected: ${newProperties.join(', ')}\n\n` +
`Fix: Store your data in this.data instead:\n` +
` ❌ this.${newProperties[0]} = value;\n` +
` ✅ this.data.${newProperties[0]} = value;`);
}
// NOTE: We previously checked for new properties being added to the component instance
// during on_load() and warned about them. However, this validation was removed because:
// 1. The restricted proxy already throws errors if code tries to set properties during on_load()
// 2. The validation was producing false positives for properties set during template render
// (templates can legitimately set this.* properties in code blocks like <% this._tabs = [...] %>)
// 3. Properties set via the restricted proxy throw immediately, making post-hoc validation redundant
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
// This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
// (restored from cache). Unregistered class instances become plain objects immediately,
@@ -2645,6 +2640,10 @@ class Jqhtml_Component {
* Lifecycle event callbacks fire after the lifecycle method completes
* If a lifecycle event has already occurred, the callback fires immediately AND registers for future occurrences
* Custom events only fire when explicitly triggered via .trigger()
*
* Callback signature: (component, data?) => void
* - component: The component instance that triggered the event
* - data: Optional data passed as second parameter to trigger()
*/
on(event_name, callback) {
// Initialize callback array for this event if needed
@@ -2655,9 +2654,10 @@ class Jqhtml_Component {
this._lifecycle_callbacks.get(event_name).push(callback);
// If this lifecycle event has already occurred, fire the callback immediately
// (only for lifecycle events - custom events don't have this behavior)
// Note: For already-occurred events, data is undefined since we don't store it
if (this._lifecycle_states.has(event_name)) {
try {
callback(this);
callback(this, undefined);
}
catch (error) {
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
@@ -2668,8 +2668,11 @@ class Jqhtml_Component {
/**
* Trigger a lifecycle event - fires all registered callbacks
* Marks event as occurred so future .on() calls fire immediately
*
* @param event_name - Name of the event to trigger
* @param data - Optional data to pass to callbacks as second parameter
*/
trigger(event_name) {
trigger(event_name, data) {
// Mark this event as occurred
this._lifecycle_states.add(event_name);
// Fire all registered callbacks for this event
@@ -2677,7 +2680,7 @@ class Jqhtml_Component {
if (callbacks) {
for (const callback of callbacks) {
try {
callback.bind(this)(this);
callback.bind(this)(this, data);
}
catch (error) {
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
@@ -4777,7 +4780,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.3.16';
const version = '2.3.17';
// Default export with all functionality
const jqhtml = {
// Core

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
/**
* JQHTML Core v2.3.16
* JQHTML Core v2.3.17
* (c) 2025 JQHTML Team
* Released under the MIT License
*/
@@ -1956,7 +1956,7 @@ class Jqhtml_Component {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) is the leader`, { args: this.args });
}
// Capture state before on_load() for validation
// Capture args state before on_load() for validation
let argsBeforeLoad = null;
try {
argsBeforeLoad = JSON.stringify(this.args);
@@ -1964,7 +1964,6 @@ class Jqhtml_Component {
catch (error) {
// Args contain circular references - skip validation
}
const propertiesBeforeLoad = new Set(Object.keys(this));
// Set loading flag to prevent render() calls during on_load()
this.__loading = true;
// Create restricted proxy to prevent DOM access during on_load()
@@ -2053,7 +2052,6 @@ class Jqhtml_Component {
catch (error) {
// Args contain circular references - skip validation
}
const propertiesAfterLoad = Object.keys(this);
// Check if args were modified (skip if args are non-serializable)
if (argsBeforeLoad !== null && argsAfterLoad !== null && argsBeforeLoad !== argsAfterLoad) {
console.error(`[JQHTML] WARNING: Component "${this.component_name()}" modified this.args in on_load().\n` +
@@ -2063,15 +2061,12 @@ class Jqhtml_Component {
`Fix: Modify this.args in on_create() or other lifecycle methods, not in on_load().\n` +
`this.args stores state that on_load() depends on. Modifying it inside on_load() creates circular dependencies.`);
}
// Check if new properties were added to the component instance
const newProperties = propertiesAfterLoad.filter(prop => !propertiesBeforeLoad.has(prop) && prop !== 'data');
if (newProperties.length > 0) {
console.error(`[JQHTML] WARNING: Component "${this.component_name()}" added new properties in on_load().\n` +
`on_load() should ONLY modify this.data. New properties detected: ${newProperties.join(', ')}\n\n` +
`Fix: Store your data in this.data instead:\n` +
` ❌ this.${newProperties[0]} = value;\n` +
` ✅ this.data.${newProperties[0]} = value;`);
}
// NOTE: We previously checked for new properties being added to the component instance
// during on_load() and warned about them. However, this validation was removed because:
// 1. The restricted proxy already throws errors if code tries to set properties during on_load()
// 2. The validation was producing false positives for properties set during template render
// (templates can legitimately set this.* properties in code blocks like <% this._tabs = [...] %>)
// 3. Properties set via the restricted proxy throw immediately, making post-hoc validation redundant
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
// This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
// (restored from cache). Unregistered class instances become plain objects immediately,
@@ -2650,6 +2645,10 @@ class Jqhtml_Component {
* Lifecycle event callbacks fire after the lifecycle method completes
* If a lifecycle event has already occurred, the callback fires immediately AND registers for future occurrences
* Custom events only fire when explicitly triggered via .trigger()
*
* Callback signature: (component, data?) => void
* - component: The component instance that triggered the event
* - data: Optional data passed as second parameter to trigger()
*/
on(event_name, callback) {
// Initialize callback array for this event if needed
@@ -2660,9 +2659,10 @@ class Jqhtml_Component {
this._lifecycle_callbacks.get(event_name).push(callback);
// If this lifecycle event has already occurred, fire the callback immediately
// (only for lifecycle events - custom events don't have this behavior)
// Note: For already-occurred events, data is undefined since we don't store it
if (this._lifecycle_states.has(event_name)) {
try {
callback(this);
callback(this, undefined);
}
catch (error) {
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
@@ -2673,8 +2673,11 @@ class Jqhtml_Component {
/**
* Trigger a lifecycle event - fires all registered callbacks
* Marks event as occurred so future .on() calls fire immediately
*
* @param event_name - Name of the event to trigger
* @param data - Optional data to pass to callbacks as second parameter
*/
trigger(event_name) {
trigger(event_name, data) {
// Mark this event as occurred
this._lifecycle_states.add(event_name);
// Fire all registered callbacks for this event
@@ -2682,7 +2685,7 @@ class Jqhtml_Component {
if (callbacks) {
for (const callback of callbacks) {
try {
callback.bind(this)(this);
callback.bind(this)(this, data);
}
catch (error) {
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
@@ -4782,7 +4785,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.3.16';
const version = '2.3.17';
// Default export with all functionality
const jqhtml = {
// Core

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
2.3.16
2.3.17

View File

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

18
package-lock.json generated
View File

@@ -2658,9 +2658,9 @@
}
},
"node_modules/@jqhtml/core": {
"version": "2.3.16",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.16.tgz",
"integrity": "sha512-kAV24i6JqgmN3hJlRyv0dDgAyXmQUebp7WMwBTrtKJZJK+m59O0n4Uf5GeFEdfuxShMeWuz64Uh/BdrOCIB9uw==",
"version": "2.3.17",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.17.tgz",
"integrity": "sha512-v4M9kX1Z/NH5BNiMO9FrqdhYYT3zWehCYz7AVIZVfcOL0PHJskmoa2e+yUplgyexQ7ufQ31XzVefocdmSX3DHA==",
"license": "MIT",
"dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
@@ -2684,9 +2684,9 @@
}
},
"node_modules/@jqhtml/parser": {
"version": "2.3.16",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.16.tgz",
"integrity": "sha512-9qKR+hH+y6JsaSquq2AGZDOvS1nFH3U4GUodrdCEIpw959XivAi5BEBPr6uJ5dN4beYmonMge1SF4XZnaoiaEw==",
"version": "2.3.17",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.17.tgz",
"integrity": "sha512-Jr7vKjqmL/JIyGa6ojmnG3xcvWLaprLqG5BLPnZHXH8hC1AG3ReVuNZs1P3dT6GCp6dYjQ3ly30sl+PbZ32IQQ==",
"license": "MIT",
"dependencies": {
"@types/jest": "^29.5.11",
@@ -2724,9 +2724,9 @@
}
},
"node_modules/@jqhtml/vscode-extension": {
"version": "2.3.16",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.16.tgz",
"integrity": "sha512-QS+soQLhZquTQ5V5nCo3J13ztKW8cXacNaW05vJSvaYQnKFmSvmiZrGfnaF/7UToIYfs5+tpjDRGkt72ENTQ/A==",
"version": "2.3.17",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.17.tgz",
"integrity": "sha512-6pyOuM5G3hlJx45fRwrTZZfyE/t7IvWK3P/+lsqOIk2uoQcNn8SZjGMnSJyYEdCaPJmA5rHZRok5izGStiEUNg==",
"license": "MIT",
"engines": {
"vscode": "^1.74.0"