/** * JQHTML Integration - Automatic component registration and binding * * This module automatically: * 1. Registers component classes that extend Component * 2. Binds templates to component classes when names match * 3. Enables $(selector).component("Component_Name") syntax */ class Jqhtml_Integration { /** * Compiled Jqhtml templates self-register. The developer (the framework in this case) is still * responsible for registering es6 component classes with jqhtml. This does so at an early stage * of framework init. */ static _on_framework_modules_define() { 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); } // Assign unique cache IDs to all static methods for component caching // This enables JQHTML to generate deterministic cache keys when functions // are passed as component arguments (e.g., DataGrid data_source functions) 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; // Get all property names from the class object (static methods/properties) const property_names = Object.getOwnPropertyNames(class_object); for (const property_name of property_names) { // Skip built-in properties and non-functions if (property_name === 'length' || property_name === 'name' || property_name === 'prototype') { continue; } const property_value = class_object[property_name]; // Only tag functions (static methods) if (typeof property_value === 'function') { // Assign unique cache ID: "ClassName.methodName" property_value._jqhtml_cache_id = `${class_name}.${property_name}`; methods_tagged++; } } } console_debug('JQHTML_INIT', `Tagged ${methods_tagged} static methods with _jqhtml_cache_id`); // Set the cache key for jqhtml to the application key to enable component caching in local storage. // Modifying the application or the user logging out modifies the scope_key and will invalidate jqhtml cache. jqhtml.set_cache_key(Rsx.scope_key()); // set this to true if we desire jqhtml verbose output (probably not necessary for end developers) window.jqhtml.debug.verbose = false; } /** * Framework modules init phase - Bind components and initialize DOM * This runs after templates are registered to bind component classes * @param {jQuery} [$scope] Optional scope to search within (defaults to body) * @returns {Array|undefined} Array of 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); // Skip if element is no longer attached to the document // (may have been removed by a parent component's .empty() call) if (!document.contains($element[0])) { return; } // Check if any parent has Component_Init class - skip nested components let parent = $element[0].parentElement; while (parent) { if (parent.classList.contains('Component_Init')) { return; // Skip this element, it's nested } parent = parent.parentElement; } const component_name = $element.attr('data-component-init-name'); // jQuery's .data() doesn't auto-parse JSON - we need to parse it manually let component_args = {}; const args_string = $element.attr('data-component-args'); // Unset component- php side initialization args, it is no longer needed as a compionent attribute // Unsetting also prevents undesired access to this code in other parts of the program, prevening an // unwanted future dependency on this paradigm $element.removeAttr('data-component-init-name'); $element.removeAttr('data-component-args'); $element.removeData('component-init-name'); $element.removeData('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 = {}; } } if (component_name) { // Transform $ prefixed keys to data- attributes let component_args_filtered = {}; for (const [key, value] of Object.entries(component_args)) { // if (key.startsWith('$')) { // component_args_filtered[key.substring(1)] = value; // } else if (key.startsWith('data-')) { component_args_filtered[key.substring(5)] = value; } else { component_args_filtered[key] = value; } } try { // Store inner HTML as string for nested component processing component_args_filtered._inner_html = $element.html(); $element.empty(); // Remove the init class before instantiation to prevent re-initialization $element.removeClass('Component_Init'); // Create promise for this component's initialization const component_promise = new Promise((resolve) => { // Use jQuery component plugin to create the component // Plugin handles element internally, just pass args // Get the updated $element from let component = $element.component(component_name, component_args_filtered).component(); component.on('render', function () { // Recursively collect promises from nested components // Getting the updated component here - if the tag name was not div, the element would have been recreated, so we need to get the element set on the component, not from our earlier selector const nested_promises = Jqhtml_Integration._on_framework_modules_init(component.$); promises.push(...nested_promises); // Resolve this component's promise 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); } } }); // Top-level call: spawn async handler to wait for all promises, then trigger event if (is_top_level) { (async () => { await Promise.all(promises); await Rsx._rsx_call_all_classes('on_jqhtml_ready'); Rsx.trigger('jqhtml_ready'); })(); return; } // Recursive call: return promises for parent to collect return promises; } /** * Get all registered component names * @returns {Array} Array of component names */ static get_component_names() { return jqhtml.get_component_names(); } /** * Check if a component is registered * @param {string} name Component name * @returns {boolean} True if component is registered */ static has_component(name) { return jqhtml.has_component(name); } } // RSX manifest automatically makes classes global - no manual assignment needed