@memberjunction/ng-react
Version:
Angular components for hosting React components in MemberJunction applications
956 lines • 47.9 kB
JavaScript
/**
* @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>
(!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