Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, ComponentRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { getComponentFromMap } from '../helpers/sdk_component_map';
import { ErrorBoundaryComponent } from '../../_components/infra/error-boundary/error-boundary.component';
import { getComponentClassAsync } from '../helpers/sdk_component_map';

const componentsRequireDisplayOnlyFAProp: string[] = ['HybridViewContainer', 'ModalViewContainer', 'ViewContainer', 'RootContainer', 'View'];

Expand All @@ -17,6 +17,7 @@ export class ComponentMapperComponent implements OnInit, OnDestroy, OnChanges {

public componentRef: ComponentRef<any> | undefined;
public isInitialized = false;
public lastLoadedName: string | undefined;

@Input() name?: string = '';
@Input() props: any;
Expand All @@ -25,7 +26,10 @@ export class ComponentMapperComponent implements OnInit, OnDestroy, OnChanges {
// parent prop is compulsory when outputEvents is present
@Input() parent: any;

private loadingToken = 0; // Guards against race conditions during rapid name changes

ngOnInit(): void {
// Begin async load (non-blocking) while preserving original synchronous signature
this.loadComponent();
this.isInitialized = true;
}
Expand All @@ -42,14 +46,35 @@ export class ComponentMapperComponent implements OnInit, OnDestroy, OnChanges {
}
}

// Backwards-compatible method name; now performs async dynamic import.
loadComponent() {
const component = getComponentFromMap(this.name || '');
this.loadComponentAsync();
}

private async loadComponentAsync() {
const requestedName = this.name || '';
const token = ++this.loadingToken;

// Prefer dynamic loader for lazy chunks; fallback to static map if not defined yet.
let componentClass: any;
try {
componentClass = await getComponentClassAsync(requestedName);
} catch (err) {
console.error('Error loading component dynamically; falling back to static map', requestedName, err);
componentClass = ErrorBoundaryComponent;
}

// If another async load started after this one, abandon this result.
if (token !== this.loadingToken) {
return;
}

if (this.dynamicComponent) {
this.dynamicComponent.clear();
this.componentRef = this.dynamicComponent.createComponent(component);
this.componentRef = this.dynamicComponent.createComponent(componentClass);
this.lastLoadedName = requestedName;

if (component === ErrorBoundaryComponent) {
if (componentClass === ErrorBoundaryComponent) {
this.componentRef.instance.message = this.errorMsg;
} else {
this.bindInputProps();
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,41 +1,39 @@
// Helper singleton class to assist with loading and
// accessing the SDK components
// import localSdkComponentMap from '../../../sdk-local-component-map';
import pegaSdkComponentMap from './sdk-pega-component-map';
import { Type } from '@angular/core';
import { componentClassCache, componentLoaders } from './sdk-pega-component-map';
import { ErrorBoundaryComponent } from '../../_components/infra/error-boundary/error-boundary.component';

// Statically load all "local" components

// Create a singleton for this class (with async loading of components map file) and export it
// Note: Initializing SdkComponentMap to null seems to cause lots of compile issues with references
// within other components and the value potentially being null (so try to leave it undefined)

export let SdkComponentMap;
export let SdkComponentMap: ComponentMap;
let SdkComponentMapCreateInProgress = false;

interface ISdkComponentMap {
localComponentMap: object;
pegaProvidedComponentMap: object;
}

/**
* Helper singleton class to assist with loading and
* accessing the SDK components.
*
* Creates a singleton for this class (with async loading of the components map file) and exports it.
*
* Note: Initializing SdkComponentMap to null seems to cause compile issues with references
* within other components and the value potentially being null, so try to leave it undefined.
*/
class ComponentMap {
sdkComponentMap: ISdkComponentMap; // Top level object
sdkComponentMap: ISdkComponentMap;
isComponentMapLoaded: boolean;

constructor() {
// sdkComponentMap is top-level object
this.sdkComponentMap = { localComponentMap: {}, pegaProvidedComponentMap: {} };

// isCoComponentMapLoaded will be updated to true after the async load is complete
// isComponentMapLoaded will be updated to true after the async load is complete
this.isComponentMapLoaded = false;

// pegaSdkComponents.local is the JSON object where we'll store the components that are
// found locally or can be found in the Pega-provided repo
// Initialize the local and Pega-provided component maps as empty objects.
// These will later be populated with components either defined locally or provided by Pega.
this.sdkComponentMap.localComponentMap = {};

this.sdkComponentMap.pegaProvidedComponentMap = {};

// The "work" to load the config file is done (asynchronously) via the initialize
// (Factory function) below)
}

/**
Expand All @@ -48,11 +46,8 @@ class ComponentMap {
Object.keys(this.sdkComponentMap.localComponentMap).length === 0 &&
Object.keys(this.sdkComponentMap.pegaProvidedComponentMap).length === 0
) {
const theLocalCompPromise = this.readLocalSdkComponentMap(inLocalSdkComponentMap);
const thePegaCompPromise = this.readPegaSdkComponentMap(pegaSdkComponentMap);

Promise.all([theLocalCompPromise, thePegaCompPromise])
.then((/* results */) => {
this.readLocalSdkComponentMap(inLocalSdkComponentMap)
.then(() => {
resolve(this.sdkComponentMap);
})
.catch(error => {
Expand All @@ -65,15 +60,13 @@ class ComponentMap {
}

async readLocalSdkComponentMap(inLocalSdkComponentMap = {}) {
// debugger;
if (Object.entries(this.getLocalComponentMap()).length === 0) {
this.sdkComponentMap.localComponentMap = inLocalSdkComponentMap;
}
return Promise.resolve(this);
}

async readPegaSdkComponentMap(inPegaSdkComponentMap = {}) {
// debugger;
if (Object.entries(this.getPegaProvidedComponentMap()).length === 0) {
this.sdkComponentMap.pegaProvidedComponentMap = inPegaSdkComponentMap;
}
Expand All @@ -99,32 +92,41 @@ class ComponentMap {
};
}

// Implement Factory function to allow async load
// See https://stackoverflow.com/questions/49905178/asynchronous-operations-in-constructor/49906064#49906064 for inspiration
/**
* Implement Factory function to allow async load.
*/
async function createSdkComponentMap(inLocalComponentMap = {}) {
// Note that our initialize function returns a promise...
const singleton = new ComponentMap();
await singleton.readSdkComponentMap(inLocalComponentMap);
return singleton;
}

// Initialize exported SdkComponentMap structure
/**
* Retrieves the singleton SDK component map, creating it if necessary.
*
* This function ensures that only one instance of the SDK component map exists.
* If the map is not yet initialized, it triggers its creation and resolves once ready.
* If the map is already being created by another call, it waits until the map is available.
* Once the map is created, a `SdkComponentMapReady` event is dispatched on the document.
*
* @param inLocalComponentMap - An optional object to use as the initial local component map.
* @returns A promise that resolves to the SDK component map instance.
*/
export async function getSdkComponentMap(inLocalComponentMap = {}) {
return new Promise(resolve => {
let idNextCheck;
if (!SdkComponentMap && !SdkComponentMapCreateInProgress) {
SdkComponentMapCreateInProgress = true;
createSdkComponentMap(inLocalComponentMap).then(theComponentMap => {
// debugger;
// Key initialization of SdkComponentMap
SdkComponentMap = theComponentMap;
SdkComponentMapCreateInProgress = false;
console.log(`getSdkComponentMap: created SdkComponentMap singleton`);
// Create and dispatch the SdkConfigAccessReady event
// Not used anyplace yet but putting it in place in case we need it.
// Not used anyplace yet but putting it in place in case we need it.
const event = new CustomEvent('SdkComponentMapReady', {});
document.dispatchEvent(event);
return resolve(SdkComponentMap /* .sdkComponentMap */);
return resolve(SdkComponentMap);
});
} else {
const fnCheckForConfig = () => {
Expand All @@ -144,16 +146,25 @@ export async function getSdkComponentMap(inLocalComponentMap = {}) {
});
}

/**
* Retrieves the component implementation associated with the given component name.
*
* This function first attempts to find the component in the local component map.
* If not found, it checks the Pega-provided component map. If the component is
* not found in either map, it logs an error and recursively attempts to return
* the 'ErrorBoundary' component implementation as a fallback.
*
* @param inComponentName - The name of the component to retrieve.
* @returns The implementation of the requested component, or the ErrorBoundary component if not found.
*/
export function getComponentFromMap(inComponentName: string): any {
let theComponentImplementation = null;
const theLocalComponent = SdkComponentMap.getLocalComponentMap()[inComponentName];
if (theLocalComponent !== undefined) {
console.log(`Requested component found ${inComponentName}: Local`);
theComponentImplementation = theLocalComponent;
} else {
const thePegaProvidedComponent = SdkComponentMap.getPegaProvidedComponentMap()[inComponentName];
if (thePegaProvidedComponent !== undefined) {
// console.log(`Requested component found ${inComponentName}: Pega-provided`);
theComponentImplementation = thePegaProvidedComponent;
} else {
console.error(`Requested component has neither Local nor Pega-provided implementation: ${inComponentName}`);
Expand All @@ -162,3 +173,44 @@ export function getComponentFromMap(inComponentName: string): any {
}
return theComponentImplementation;
}

/**
* Dynamically loads an Angular component class by its logical name.
* Uses a cache to avoid re-importing components during the session.
* Falls back to ErrorBoundaryComponent if the requested component is unknown or fails to load.
*
* @param name The logical component name (as used in metadata or input).
* @returns A Promise resolving to the Angular component class (Type<any>).
*/
export async function getComponentClassAsync(name: string): Promise<Type<any>> {
// Use 'ErrorBoundary' as a fallback if name is empty or undefined
const safeName = name || 'ErrorBoundary';

// Return cached class if already loaded
if (componentClassCache[safeName]) {
return componentClassCache[safeName];
}

// Get the loader function for the component
const loader = componentLoaders[safeName];
if (!loader) {
// If component is unknown, fallback to ErrorBoundary
return getComponentClassAsync('ErrorBoundary');
}

try {
// Dynamically import and cache the component class
const cls = await loader();
componentClassCache[safeName] = cls;
return cls;
} catch (e) {
// Log error and fallback to ErrorBoundary if loading fails

console.error('Dynamic import failed for', safeName, e);
if (safeName !== 'ErrorBoundary') {
return getComponentClassAsync('ErrorBoundary');
}
// If ErrorBoundary itself fails, return the static ErrorBoundaryComponent
return ErrorBoundaryComponent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@ import { Utils } from 'packages/angular-sdk-components/src/lib/_helpers/utils';
import { compareSdkPCoreVersions } from 'packages/angular-sdk-components/src/lib/_helpers/versionHelpers';
import { HeaderComponent } from './header/header.component';
import { MainScreenComponent } from './main-screen/main-screen.component';

import { getSdkComponentMap } from 'packages/angular-sdk-components/src/lib/_bridge/helpers/sdk_component_map';
import localSdkComponentMap from 'packages/angular-sdk-components/src/sdk-local-component-map';
import { ThemeService } from 'packages/angular-sdk-components/src/lib/_services/theme.service';
import { initializeAuthentication } from './utils';
import { ThemeService } from 'packages/angular-sdk-components/src/lib/_services/theme.service';

declare global {
interface Window {
Expand Down Expand Up @@ -87,12 +84,6 @@ export class EmbeddedComponent implements OnInit, OnDestroy {

// Check that we're seeing the PCore version we expect
compareSdkPCoreVersions();

// Initialize the SdkComponentMap (local and pega-provided)
await getSdkComponentMap(localSdkComponentMap);
console.log(`SdkComponentMap initialized`);

// Don't call initialRender until SdkComponentMap is fully initialized
this.initialRender(renderObj);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import { ServerConfigService } from 'packages/angular-sdk-components/src/lib/_se
import { compareSdkPCoreVersions } from 'packages/angular-sdk-components/src/lib/_helpers/versionHelpers';
import { ComponentMapperComponent } from 'packages/angular-sdk-components/src/lib/_bridge/component-mapper/component-mapper.component';

import { getSdkComponentMap } from 'packages/angular-sdk-components/src/lib/_bridge/helpers/sdk_component_map';
import localSdkComponentMap from 'packages/angular-sdk-components/src/sdk-local-component-map';

declare global {
interface Window {
myLoadPortal: Function;
Expand Down Expand Up @@ -93,14 +90,7 @@ export class FullPortalComponent implements OnInit, OnDestroy {
PCore.onPCoreReady(renderObj => {
// Check that we're seeing the PCore version we expect
compareSdkPCoreVersions();

// Initialize the SdkComponentMap (local and pega-provided)
getSdkComponentMap(localSdkComponentMap).then((theComponentMap: any) => {
console.log(`SdkComponentMap initialized`, theComponentMap);

// Don't call initialRender until SdkComponentMap is fully initialized
this.initialRender(renderObj);
});
this.initialRender(renderObj);
});

const { appPortal: thePortal, excludePortals } = this.scservice.getSdkConfigServer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import { compareSdkPCoreVersions } from '../../../../../../../packages/angular-s
import { MainContentComponent } from '../main-content/main-content.component';
import { SideBarComponent } from '../side-bar/side-bar.component';

import { getSdkComponentMap } from '../../../../../../../packages/angular-sdk-components/src/lib/_bridge/helpers/sdk_component_map';
import localSdkComponentMap from '../../../../../../../packages/angular-sdk-components/src/sdk-local-component-map';

declare global {
interface Window {
myLoadMashup: Function;
Expand Down Expand Up @@ -123,13 +120,7 @@ export class NavigationComponent implements OnInit, OnDestroy {

startMashup() {
PCore.onPCoreReady(renderObj => {
// Initialize the SdkComponentMap (local and pega-provided)
getSdkComponentMap(localSdkComponentMap).then((theComponentMap: any) => {
console.log(`SdkComponentMap initialized`, theComponentMap);

// Don't call initialRender until SdkComponentMap is fully initialized
this.initialRender(renderObj);
});
this.initialRender(renderObj);
});

window.myLoadMashup('app-root', false); // this is defined in bootstrap shell that's been loaded already
Expand Down
34 changes: 18 additions & 16 deletions projects/angular-test-app/src/app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { Routes } from '@angular/router';
import { FullPortalComponent } from './_samples/full-portal/full-portal.component';
import { EmbeddedComponent } from './_samples/embedded/embedded.component';
import { NavigationComponent } from './_samples/simple-portal/navigation/navigation.component';
import { endpoints } from '../../../../packages/angular-sdk-components/src/lib/_services/endpoints';
import { endpoints } from 'packages/angular-sdk-components/src/public-api';

// Adding path to remove "Cannot match routes" error at launch
// Tried this at one point... Need to add /app in path now...
Expand All @@ -12,17 +9,22 @@ import { endpoints } from '../../../../packages/angular-sdk-components/src/lib/_
// But we can get it from window.location.pathname

// const appName = window.location.pathname.split('/')[3];

export const routes: Routes = [
{ path: '', component: EmbeddedComponent },
{ path: endpoints.PORTAL, component: FullPortalComponent },
{ path: endpoints.PORTALHTML, component: FullPortalComponent },
{ path: endpoints.FULLPORTAL, component: FullPortalComponent },
{ path: endpoints.FULLPORTALHTML, component: FullPortalComponent },
{ path: endpoints.EMBEDDED, component: EmbeddedComponent },
{ path: endpoints.EMBEDDEDHTML, component: EmbeddedComponent },
{ path: endpoints.MASHUP, component: EmbeddedComponent },
{ path: endpoints.MASHUPHTML, component: EmbeddedComponent },
{ path: endpoints.SIMPLEPORTAL, component: NavigationComponent },
{ path: endpoints.SIMPLEPORTALHTML, component: NavigationComponent }
{ path: '', loadComponent: () => import('./_samples/embedded/embedded.component').then(m => m.EmbeddedComponent) },
{ path: endpoints.PORTAL, loadComponent: () => import('./_samples/full-portal/full-portal.component').then(m => m.FullPortalComponent) },
{ path: endpoints.PORTALHTML, loadComponent: () => import('./_samples/full-portal/full-portal.component').then(m => m.FullPortalComponent) },
{ path: endpoints.FULLPORTAL, loadComponent: () => import('./_samples/full-portal/full-portal.component').then(m => m.FullPortalComponent) },
{ path: endpoints.FULLPORTALHTML, loadComponent: () => import('./_samples/full-portal/full-portal.component').then(m => m.FullPortalComponent) },
{ path: endpoints.EMBEDDED, loadComponent: () => import('./_samples/embedded/embedded.component').then(m => m.EmbeddedComponent) },
{ path: endpoints.EMBEDDEDHTML, loadComponent: () => import('./_samples/embedded/embedded.component').then(m => m.EmbeddedComponent) },
{ path: endpoints.MASHUP, loadComponent: () => import('./_samples/embedded/embedded.component').then(m => m.EmbeddedComponent) },
{ path: endpoints.MASHUPHTML, loadComponent: () => import('./_samples/embedded/embedded.component').then(m => m.EmbeddedComponent) },
{
path: endpoints.SIMPLEPORTAL,
loadComponent: () => import('./_samples/simple-portal/navigation/navigation.component').then(m => m.NavigationComponent)
},
{
path: endpoints.SIMPLEPORTALHTML,
loadComponent: () => import('./_samples/simple-portal/navigation/navigation.component').then(m => m.NavigationComponent)
}
];