Refactor jqhtml integration to use jqhtml.boot() and migrate blade highlighting to jqhtml extension

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-25 03:33:09 +00:00
parent bd5809fdbd
commit 9be3dfc14e
58 changed files with 817 additions and 672 deletions

View File

@@ -1,55 +1,26 @@
/**
* JQHTML Integration - Component Hydration System
* JQHTML Integration - Component Registration and Hydration Bootstrap
*
* This module bridges server-rendered HTML and client-side jqhtml components.
*
* == THE HYDRATION PROBLEM ==
*
* When PHP/Blade renders a page, jqhtml components appear as placeholder elements:
*
* Blade source: <User_Card $name="John" />
* Rendered HTML: <div class="Component_Init"
* data-component-init-name="User_Card"
* data-component-args='{"name":"John"}'>
* </div>
*
* These are just div tags - they have no behavior until JavaScript runs.
* "Hydration" is the process of finding these placeholders and converting them
* into live, interactive jqhtml components.
* This module bridges RSpade's manifest system with jqhtml's component runtime.
*
* == TWO-PHASE INITIALIZATION ==
*
* Phase 1: _on_framework_modules_define() - Component Registration
* - Runs early in framework boot, before DOM is processed
* - Registers all ES6 classes extending Component with jqhtml runtime
* - Tags static methods with cache IDs for jqhtml's caching system
* - After this phase, jqhtml knows: "User_Card" → UserCardClass
*
* Phase 2: _on_framework_modules_init() - DOM Hydration
* - Runs after templates are loaded
* - Finds all .Component_Init elements in the DOM
* - Extracts component name and args from data attributes
* - Calls $element.component(name, args) to hydrate each one
* - Recursively processes nested components
* - Calls jqhtml.boot() to hydrate all ._Component_Init placeholders
* - Triggers 'jqhtml_ready' when all components are initialized
*
* == THE TRANSFORMATION ==
*
* Before hydration:
* <div class="Component_Init" data-component-init-name="User_Card" ...>
*
* After hydration:
* <div class="User_Card"> ← Component_Init removed, component class added
* [component template content] ← Template rendered into element
* </div>
*
* The element is now a live component with event handlers, state, and lifecycle.
*
* == KEY PARTICIPANTS ==
*
* JqhtmlBladeCompiler.php - Transforms <Component /> tags into .Component_Init divs
* JqhtmlBladeCompiler.php - Transforms <Component /> tags into ._Component_Init divs
* jqhtml runtime - Maintains registry of component names → classes
* This module - Orchestrates registration and hydration
* $().component() - jQuery plugin that creates component instances
* jqhtml.boot() - Finds and hydrates all ._Component_Init placeholders
* This module - Orchestrates registration and triggers hydration
*/
class Jqhtml_Integration {
/**
@@ -120,165 +91,20 @@ class Jqhtml_Integration {
/**
* Phase 2: DOM Hydration
*
* Finds all .Component_Init placeholders and converts them into live components.
* Delegates to jqhtml.boot() which finds all ._Component_Init placeholders
* and converts them into live components.
*
* == HYDRATION PROCESS ==
*
* For each .Component_Init element:
*
* 1. EXTRACT: Read data-component-init-name and data-component-args
* 2. CLEANUP: Remove data attributes (prevents re-hydration, hides implementation)
* 3. CAPTURE: Save innerHTML for slot/content() processing
* 4. INSTANTIATE: Call $element.component(name, args)
* 5. RECURSE: After render, hydrate any nested .Component_Init elements
*
* == NESTED COMPONENT HANDLING ==
*
* Components can contain other components in their Blade output:
*
* <User_Card> ← Parent hydrates first
* <Avatar /> ← Child hydrates after parent renders
* </User_Card>
*
* We skip nested .Component_Init elements on first pass (they're inside a parent
* that hasn't rendered yet). After each component renders, we recursively scan
* its content for children to hydrate.
*
* == ASYNC COMPLETION ==
*
* Hydration is async because components may have on_load() methods that fetch data.
* We track all component promises and trigger 'jqhtml_ready' only when every
* component (including nested ones) has completed its full lifecycle.
*
* @param {jQuery} [$scope] Scope to search (defaults to body). Recursive calls pass component.$
* @returns {Array<Promise>|undefined} Promises for recursive calls; undefined for top-level
* jqhtml.boot() handles:
* - Finding ._Component_Init elements
* - Parsing data-component-init-name / data-component-args
* - Calling $element.component(name, args)
* - Recursive nested component handling
* - Promise tracking for async components
*/
static _on_framework_modules_init($scope) {
const is_top_level = !$scope;
const promises = [];
const components_needing_init = ($scope || $('body')).find('.Component_Init');
if (components_needing_init.length > 0) {
console_debug('JQHTML_INIT', `Initializing ${components_needing_init.length} DOM components`);
}
components_needing_init.each(function () {
const $element = $(this);
// Guard: Element may have been removed by a parent component's render
if (!document.contains($element[0])) {
return;
}
// Guard: Skip nested components - they'll be processed after their parent renders
let parent = $element[0].parentElement;
while (parent) {
if (parent.classList.contains('Component_Init')) {
return;
}
parent = parent.parentElement;
}
// ─────────────────────────────────────────────────────────────────
// STEP 1: Extract hydration data from placeholder element
// ─────────────────────────────────────────────────────────────────
const component_name = $element.attr('data-component-init-name');
const args_string = $element.attr('data-component-args');
let component_args = {};
if (args_string) {
try {
component_args = JSON.parse(args_string);
} catch (e) {
console.error(`[JQHTML Integration] Failed to parse component args for ${component_name}:`, e);
component_args = {};
}
}
// ─────────────────────────────────────────────────────────────────
// STEP 2: Remove hydration markers (cleanup)
//
// These attributes served their purpose. Removing them:
// - Prevents accidental re-hydration
// - Hides implementation details from DOM inspection
// - Prevents other code from depending on this internal mechanism
// ─────────────────────────────────────────────────────────────────
$element.removeAttr('data-component-init-name');
$element.removeAttr('data-component-args');
$element.removeData('component-init-name');
$element.removeData('component-args');
if (component_name) {
let component_args_filtered = {};
for (const [key, value] of Object.entries(component_args)) {
if (key.startsWith('data-')) {
component_args_filtered[key.substring(5)] = value;
} else {
component_args_filtered[key] = value;
}
}
try {
// ─────────────────────────────────────────────────────────
// STEP 3: Capture innerHTML for slot/content() processing
//
// Blade content between <Component>...</Component> tags
// becomes available to the template via content() function
// ─────────────────────────────────────────────────────────
component_args_filtered._inner_html = $element.html();
$element.empty();
// ─────────────────────────────────────────────────────────
// STEP 4: Instantiate the component
//
// Remove .Component_Init first to prevent re-initialization
// if this element is somehow scanned again
// ─────────────────────────────────────────────────────────
$element.removeClass('Component_Init');
const component_promise = new Promise((resolve) => {
// $().component(name, args) creates the component instance,
// binds it to the element, and starts the lifecycle
let component = $element.component(component_name, component_args_filtered).component();
// ─────────────────────────────────────────────────────
// STEP 5: Recurse after render
//
// Component's template may contain nested .Component_Init
// elements. We must wait for render before we can find them.
//
// Note: If the template changed the tag (e.g., tag="button"),
// the original $element may have been replaced. Always use
// component.$ to get the current element.
// ─────────────────────────────────────────────────────
component.on('render', function () {
const nested_promises = Jqhtml_Integration._on_framework_modules_init(component.$);
promises.push(...nested_promises);
resolve();
}).$;
});
promises.push(component_promise);
} catch (error) {
console.error(`[JQHTML Integration] Failed to initialize component ${component_name}:`, error);
console.error('Error details:', error.stack || error);
}
}
static _on_framework_modules_init() {
jqhtml.boot().then(() => {
Rsx.trigger('jqhtml_ready');
});
// ─────────────────────────────────────────────────────────────────────
// COMPLETION: Top-level call waits for all components, then signals ready
// ─────────────────────────────────────────────────────────────────────
if (is_top_level) {
(async () => {
await Promise.all(promises);
Rsx.trigger('jqhtml_ready');
})();
return;
}
// Recursive calls return promises for parent to collect
return promises;
}
// ═══════════════════════════════════════════════════════════════════════