/** * JQHTML Integration - Component Hydration System * * 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: * Rendered HTML:
*
* * 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. * * == 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 * - 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 * - Triggers 'jqhtml_ready' when all components are initialized * * == THE TRANSFORMATION == * * Before hydration: *
* * After hydration: *
← Component_Init removed, component class added * [component template content] ← Template rendered into element *
* * The element is now a live component with event handlers, state, and lifecycle. * * == KEY PARTICIPANTS == * * JqhtmlBladeCompiler.php - Transforms 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 */ class Jqhtml_Integration { /** * Phase 1: Register Component Classes * * Compiled .jqhtml templates self-register their render methods with jqhtml. * But the framework must separately register ES6 component classes (the ones * extending Component with lifecycle methods like on_create, on_load, etc). * * This runs during framework_modules_define, before any DOM processing. */ static _on_framework_modules_define() { // ───────────────────────────────────────────────────────────────────── // Register Component Classes with jqhtml Runtime // // The Manifest knows all classes extending Component. We register each // with jqhtml so it can instantiate them by name during hydration. // ───────────────────────────────────────────────────────────────────── let jqhtml_components = Manifest.get_extending('Component'); console_debug('JQHTML_INIT', 'Registering ' + jqhtml_components.length + ' Components'); for (let component of jqhtml_components) { jqhtml.register_component(component.class_name, component.class_object); } // ───────────────────────────────────────────────────────────────────── // Tag Static Methods with Cache IDs // // jqhtml caches component output based on args. When a function is passed // as an arg (e.g., DataGrid's data_source), we need a stable string key. // // Without this: data_source: function() {...} → no cache (functions aren't serializable) // With this: function._jqhtml_cache_id = "My_DataGrid.fetch_data" → cacheable // ───────────────────────────────────────────────────────────────────── const all_classes = Manifest.get_all_classes(); let methods_tagged = 0; for (const class_info of all_classes) { const class_object = class_info.class_object; const class_name = class_info.class_name; const property_names = Object.getOwnPropertyNames(class_object); for (const property_name of property_names) { if (property_name === 'length' || property_name === 'name' || property_name === 'prototype') { continue; } const property_value = class_object[property_name]; if (typeof property_value === 'function') { property_value._jqhtml_cache_id = `${class_name}.${property_name}`; methods_tagged++; } } } console_debug('JQHTML_INIT', `Tagged ${methods_tagged} static methods with _jqhtml_cache_id`); // ───────────────────────────────────────────────────────────────────── // Configure jqhtml Caching // // scope_key() changes when: app code changes, user logs out, site changes. // This automatically invalidates cached component renders. // ───────────────────────────────────────────────────────────────────── jqhtml.set_cache_key(Rsx.scope_key()); window.jqhtml.debug.verbose = false; } /** * Phase 2: DOM Hydration * * 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: * * ← Parent hydrates first * ← Child hydrates after parent renders * * * 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|undefined} Promises for recursive calls; undefined for top-level */ 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 ... 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); } } }); // ───────────────────────────────────────────────────────────────────── // 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; } // ═══════════════════════════════════════════════════════════════════════ // Utility Methods (pass-through to jqhtml runtime) // ═══════════════════════════════════════════════════════════════════════ /** Get all registered component names. Useful for debugging/introspection. */ static get_component_names() { return jqhtml.get_component_names(); } /** Check if a component is registered by name. */ static has_component(name) { return jqhtml.has_component(name); } } // Class is automatically made global by RSX manifest - no window assignment needed