UNPKG

@quartal/bridge-client

Version:

Universal client library for embedding applications with URL-configurable transport support (iframe, postMessage) and framework adapters for Angular and Vue

556 lines (458 loc) 16.2 kB
// Example: Generic Angular child client service using QuartalAngularAdapter // This example shows how to implement a child client service that can be embedded in parent applications // Based on quartal-invoicing-ui app.service.ts but made generic for any Angular application import { Injectable } from '@angular/core'; import { NavigationEnd, Router, UrlTree } from '@angular/router'; import { ChildClient, QuartalAngularAdapter, FastAction, FastActionCallbackResponse, ParentDataResponse, ClearStorageRequest } from '@quartal/client'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { filter } from 'rxjs/operators'; // Define your app-specific interfaces interface AppSettings { customActions?: any[]; customEntities?: any[]; customTabs?: any; customTables?: any[]; [key: string]: any; } interface HttpStatusService { hasPendingRequest(): Observable<boolean>; } @Injectable({ providedIn: 'root' }) export class GenericChildClientService { // App settings management private _appSettings = new BehaviorSubject<AppSettings | null>(null); appSettings$ = this._appSettings.asObservable(); get appSettings(): AppSettings | null { return this._appSettings.value; } // Fast actions management private _fastAction = new Subject<FastAction>(); fastAction$ = this._fastAction.asObservable(); private _fastActionCallback = new Subject<FastActionCallbackResponse>(); fastActionCallback$ = this._fastActionCallback.asObservable(); // Parent data management private _parentData = new Subject<ParentDataResponse>(); parentData$ = this._parentData.asObservable(); // Custom entities management private _customEntities = new Subject<any>(); customEntities$ = this._customEntities.asObservable(); // User notifications (optional) private _userNotifications = new Subject<any>(); userNotifications$ = this._userNotifications.asObservable(); // Core state quartalClient: ChildClient | null = null; isInIframe: boolean = false; fastActions: FastAction[] = []; private isInitialized = false; constructor( private router: Router, private storageService: StorageService, private httpStatusService?: HttpStatusService // Optional ) { // Check if running in iframe this.isInIframe = window !== window.parent; if (this.isInIframe) { console.log('Child client detected iframe context, initializing...'); this.initializeChildClient(); } else { console.log('Running as standalone app, no child client needed'); // Set default app settings for standalone mode const defaultSettings = this.getDefaultAppSettings(); this.setAppSettings(defaultSettings); } } private initializeChildClient(): void { if (this.quartalClient) { console.log('Child client already initialized'); return; } try { // Create Angular adapter with required services const quartalAdapter = new QuartalAngularAdapter( this.router, this.storageService ); // Create child client with configuration this.quartalClient = quartalAdapter.createChildClient({ debug: false, trace: false, appPrefix: 'MyApp', autoConnect: true }, { // Called when parent client sends initialization data onInited: (data) => { console.log('Child client inited with data:', data); this.handleInitData(data); }, // Parent requests URL change in child onOpenUrl: (url: string) => { console.log('Parent requested URL change:', url); this.handleUrlChange(url); }, // Parent requests navigation with redirect onRedirect: (url: string[]) => { console.log('Parent requested redirect:', url); this.handleRedirect(url); }, // Parent sends action to execute onOpenAction: (data: any) => { console.log('Parent sent action:', data); this.handleAction(data); }, // Parent requests cache clear onClearCache: (data: any) => { console.log('Parent requested cache clear:', data); this.handleClearCache(data); }, // Parent requests storage clear onClearStorage: (request: ClearStorageRequest) => { console.log('Parent requested storage clear:', request); this.handleClearStorage(request); }, // Parent requests logout onLogout: () => { console.log('Parent requested logout'); this.handleLogout(); }, // Parent sends data onFetchedData: (data: any) => { console.log('Parent sent data:', data); this.setParentData(data); } }); console.log('Child client initialized successfully'); } catch (error) { console.error('Failed to initialize child client:', error); } } private handleInitData(data: any): void { // Get app settings from parent or use defaults const appSettings: AppSettings = this.quartalClient?.getAppSettings() || this.getDefaultAppSettings(); // Merge custom data from parent appSettings.customActions = data.customActions || []; appSettings.customEntities = data.customEntities || []; appSettings.customTabs = data.customTabs || {}; appSettings.customTables = data.customTables || []; this.setAppSettings(appSettings); console.log('App settings updated:', appSettings); // Initialize event listeners after client is ready this.initializeEventListeners(); } private initializeEventListeners(): void { if (!this.quartalClient || !this.isInIframe || this.isInitialized) { return; } this.isInitialized = true; let firstNavigation = true; // Track navigation changes and send to parent this.router.events .pipe( filter(event => event instanceof NavigationEnd) ).subscribe((event: NavigationEnd) => { // Skip first navigation to avoid redundant initial URL change if (!firstNavigation) { const url = event.urlAfterRedirects || event.url; // Skip certain URLs that shouldn't be sent to parent if (this.shouldSkipUrlChange(url)) { return; } this.quartalClient!.sendUrlChange(url); } else { firstNavigation = false; } }); // Track pending HTTP requests (if service provided) if (this.httpStatusService) { this.httpStatusService.hasPendingRequest().subscribe(pending => { if (this.quartalClient && this.isInIframe) { this.quartalClient.sendPendingRequest(pending); } }); } // Track storage changes and sync to parent this.storageService.dataUpdated$.subscribe(({ key, value }) => { console.log('Storage updated:', key, value); // Skip sensitive data that shouldn't be synced if (this.shouldSkipStorageSync(key)) { return; } if (this.quartalClient && this.isInIframe) { this.quartalClient.sendDataChanged(key, JSON.stringify(value)); } }); // Track fast action callbacks this.fastActionCallback$.subscribe(callback => { if (this.quartalClient && this.isInIframe) { this.quartalClient.sendFastActionCallback(callback); } }); // Track user notifications if available this.userNotifications$.subscribe(notifications => { if (this.quartalClient && this.isInIframe) { this.quartalClient.sendUserNotifications(notifications); } }); // Initialize user-specific data this.initUserSpecificData(); // Signal to parent that child is ready this.setInitialized(true); } private handleUrlChange(url: string): void { if (!url) { // Navigate to empty/default route this.router.navigate(['/']); return; } if (url.includes('?')) { // Handle URLs with query parameters const tree: UrlTree = this.router.parseUrl(url); this.router.navigate([url.split('?')[0]], { queryParams: tree.queryParams }); } else { this.router.navigate([url]); } // Update iframe size after navigation this.updateIframeSize(); } private handleRedirect(route: string[]): void { // Clear cache before redirect this.handleClearCache(null); // Navigate with redirect pattern to ensure clean state this.router.navigateByUrl('/redirect', { skipLocationChange: true }) .then(() => this.router.navigate(route)) .catch(console.error); } private handleAction(data: any): void { if (data.action) { this._fastAction.next(data); } } private handleClearCache(data: any): void { // Implement cache clearing logic specific to your app // Example: Clear HTTP cache, component caches, etc. console.log('Clearing application caches...'); // Notify cache clearing to your app's cache systems // globalCacheBusterNotifier.next(); // Clear any other caches your app uses this.clearApplicationCaches(); } private handleClearStorage(request: ClearStorageRequest): void { if (request.dataTables) { // Clear DataTables storage for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (key && key.startsWith('DataTables_')) { localStorage.removeItem(key); } } } if (request.everything) { // Clear all local storage (be careful with this) localStorage.clear(); } // Add your own storage clearing logic here this.clearApplicationStorage(request); } private handleLogout(): void { // Handle logout request from parent localStorage.removeItem('idToken'); localStorage.removeItem('accessToken'); // Clear any other authentication data this.clearAuthenticationData(); // Navigate to login or home this.router.navigate(['/']); } // Public API methods setAppSettings(appSettings: AppSettings): void { if (!appSettings) return; this._appSettings.next(appSettings); } setFastAction(fastAction: FastAction): void { if (fastAction) { console.log('Setting fast action:', fastAction); this._fastAction.next(fastAction); } } setFastActionCallback(callback: FastActionCallbackResponse): void { if (callback) { console.log('Setting fast action callback:', callback); this._fastActionCallback.next(callback); } } setParentData(data: ParentDataResponse): void { if (data) { console.log('Setting parent data:', data); this._parentData.next(data); } } setUserNotifications(notifications: any): void { if (notifications) { this._userNotifications.next(notifications); } } setInitialized(initialized: boolean): void { console.log('Setting initialized state:', initialized); if (this.quartalClient) { this.quartalClient.sendInited({ inited: initialized, version: this.getAppVersion() }); } } // Update iframe size after content changes updateIframeSize(): void { if (!this.isInIframe) return; // Initial size update setTimeout(() => { const height = document.body.scrollHeight; this.setIframeSize(height); }); // Secondary update for dynamic content setTimeout(() => { const newHeight = document.body.scrollHeight; this.setIframeSize(newHeight); }, 400); } setIframeSize(height: number): void { if (this.quartalClient && this.isInIframe) { this.quartalClient.sendHeightChange(height); } } // Send dialog state changes to parent sendDialogState(isOpen: boolean): void { if (this.quartalClient && this.isInIframe) { this.quartalClient.sendDialogChanged(isOpen); } } // Send fast actions to parent sendFastActions(actions: FastAction[]): void { if (this.quartalClient && this.isInIframe) { this.quartalClient.sendFastActions(actions); } } // Cleanup method destroy(): void { if (this.quartalClient) { this.quartalClient.destroy(); this.quartalClient = null; } this._appSettings.complete(); this._fastAction.complete(); this._fastActionCallback.complete(); this._parentData.complete(); this._customEntities.complete(); this._userNotifications.complete(); } // Override these methods in your implementation protected getDefaultAppSettings(): AppSettings { return { customActions: [], customEntities: [], customTabs: {}, customTables: [] }; } protected getAppVersion(): string { return '1.0.0'; // Replace with your app version } protected shouldSkipUrlChange(url: string): boolean { // Skip URLs that shouldn't be reported to parent const skipUrls = ['/redirect', '/not-authenticated', '/empty', '/login']; return skipUrls.some(skipUrl => url.includes(skipUrl)); } protected shouldSkipStorageSync(key: string): boolean { // Skip sensitive data that shouldn't be synced to parent const skipKeys = ['idToken', 'accessToken', 'refreshToken']; return skipKeys.includes(key); } protected clearApplicationCaches(): void { // Implement your app-specific cache clearing logic console.log('Clearing application-specific caches...'); } protected clearApplicationStorage(request: ClearStorageRequest): void { // Implement your app-specific storage clearing logic console.log('Clearing application-specific storage...', request); } protected clearAuthenticationData(): void { // Clear your app-specific authentication data console.log('Clearing authentication data...'); } protected initUserSpecificData(): void { // Initialize user-specific data after client is ready // Example: Load user notifications, fast actions, etc. setTimeout(() => { this.loadUserNotifications(); this.loadFastActions(); }, 500); } protected loadUserNotifications(): void { // Load and send user notifications if applicable console.log('Loading user notifications...'); } protected loadFastActions(): void { // Load and send fast actions if applicable console.log('Loading fast actions...'); // Example: // this.getFastActions().subscribe(actions => { // this.fastActions = actions; // this.sendFastActions(actions); // }); } } // Usage example in your Angular app: /* // In your app.module.ts or main component export class AppComponent implements OnInit, OnDestroy { constructor( private childClientService: GenericChildClientService, private dialog: MatDialog // If using Angular Material ) {} ngOnInit() { // Listen for app settings changes this.childClientService.appSettings$.subscribe(settings => { if (settings) { console.log('App settings updated:', settings); // Update your app based on settings } }); // Listen for fast actions this.childClientService.fastAction$.subscribe(action => { if (action) { console.log('Fast action received:', action); // Handle the action in your app } }); // Track dialog state if using Angular Material this.dialog.afterOpened.subscribe(() => { this.childClientService.sendDialogState(true); }); this.dialog.afterAllClosed.subscribe(() => { this.childClientService.sendDialogState(false); }); } ngOnDestroy() { this.childClientService.destroy(); } } // In a component that needs to send data to parent export class SomeComponent { constructor(private childClientService: GenericChildClientService) {} onActionComplete() { // Send callback to parent this.childClientService.setFastActionCallback({ success: true, data: { message: 'Action completed successfully' } }); } onContentChanged() { // Update iframe size when content changes this.childClientService.updateIframeSize(); } } */