@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
text/typescript
// 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>;
}
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();
}
}
*/