UNPKG

@memberjunction/ng-react

Version:

Angular components for hosting React components in MemberJunction applications

956 lines 47.9 kB
/** * @fileoverview Angular component that hosts React components with proper memory management. * Provides a bridge between Angular and React ecosystems in MemberJunction applications. * @module @memberjunction/ng-react */ import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Subject } from 'rxjs'; import { ComponentSpec } from '@memberjunction/interactive-component-types'; import { ReactBridgeService } from '../services/react-bridge.service'; import { AngularAdapterService } from '../services/angular-adapter.service'; import { createErrorBoundary, ComponentHierarchyRegistrar, resourceManager, reactRootManager, SetupStyles, ComponentRegistryService } from '@memberjunction/react-runtime'; import { createRuntimeUtilities } from '../utilities/runtime-utilities'; import { LogError, CompositeKey, Metadata, RunView } from '@memberjunction/core'; import { MJNotificationService } from '@memberjunction/ng-notifications'; import { ComponentMetadataEngine } from '@memberjunction/core-entities'; import * as i0 from "@angular/core"; import * as i1 from "../services/react-bridge.service"; import * as i2 from "../services/angular-adapter.service"; import * as i3 from "@memberjunction/ng-notifications"; const _c0 = ["container"]; function MJReactComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelementStart(0, "div", 3)(1, "div", 4); i0.ɵɵelement(2, "i", 5); i0.ɵɵelementEnd(); i0.ɵɵelementStart(3, "div", 6); i0.ɵɵtext(4, "Loading component..."); i0.ɵɵelementEnd()(); } } /** * Angular component that hosts React components with proper memory management. * This component provides a bridge between Angular and React, allowing React components * to be used seamlessly within Angular applications. */ export class MJReactComponent { /** * The component specification to render. * When this changes after initialization, the component will be reinitialized * to load and render the new specification. */ set component(value) { const previousComponent = this._component; this._component = value; // If already initialized and component spec changed, reinitialize if (this.isInitialized && value && previousComponent !== value) { // Check if it's actually a different component (not just same reference) const isDifferent = !previousComponent || previousComponent.name !== value.name || previousComponent.code !== value.code || previousComponent.version !== value.version; if (isDifferent) { this.reinitializeComponent(); } } } get component() { return this._component; } set utilities(value) { this._utilities = value; } get utilities() { // Lazy initialization - only create default utilities when needed if (!this._utilities) { const runtimeUtils = createRuntimeUtilities(); this._utilities = runtimeUtils.buildUtilities(this.enableLogging); if (this.enableLogging) { console.log('MJReactComponent: Auto-initialized utilities using createRuntimeUtilities()'); } } return this._utilities; } set styles(value) { this._styles = value; } get styles() { // Lazy initialization - only create default styles when needed if (!this._styles) { this._styles = SetupStyles(); if (this.enableLogging) { console.log('MJReactComponent: Auto-initialized styles using SetupStyles()'); } } return this._styles; } set savedUserSettings(value) { this._savedUserSettings = value || {}; // Re-render if component is initialized if (this.isInitialized) { this.renderComponent(); } } get savedUserSettings() { return this._savedUserSettings; } constructor(reactBridge, adapter, cdr, notificationService) { this.reactBridge = reactBridge; this.adapter = adapter; this.cdr = cdr; this.notificationService = notificationService; /** * Controls verbose logging for component lifecycle and operations. * Note: This does NOT control which React build (dev/prod) is loaded. * To control React builds, use ReactDebugConfig.setDebugMode() at app startup. */ this.enableLogging = false; this.useComponentManager = true; // NEW: Use unified ComponentManager by default this._savedUserSettings = {}; this.stateChange = new EventEmitter(); this.componentEvent = new EventEmitter(); this.refreshData = new EventEmitter(); this.openEntityRecord = new EventEmitter(); this.userSettingsChanged = new EventEmitter(); this.reactRootId = null; this.compiledComponent = null; this.loadedDependencies = {}; this.destroyed$ = new Subject(); this.currentCallbacks = null; this.isInitialized = false; this.isRendering = false; this.pendingRender = false; this.isDestroying = false; this.componentVersion = ''; // Store the version for resolver this.hasError = false; /** * Public property containing the fully resolved component specification. * This includes all external code fetched from registries, allowing consumers * to inspect the complete resolved specification including dependencies. * Only populated after successful component initialization. */ this.resolvedComponentSpec = null; // Generate unique component ID for resource tracking this.componentId = `mj-react-component-${Date.now()}-${Math.random()}`; } async ngAfterViewInit() { // Try to get registry size safely let registrySize = 'N/A'; try { if (this.adapter.isInitialized()) { registrySize = this.adapter.getRegistry().size().toString(); } else { registrySize = 'Not initialized yet'; } } catch (e) { registrySize = 'Not available'; } console.log(`🎬 [ngAfterViewInit] Starting component initialization:`, { componentId: this.componentId, componentName: this.component?.name, timestamp: new Date().toISOString(), registrySize: registrySize }); // Trigger change detection to show loading state this.cdr.detectChanges(); await this.initializeComponent(); } ngOnDestroy() { // Set destroying flag immediately this.isDestroying = true; // Cancel any pending renders this.pendingRender = false; this.destroyed$.next(); this.destroyed$.complete(); this.cleanup(); } /** * Reinitialize the component when the input spec changes. * Cleans up the current component and initializes with the new spec. */ async reinitializeComponent() { // Don't reinitialize if we're being destroyed if (this.isDestroying) { return; } // Clear cached state from previous component this.compiledComponent = null; this.resolvedComponentSpec = null; this.loadedDependencies = {}; this.componentVersion = ''; this.hasError = false; this.isInitialized = false; // Unmount existing React root if present if (this.reactRootId) { this.isRendering = false; this.pendingRender = false; reactRootManager.unmountRoot(this.reactRootId); this.reactRootId = null; } // Trigger change detection to show loading state this.cdr.detectChanges(); // Initialize with the new component spec await this.initializeComponent(); } /** * Initialize the React component */ async initializeComponent() { try { // Ensure React is loaded await this.reactBridge.getReactContext(); // Wait for React to be fully ready (handles first-load delay) await this.reactBridge.waitForReactReady(); // NEW: Use ComponentManager if enabled (default: true) if (this.useComponentManager) { console.log(`🎯 [initializeComponent] Using NEW ComponentManager approach`); await this.loadComponentWithManager(); // Component is already compiled and stored in this.compiledComponent // No need to fetch from registry - it's already set } else { console.log(`📦 [initializeComponent] Using legacy approach (will be deprecated)`); // Register component hierarchy (this compiles and registers all components including from registries) await this.registerComponentHierarchy(); // The resolved spec should now be available from the registration result // No need to fetch again // Get the already-registered component from the registry const registry = this.adapter.getRegistry(); console.log(`🔍 [initializeComponent] Looking for component in registry:`, { name: this.component.name, namespace: this.component.namespace || 'Global', version: this.componentVersion }); // Let's also check what's actually in the registry // Note: ComponentRegistry doesn't have a list() method, so we'll skip this for now const componentWrapper = registry.get(this.component.name, this.component.namespace || 'Global', this.componentVersion); console.log(`🔍 [initializeComponent] Registry.get result:`, { found: !!componentWrapper, type: componentWrapper ? typeof componentWrapper : 'undefined', hasComponent: componentWrapper ? !!componentWrapper.component : false }); if (!componentWrapper) { const source = this.component.registry ? `external registry ${this.component.registry}` : 'local registry'; console.error(`❌ [initializeComponent] Component not found! Details:`, { searchedName: this.component.name, searchedNamespace: this.component.namespace || 'Global', searchedVersion: this.componentVersion, source: source }); throw new Error(`Component ${this.component.name} was not found in registry after registration from ${source}`); } // The registry now stores ComponentObjects directly // Validate it has the expected structure if (!componentWrapper || typeof componentWrapper !== 'object') { throw new Error(`Invalid component wrapper returned for ${this.component.name}: ${typeof componentWrapper}`); } if (!componentWrapper.component) { throw new Error(`Component wrapper missing 'component' property for ${this.component.name}`); } // Now that we use a regular HOC wrapper, components should always be functions if (typeof componentWrapper.component !== 'function') { throw new Error(`Component is not a function for ${this.component.name}: ${typeof componentWrapper.component}`); } this.compiledComponent = componentWrapper; } // End of else block for legacy approach // Create managed React root const reactContext = this.reactBridge.getCurrentContext(); if (!reactContext) { throw new Error('React context not available'); } this.reactRootId = reactRootManager.createRoot(this.container.nativeElement, (container) => reactContext.ReactDOM.createRoot(container), this.componentId); // Initial render this.renderComponent(); this.isInitialized = true; // Trigger change detection since we're using OnPush this.cdr.detectChanges(); } catch (error) { this.hasError = true; LogError(`Failed to initialize React component: ${error}`); this.componentEvent.emit({ type: 'error', payload: { error: error instanceof Error ? error.message : String(error), source: 'initialization' } }); // Trigger change detection to show error state this.cdr.detectChanges(); } } /** * Generate a hash from component code for versioning * Uses a simple hash function that's fast and sufficient for version differentiation */ generateComponentHash(spec) { // Collect all code from the component hierarchy const codeStrings = []; const collectCode = (s) => { if (s.code) { codeStrings.push(s.code); } if (s.dependencies) { for (const dep of s.dependencies) { collectCode(dep); } } }; collectCode(spec); // Generate hash from concatenated code const fullCode = codeStrings.join('|'); let hash = 0; for (let i = 0; i < fullCode.length; i++) { const char = fullCode.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } // Convert to hex string and take first 8 characters for readability const hexHash = Math.abs(hash).toString(16).padStart(8, '0').substring(0, 8); return `v${hexHash}`; } /** * Resolve components using the runtime's resolver */ async resolveComponentsWithVersion(spec, version, namespace = 'Global') { const resolver = this.adapter.getResolver(); // Debug: Log what dependencies we're trying to resolve if (this.enableLogging) { console.log(`Resolving components for ${spec.name}. Dependencies:`, spec.dependencies); } // Use the runtime's resolver which now handles registry-based components const resolved = await resolver.resolveComponents(spec, namespace, Metadata.Provider.CurrentUser // Pass current user context for database operations ); if (this.enableLogging) { console.log(`Resolved ${Object.keys(resolved).length} components for version ${version}:`, Object.keys(resolved)); } return resolved; } /** * NEW: Load component using unified ComponentManager - MUCH SIMPLER! */ async loadComponentWithManager() { try { const manager = this.adapter.getComponentManager(); console.log(`🚀 [ComponentManager] Loading component hierarchy: ${this.component.name}`); // Load the entire hierarchy with one simple call const result = await manager.loadHierarchy(this.component, { contextUser: Metadata.Provider.CurrentUser, defaultNamespace: 'Global', defaultVersion: this.component.version || this.generateComponentHash(this.component), returnType: 'both' }); if (!result.success) { const errorMessages = result.errors.map(e => `${e.componentName}: ${e.message}`).join(', '); console.error(`❌ [ComponentManager] Failed to load hierarchy:`, errorMessages); throw new Error(`Component loading failed: ${errorMessages}`); } // Store the results (handle undefined values) this.resolvedComponentSpec = this.enrichSpecWithRegistryInfo(result.resolvedSpec || null); this.compiledComponent = result.rootComponent || null; this.componentVersion = result.resolvedSpec?.version || this.component.version || 'latest'; // IMPORTANT: Store the loaded dependencies for use in renderComponent this.loadedDependencies = result.components || {}; console.log(`✅ [ComponentManager] Successfully loaded hierarchy:`, { rootComponent: result.resolvedSpec?.name, loadedCount: result.loadedComponents.length, dependencies: Object.keys(this.loadedDependencies), stats: result.stats }); // Component is ready to render return true; } catch (error) { console.error(`❌ [ComponentManager] Error loading component:`, error); throw error; } } /** * Register all components in the hierarchy * @deprecated Use loadComponentWithManager() instead */ async registerComponentHierarchy() { // Use semantic version from spec or generate hash-based version for uniqueness const version = this.component.version || this.generateComponentHash(this.component); this.componentVersion = version; // Store for use in resolver console.log(`🔍 [registerComponentHierarchy] Starting registration for ${this.component.name}@${version}`, { location: this.component.location, registry: this.component.registry, namespace: this.component.namespace, hasCode: !!this.component.code, codeLength: this.component.code?.length || 0 }); // Check if already registered to avoid duplication const registry = this.adapter.getRegistry(); const checkNamespace = this.component.namespace || 'Global'; console.log(`🔍 [registerComponentHierarchy] Checking registry for existing component:`, { name: this.component.name, namespace: checkNamespace, version: version, registrySize: registry.size(), registryId: registry.registryId || 'unknown' }); // Log registry state for debugging console.log(`📦 [registerComponentHierarchy] Registry state:`, { totalSize: registry.size(), registryInstance: registry.registryId || 'unknown' }); const existingComponent = registry.get(this.component.name, checkNamespace, version); if (existingComponent) { console.log(`⚠️ [registerComponentHierarchy] Component ${this.component.name}@${version} already registered!`, { existingType: typeof existingComponent, hasComponent: !!existingComponent.component, registrationTime: existingComponent.registeredAt || 'unknown', runtimeContextLibraries: Object.keys(this.adapter.getRuntimeContext().libraries || {}) }); // For registry components, we need to check the resolved spec's libraries, not the input spec // The input spec from Angular doesn't have library information for registry components if (this.component.location === 'registry' && this.component.registry) { console.log(`📋 [registerComponentHierarchy] Component is from registry, need to fetch full spec to check libraries`); // Continue to fetch the full spec below - don't return early } else { // For local components, check using the input spec const requiredLibraries = this.component.libraries || []; const runtimeLibraries = this.adapter.getRuntimeContext().libraries || {}; const missingLibraries = requiredLibraries.filter(lib => !runtimeLibraries[lib.globalVariable]); if (missingLibraries.length > 0) { console.warn(`⚠️ [registerComponentHierarchy] Component registered but libraries missing:`, { required: requiredLibraries.map(l => l.globalVariable), loaded: Object.keys(runtimeLibraries), missing: missingLibraries.map(l => l.globalVariable) }); // Don't return early - continue to load libraries } else { console.log(`✅ [registerComponentHierarchy] Component ${this.component.name}@${version} already registered with all libraries, skipping`); return; } } } else { console.log(`🆕 [registerComponentHierarchy] Component not found in registry, proceeding with registration`); } // Initialize metadata engine await ComponentMetadataEngine.Instance.Config(false, Metadata.Provider.CurrentUser); // Use the runtime's hierarchy registrar const registrar = new ComponentHierarchyRegistrar(this.adapter.getCompiler(), this.adapter.getRegistry(), this.adapter.getRuntimeContext()); console.log(`📦 [registerComponentHierarchy] Calling registrar.registerHierarchy for ${this.component.name}`, { hasStyles: !!this.styles, namespace: this.component.namespace || 'Global', version: version, libraryCount: ComponentMetadataEngine.Instance.ComponentLibraries?.length || 0, hasCode: !!this.component.code, codeLength: this.component.code?.length || 0 }); // Register with proper configuration // Pass the partial spec - the React runtime will handle fetching from registries const result = await registrar.registerHierarchy(this.component, // Pass the original spec, not fetched { styles: this.styles, namespace: this.component.namespace || 'Global', version: version, allowOverride: false, // Each version is unique allLibraries: ComponentMetadataEngine.Instance.ComponentLibraries, debug: true, contextUser: Metadata.Provider.CurrentUser }); if (!result.success) { const errors = result.errors.map(e => e.error).join(', '); console.error(`❌ [registerComponentHierarchy] Registration failed:`, errors); throw new Error(`Component registration failed: ${errors}`); } // Store the resolved spec from the registration result if (result.resolvedSpec) { this.resolvedComponentSpec = this.enrichSpecWithRegistryInfo(result.resolvedSpec || null); console.log(`📋 [registerComponentHierarchy] Received resolved spec from runtime:`, { name: result.resolvedSpec.name, hasCode: !!result.resolvedSpec.code, libraryCount: result.resolvedSpec.libraries?.length || 0, dependencyCount: result.resolvedSpec.dependencies?.length || 0 }); } console.log(`✅ [registerComponentHierarchy] Successfully registered ${result.registeredComponents.length} components:`, result.registeredComponents); // Verify the component is actually in the registry const verifyComponent = registry.get(this.component.name, this.component.namespace || 'Global', version); console.log(`🔍 [registerComponentHierarchy] Verification - component in registry after registration:`, { found: !!verifyComponent, name: this.component.name, namespace: this.component.namespace || 'Global', version: version, componentType: verifyComponent ? typeof verifyComponent : 'not found' }); } /** * Post-process resolved spec to ensure all components show their true registry source. * This enriches the spec for UI display purposes to show where components actually came from. * Applied to all resolved specs so any consumer of this wrapper benefits. */ enrichSpecWithRegistryInfo(spec) { if (!spec || !this.component) return spec; // Create a deep copy to avoid mutating the original const enrichedSpec = JSON.parse(JSON.stringify(spec)); // Recursive function to process spec and all dependencies // Takes the original spec at the same level to find registry info const processSpec = (currentSpec, originalSpec) => { // If this component has code but shows location as 'embedded', // check the original spec to see where it came from if (currentSpec.code && currentSpec.location === 'embedded' && currentSpec.name) { // Try to find this component in the original spec at the same level // First check if the original spec itself matches by name if (originalSpec.name === currentSpec.name) { // Use the original's registry info if it had any if (originalSpec.location === 'registry' || originalSpec.registry) { currentSpec.location = 'registry'; if (originalSpec.registry) { currentSpec.registry = originalSpec.registry; } if (originalSpec.namespace) { currentSpec.namespace = originalSpec.namespace; } } } // Also check in original's dependencies for a match if (originalSpec.dependencies) { const originalDep = originalSpec.dependencies.find(d => d.name === currentSpec.name); if (originalDep && (originalDep.location === 'registry' || originalDep.registry)) { currentSpec.location = 'registry'; if (originalDep.registry) { currentSpec.registry = originalDep.registry; } if (originalDep.namespace) { currentSpec.namespace = originalDep.namespace; } } } } // Process all dependencies recursively if (currentSpec.dependencies && Array.isArray(currentSpec.dependencies)) { currentSpec.dependencies.forEach((dep, index) => { // Find the corresponding original dependency by name or use the one at same index let originalDep = originalSpec.dependencies?.find(d => d.name === dep.name); if (!originalDep && originalSpec.dependencies && index < originalSpec.dependencies.length) { originalDep = originalSpec.dependencies[index]; } if (originalDep) { processSpec(dep, originalDep); } }); } }; processSpec(enrichedSpec, this.component); return enrichedSpec; } /** * Render the React component */ async renderComponent() { // Don't render if component is being destroyed if (this.isDestroying) { return; } if (!this.compiledComponent || !this.reactRootId) { return; } // Prevent concurrent renders if (this.isRendering) { this.pendingRender = true; return; } const context = this.reactBridge.getCurrentContext(); if (!context) { return; } this.isRendering = true; const { React } = context; // Resolve components with the correct version using runtime's resolver // SKIP this if using ComponentManager - components are already loaded! let components = {}; if (!this.useComponentManager) { components = await this.resolveComponentsWithVersion(this.component, this.componentVersion); } else { // Use the dependencies that were already loaded and unwrapped by ComponentManager components = this.loadedDependencies; console.log(`🎯 [renderComponent] Using dependencies from ComponentManager:`, Object.keys(components)); } // Create callbacks once per component instance if (!this.currentCallbacks) { this.currentCallbacks = this.createCallbacks(); } // Get libraries from runtime context const runtimeContext = this.adapter.getRuntimeContext(); const libraries = runtimeContext.libraries || {}; // Build props with savedUserSettings pattern const props = { utilities: this.utilities, // Now uses getter which auto-initializes if needed callbacks: this.currentCallbacks, components, styles: this.styles, libraries, // Pass the loaded libraries to components savedUserSettings: this._savedUserSettings, onSaveUserSettings: this.handleSaveUserSettings.bind(this) }; // Validate component before creating element if (!this.compiledComponent.component) { LogError(`Component is undefined for ${this.component.name} during render`); return; } // Components should be functions after HOC wrapping if (typeof this.compiledComponent.component !== 'function') { LogError(`Component is not a function for ${this.component.name}: ${typeof this.compiledComponent.component}`); return; } // Create error boundary const ErrorBoundary = createErrorBoundary(React, { onError: this.handleReactError.bind(this), logErrors: true, recovery: 'retry' }); // Create element with error boundary const element = React.createElement(ErrorBoundary, null, React.createElement(this.compiledComponent.component, props)); // Render with timeout protection using resource manager const timeoutId = resourceManager.setTimeout(this.componentId, () => { // Check if still rendering and not destroyed if (this.isRendering && !this.isDestroying) { this.componentEvent.emit({ type: 'error', payload: { error: 'Component render timeout - possible infinite loop detected', source: 'render' } }); } }, 5000, { purpose: 'render-timeout-protection' }); // Use managed React root for rendering reactRootManager.render(this.reactRootId, element, () => { // Clear the timeout as render completed resourceManager.clearTimeout(this.componentId, timeoutId); // Don't update state if component is destroyed if (this.isDestroying) { return; } this.isRendering = false; // If there was a pending render request, execute it now if (this.pendingRender) { this.pendingRender = false; this.renderComponent(); } }); } /** * Create callbacks for the React component */ createCallbacks() { return { RegisterMethod: (_methodName, _handler) => { // The component compiler wrapper will handle this internally // This is just a placeholder to satisfy the interface // The actual registration happens in the wrapper component }, CreateSimpleNotification: (message, style, hideAfter) => { // Use the MJ notification service to display the notification const notificationStyle = style; this.notificationService.CreateSimpleNotification(message, style, hideAfter); }, OpenEntityRecord: async (entityName, key) => { let keyToUse = null; if (key instanceof Array) { keyToUse = CompositeKey.FromKeyValuePairs(key); } else if (typeof key === 'object' && !!key.GetValueByFieldName) { keyToUse = key; } else if (typeof key === 'object') { //} && !!key.FieldName && !!key.Value) { // possible that have an object that is a simple key/value pair with // FieldName and value properties const keyAny = key; if (keyAny.FieldName && keyAny.Value) { keyToUse = CompositeKey.FromKeyValuePairs([keyAny]); } } if (keyToUse) { // now in some cases we have key/value pairs that the component we are hosting // use, but are not the pkey, so if that is the case, we'll run a quick view to try // and get the pkey so that we can emit the openEntityRecord call with the pkey const md = new Metadata(); const e = md.EntityByName(entityName); let shouldRunView = false; // now check each key in the keyToUse to see if it is a pkey for (const singleKey of keyToUse.KeyValuePairs) { const field = e.Fields.find(f => f.Name.trim().toLowerCase() === singleKey.FieldName.trim().toLowerCase()); if (!field) { // if we get here this is a problem, the component has given us a non-matching field, this shouldn't ever happen // but if it doesn't log warning to console and exit console.warn(`Non-matching field found for key: ${JSON.stringify(keyToUse)}`); return; } else if (!field.IsPrimaryKey) { // if we get here that means we have a non-pkey so we'll want to do a lookup via a RunView // to get the actual pkey value shouldRunView = true; break; } } // if we get here and shouldRunView is true, we need to run a view using the info provided // by our contained component to get the pkey if (shouldRunView) { const rv = new RunView(); const result = await rv.RunView({ EntityName: entityName, ExtraFilter: keyToUse.ToWhereClause() }); if (result && result.Success && result.Results.length > 0) { // we have a match, use the first row and update our keyToUse const kvPairs = []; e.PrimaryKeys.forEach(pk => { kvPairs.push({ FieldName: pk.Name, Value: result.Results[0][pk.Name] }); }); keyToUse = CompositeKey.FromKeyValuePairs(kvPairs); } } this.openEntityRecord.emit({ entityName, key: keyToUse }); } } }; } /** * Handle React component errors */ handleReactError(error, errorInfo) { LogError(`React component error: ${error?.toString() || 'Unknown error'}`, errorInfo); this.componentEvent.emit({ type: 'error', payload: { error: error?.toString() || 'Unknown error', errorInfo, source: 'react' } }); } /** * Handle onSaveUserSettings from components * This implements the SavedUserSettings pattern */ handleSaveUserSettings(newSettings) { // Just bubble the event up to parent containers for persistence // We don't need to store anything here this.userSettingsChanged.emit({ settings: newSettings, componentName: this.component?.name, timestamp: new Date() }); // DO NOT re-render the component! // The component already has the correct state - it's the one that told us about the change. // Re-rendering would cause unnecessary DOM updates and visual flashing. } /** * Clean up resources */ cleanup() { // Clean up all resources managed by resource manager resourceManager.cleanupComponent(this.componentId); // Clean up prop builder subscriptions if (this.currentCallbacks) { this.currentCallbacks = null; } // Unmount React root using managed unmount if (this.reactRootId) { // Force stop rendering flags this.isRendering = false; this.pendingRender = false; // This will handle waiting for render completion if needed reactRootManager.unmountRoot(this.reactRootId); this.reactRootId = null; } // Clear references this.compiledComponent = null; this.isInitialized = false; // Trigger registry cleanup this.adapter.getRegistry().cleanup(); } /** * Public method to refresh the component * @deprecated Components manage their own state and data now */ refresh() { // Check if the component has registered a refresh method if (this.compiledComponent?.refresh) { this.compiledComponent.refresh(); } else { // Fallback: trigger a re-render if needed this.renderComponent(); } } /** * Public method to update state programmatically * @param path - State path to update * @param value - New value * @deprecated Components manage their own state now */ updateState(path, value) { // Just emit the event, don't manage state here this.stateChange.emit({ path, value }); } // ================================================================= // Standard Component Methods - Strongly Typed // ================================================================= /** * Gets the current data state of the component * Used by AI agents to understand what data is currently displayed * @returns The current data state, or undefined if not implemented */ getCurrentDataState() { return this.compiledComponent?.getCurrentDataState?.(); } /** * Gets the history of data state changes in the component * @returns Array of timestamped state snapshots, or empty array if not implemented */ getDataStateHistory() { return this.compiledComponent?.getDataStateHistory?.() || []; } /** * Validates the current state of the component * @returns true if valid, false or validation errors otherwise */ validate() { return this.compiledComponent?.validate?.() || true; } /** * Checks if the component has unsaved changes * @returns true if dirty, false otherwise */ isDirty() { return this.compiledComponent?.isDirty?.() || false; } /** * Resets the component to its initial state */ reset() { this.compiledComponent?.reset?.(); } /** * Scrolls to a specific element or position within the component * @param target - Element selector, element reference, or scroll options */ scrollTo(target) { this.compiledComponent?.scrollTo?.(target); } /** * Sets focus to a specific element within the component * @param target - Element selector or element reference */ focus(target) { this.compiledComponent?.focus?.(target); } /** * Invokes a custom method on the component * @param methodName - Name of the method to invoke * @param args - Arguments to pass to the method * @returns The result of the method call, or undefined if method doesn't exist */ invokeMethod(methodName, ...args) { return this.compiledComponent?.invokeMethod?.(methodName, ...args); } /** * Checks if a method is available on the component * @param methodName - Name of the method to check * @returns true if the method exists */ hasMethod(methodName) { return this.compiledComponent?.hasMethod?.(methodName) || false; } /** * Print the component content * Uses component's print method if available, otherwise uses window.print() */ print() { if (this.compiledComponent?.print) { this.compiledComponent.print(); } else if (typeof window !== 'undefined' && window.print) { window.print(); } } /** * Force clear component registries * Used by Component Studio for fresh loads * This is a static method that can be called without a component instance */ static forceClearRegistries() { // Clear React runtime's component registry service ComponentRegistryService.reset(); // Clear any cached hierarchy registrar if (typeof window !== 'undefined' && window.__MJ_COMPONENT_HIERARCHY_REGISTRAR__) { window.__MJ_COMPONENT_HIERARCHY_REGISTRAR__ = null; } console.log('🧹 All component registries cleared for fresh load'); } static { this.ɵfac = function MJReactComponent_Factory(t) { return new (t || MJReactComponent)(i0.ɵɵdirectiveInject(i1.ReactBridgeService), i0.ɵɵdirectiveInject(i2.AngularAdapterService), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i3.MJNotificationService)); }; } static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MJReactComponent, selectors: [["mj-react-component"]], viewQuery: function MJReactComponent_Query(rf, ctx) { if (rf & 1) { i0.ɵɵviewQuery(_c0, 7, ElementRef); } if (rf & 2) { let _t; i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.container = _t.first); } }, inputs: { component: "component", enableLogging: "enableLogging", useComponentManager: "useComponentManager", utilities: "utilities", styles: "styles", savedUserSettings: "savedUserSettings" }, outputs: { stateChange: "stateChange", componentEvent: "componentEvent", refreshData: "refreshData", openEntityRecord: "openEntityRecord", userSettingsChanged: "userSettingsChanged" }, decls: 4, vars: 3, consts: [["container", ""], [1, "react-component-wrapper"], [1, "react-component-container"], [1, "loading-overlay"], [1, "loading-spinner"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "loading-text"]], template: function MJReactComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelementStart(0, "div", 1); i0.ɵɵelement(1, "div", 2, 0); i0.ɵɵtemplate(3, MJReactComponent_Conditional_3_Template, 5, 0, "div", 3); i0.ɵɵelementEnd(); } if (rf & 2) { i0.ɵɵadvance(); i0.ɵɵclassProp("loading", !ctx.isInitialized); i0.ɵɵadvance(2); i0.ɵɵconditional(!ctx.isInitialized && !ctx.hasError ? 3 : -1); } }, styles: ["[_nghost-%COMP%] {\n display: block;\n width: 100%;\n height: 100%;\n }\n .react-component-wrapper[_ngcontent-%COMP%] {\n position: relative;\n width: 100%;\n height: 100%;\n }\n .react-component-container[_ngcontent-%COMP%] {\n width: 100%;\n height: 100%;\n transition: opacity 0.3s ease;\n }\n .react-component-container.loading[_ngcontent-%COMP%] {\n opacity: 0;\n }\n .loading-overlay[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background-color: rgba(255, 255, 255, 0.9);\n z-index: 1;\n }\n .loading-spinner[_ngcontent-%COMP%] {\n font-size: 48px;\n color: #5B4FE9;\n margin-bottom: 16px;\n }\n .loading-text[_ngcontent-%COMP%] {\n font-family: -apple-system, BlinkMacSystemFont, \"Inter\", \"Segoe UI\", Roboto, sans-serif;\n font-size: 14px;\n color: #64748B;\n margin-top: 8px;\n }"], changeDetection: 0 }); } } (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJReactComponent, [{ type: Component, args: [{ selector: 'mj-react-component', template: ` <div class="react-component-wrapper"> <div #container class="react-component-container" [class.loading]="!isInitialized"></div> @if (!isInitialized && !hasError) { <div class="loading-overlay"> <div class="loading-spinner"> <i class="fa-solid fa-spinner fa-spin"></i> </div> <div class="loading-text">Loading component...</div> </div> } </div> `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n .react-component-wrapper {\n position: relative;\n width: 100%;\n height: 100%;\n }\n .react-component-container {\n width: 100%;\n height: 100%;\n transition: opacity 0.3s ease;\n }\n .react-component-container.loading {\n opacity: 0;\n }\n .loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background-color: rgba(255, 255, 255, 0.9);\n z-index: 1;\n }\n .loading-spinner {\n font-size: 48px;\n color: #5B4FE9;\n margin-bottom: 16px;\n }\n .loading-text {\n font-family: -apple-system, BlinkMacSystemFont, \"Inter\", \"Segoe UI\", Roboto, sans-serif;\n font-size: 14px;\n color: #64748B;\n margin-top: 8px;\n }\n "] }] }], () => [{ type: i1.ReactBridgeService }, { type: i2.AngularAdapterService }, { type: i0.ChangeDetectorRef }, { type: i3.MJNotificationService }], { component: [{ type: Input }], enableLogging: [{ type: Input }], useComponentManager: [{ type: Input }], utilities: [{ type: Input }], styles: [{ type: Input }], savedUserSettings: [{ type: Input }], stateChange: [{ type: Output }], componentEvent: [{ type: Output }], refreshData: [{ type: Output }], openEntityRecord: [{ type: Output }], userSettingsChanged: [{ type: Output }], container: [{ type: ViewChild, args: ['container', { read: ElementRef, static: true }] }] }); })(); (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MJReactComponent, { className: "MJReactComponent", filePath: "lib/components/mj-react-component.component.ts", lineNumber: 128 }); })(); //# sourceMappingURL=mj-react-component.component.js.map