@quartal/bridge-client
v1.0.5
Published
Universal client library for embedding applications with URL-configurable transport support (iframe, postMessage) and framework adapters for Angular and Vue
Downloads
22
Readme
Quartal Bridge Client
Universal client library for embedding applications with URL-configurable transport support and framework adapters for Angular and Vue.
Features
- 🚀 Framework-agnostic - Works without Angular/Vue dependencies
- 🔧 Framework Adapters - Easy integration for Angular and Vue
- 📦 Treeshakable - Only used parts included in bundle
- 🔄 Multiple Transports - Iframe and PostMessage communication
- 🌐 URL Configuration - Configure transport via URL parameters
- 🎯 Singleton pattern - One instance per application
- 🐛 Debug logging - Comprehensive logging and error handling
- 📱 TypeScript - Full TypeScript support
- 🎨 Named instances - Multiple isolated instances for complex applications
Installation
npm install @quartal/bridge-client
# or
yarn add @quartal/bridge-client
# or
pnpm add @quartal/bridge-clientQuick Start
URL-Based Transport Configuration (Recommended)
Child applications automatically detect transport configuration from URL parameters:
<!-- Iframe transport (default) -->
<iframe src="https://your-app.com/quartal-accounting-embed"></iframe>
<!-- PostMessage transport for popups -->
<iframe src="https://your-app.com/quartal-accounting-embed?transport=postmessage&debug=true"></iframe>Vue.js Child Application
import { QuartalVueAdapter } from '@quartal/bridge-client';
// Automatically uses URL parameters for transport configuration
const adapter = new QuartalVueAdapter(router);
const childClient = adapter.createChildClient({
appPrefix: 'VUE-APP',
autoConnect: true
}, {
onInited: (data) => console.log('Connected to parent:', data),
onOpenUrl: (url) => router.push(url)
});Angular Child Application
import { QuartalAngularAdapter } from '@quartal/bridge-client';
// Automatically uses URL parameters for transport configuration
```typescript
const adapter = new QuartalAngularAdapter(router);const childClient = adapter.createChildClient({ appPrefix: 'NG-APP', autoConnect: true }, { onInited: (data) => console.log('Connected to parent:', data), onRedirect: (url) => this.router.navigate(url) });
## URL Transport Configuration
Configure transport behavior via URL parameters without code changes:
### Supported Parameters
| Parameter | Values | Default | Description |
|-----------|--------|---------|-------------|
| `transport` | `iframe`, `postmessage` | `iframe` | Transport type |
| `debug` | `true`, `false` | `false` | Enable debug logging |
| `trace` | `true`, `false` | `false` | Enable trace logging |
| `targetOrigin` | URL or `*` | `*` | PostMessage target origin |
| `channel` | string | `quartal-bridge` | PostMessage channel |
| `timeout` | number | `10000` | Connection timeout (ms) |
### Examples
```html
<!-- Standard iframe embedding -->
<iframe src="https://app.com/embed"></iframe>
<!-- PostMessage for popups with debug -->
<iframe src="https://app.com/embed?transport=postmessage&debug=true"></iframe>
<!-- Secure PostMessage with specific origin -->
<iframe src="https://app.com/embed?transport=postmessage&targetOrigin=https://parent.com"></iframe>JavaScript Popup
const popup = window.open(
'https://app.com/embed?transport=postmessage&targetOrigin=' + window.location.origin,
'quartal-app',
'width=1200,height=800'
);For detailed configuration options, see URL Transport Configuration.
Architecture Patterns
Multi-level iframe Communication
The Quartal Client supports complex iframe chains with different integration patterns:
partner-ui (Framework agnostic parent)
↓ parentClientManager.initializeParentClient()
↓
quartal-invoicing-ui (Angular Hybrid: child + parent)
↓ QuartalAngularAdapter.createChildClient() → AppService → QuartalEventBridge → IFrameComponent → parentClientManager.initializeParentClient()
↓
quartal-accounting-embed (Vue child)
↓ useAppService() composable with ChildClientIntegration Patterns
1. Parent Applications (Framework-agnostic)
- Pattern: Use
parentClientManagerdirectly - Benefits: Framework-agnostic, singleton lifecycle management
- Used by: Partners' own UI and quartal-invoicing-ui's iframe components
2. Angular Child Applications
- Pattern: Use
QuartalAngularAdapter - Benefits: Angular router/service integration, dependency injection
- Used by: quartal-invoicing-ui (later partners' own UI)
3. Vue Child Applications
- Pattern: Direct composable usage or
QuartalVueAdapter - Benefits: Vue composable pattern, reactive integration
- Used by: quartal-accounting-embed (later partners' own UI)
Event Flow Examples
All events follow the same technical path: Child Client → QuartalEventBridge → Parent Client
Height Changes:
- quartal-accounting-embed (DOM observer) →
childClient.sendHeightChange() - → quartal-invoicing-ui (
QuartalEventBridgeforwarding) - → partner-ui (
onHeightChangecallback)
- quartal-accounting-embed (DOM observer) →
Dialog States:
- quartal-accounting-embed (QDialog component) →
childClient.sendDialogChanged() - → quartal-invoicing-ui (
QuartalEventBridgeforwarding) - → partner-ui (
onDialogChangedcallback)
- quartal-accounting-embed (QDialog component) →
HTTP Status:
- quartal-accounting-embed (HTTP interceptor) →
childClient.sendPendingRequest() - → quartal-invoicing-ui (
QuartalEventBridgeforwarding) - → partner-ui (
onPendingRequestcallback)
- quartal-accounting-embed (HTTP interceptor) →
Hybrid Application Pattern
For applications that act as both parent and child (like quartal-invoicing-ui):
import { QuartalEventBridge, INTERNAL_EVENTS } from '@quartal/client';
// quartal-invoicing-ui: Acts as child to partner applications AND parent to child applications
export class AppService {
private quartalClient: ChildClient; // Communicates UP to partner-ui
private eventBridge: QuartalEventBridge; // Internal forwarding
initializeClient() {
// Use Angular adapter for child functionality
const quartalAdapter = new QuartalAngularAdapter(
this.router
);
this.quartalClient = quartalAdapter.createChildClient({
debug: false,
appPrefix: 'MyApp'
}, {
onInited: (data) => this.handleInitialization(data),
onLogout: () => this.handleLogout()
});
// Set up internal event forwarding
this.setupEventForwarding();
}
// Forward events from internal components to parent (partner-ui)
private setupEventForwarding() {
this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, (message) => {
if (this.quartalClient?.isReady()) {
this.quartalClient.sendDialogChanged(message.data.isOpen);
}
});
this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_HEIGHT_CHANGE, (message) => {
if (this.quartalClient?.isReady()) {
this.quartalClient.sendHeightChange(message.data.height);
}
});
}
}
export class IFrameComponent {
private parentClient: ParentClient; // Communicates DOWN to child applications
private eventBridge: QuartalEventBridge; // Internal forwarding
// Initialize with parentClientManager for parent functionality
private initializeClient() {
this.parentClient = parentClientManager.initializeParentClient(
{
iframeElement: this.iframe.nativeElement,
appPrefix: 'MyApp',
debug: true,
user: this.getCurrentUser()
},
{
onHeightChange: (height) => {
// Forward to internal event bridge → AppService → partner-ui
this.eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_HEIGHT_CHANGE, { height }, 'iframe-component');
},
onDialogChanged: (dialogState) => {
this.eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, { isOpen: dialogState.opened }, 'iframe-component');
},
onPendingRequest: (pending) => {
this.eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_PENDING_REQUEST, { pending }, 'iframe-component');
}
}
);
}
}Quick Start
For Parent Applications
import { parentClientManager } from '@quartal/client';
const parentClient = parentClientManager.initializeParentClient(
{
iframeElement: document.getElementById('quartal-iframe') as HTMLIFrameElement,
appPrefix: 'MyApp',
user: {
id: 'user123',
email: '[email protected]',
name: 'John Doe',
phone: '+358401234567'
}
},
{
onInited: (data) => console.log('Child initialized:', data),
onHeightChange: (height) => console.log('Height changed:', height)
}
);For Angular Child Applications
import { QuartalAngularAdapter } from '@quartal/client';
// In your service
const adapter = new QuartalAngularAdapter(router);
const childClient = adapter.createChildClient({
appPrefix: 'MyApp'
}, {
onInited: (data) => console.log('Ready:', data)
});For Vue Child Applications
import { QuartalVueAdapter } from '@quartal/client';
// In your composable
const adapter = new QuartalVueAdapter(router);
const childClient = adapter.createChildClient({
appPrefix: 'MyApp'
}, {
onInited: () => console.log('Vue child ready')
});Detailed Usage Examples
📁 Complete examples: For production-ready, copy-paste examples see the
examples/directory.
Basic Usage (without framework adapters)
import { parentClientManager, ChildClient } from '@quartal/client';
// Parent client using manager
const parentClient = parentClientManager.initializeParentClient(
{
iframeElement: document.getElementById('quartal-iframe') as HTMLIFrameElement,
appPrefix: 'MyApp',
debug: true,
trace: false,
user: {
id: 'user123',
email: '[email protected]',
name: 'John Doe',
phone: '+358401234567'
}
},
{
onInited: (data) => console.log('Child initialized:', data),
onHeightChange: (height) => console.log('Height changed:', height),
onPendingRequest: (pending) => console.log('Pending request:', pending),
onDialogChanged: (isOpen) => console.log('Dialog state:', isOpen),
onAlert: (alert) => console.log('Alert:', alert),
onUrlChange: (url) => console.log('URL changed:', url)
}
);
// Child client
const childClient = new ChildClient({
debug: true,
callbacks: {
onLogout: () => console.log('Logout requested'),
onReload: () => window.location.reload()
}
});Angular Integration
// quartal-iframe.component.ts (Angular Parent using parentClientManager)
import { Component, ElementRef, ViewChild, OnDestroy } from '@angular/core';
import { parentClientManager, ParentClient } from '@quartal/client';
@Component({
selector: 'app-quartal-iframe',
template: '<iframe #quartalIframe id="quartalIframe" [src]="iframeUrl"></iframe>'
})
export class QuartalIframeComponent implements OnDestroy {
@ViewChild('quartalIframe') iframe!: ElementRef<HTMLIFrameElement>;
private parentClient?: ParentClient;
ngAfterViewInit() {
this.initializeClient();
}
private async initializeClient() {
const iframeElement = this.iframe.nativeElement;
try {
this.parentClient = parentClientManager.initializeParentClient(
{
iframeElement,
appPrefix: 'MyApp',
debug: false,
trace: false,
showNavigation: false,
showTopNavbar: false,
showFooter: false,
showLoading: false,
showMessages: false,
customActions: this.getCustomActions(),
customEntities: this.getCustomEntities(),
customTabs: this.getCustomTabs(),
customTables: this.getCustomTables(),
user: this.user ? {
id: String(this.user.id ?? this.user.username),
email: this.user.email ?? this.user.username,
name: this.user.name,
phone: this.user.phone ||
} : undefined
},
{
onInited: (data) => {
console.log('(MyApp) Child initialized:', data);
this.openUrl(this.router.routerState.snapshot.url);
},
onHeightChange: (height: number) => {
this.handleHeightChange(height);
},
onPendingRequest: (pending: boolean) => {
this.httpStatus.setPendingRequests(pending);
},
onDialogChanged: (dialogState: any) => {
this.handleDialogChanged(dialogState);
},
onAlert: (alert: any) => {
this.handleAlert(alert);
},
onUrlChange: (url: string) => {
this.handleUrlChange(url);
},
onMouseClick: () => {
this.iframeService.setClick();
},
onCustomAction: (action: string, data: any) => {
this.openParentAction(action, data);
},
onError: (error: Error) => {
console.error('(MyApp) Parent client error:', error);
}
}
);
// Store globally for service access
this.iframeService.setEmbedContainer(this.parentClient);
} catch (error) {
console.error('(MyApp) Failed to initialize parent client:', error);
}
}
private handleHeightChange(height: number) {
if (!this.dialogOpened) {
setTimeout(() => {
this.height = height + 'px';
}, 0);
}
}
private handleDialogChanged(dialogState: any) {
if (dialogState.opened) {
this.renderer.addClass(document.body, 'dialog-opened');
this.originalHeight = this.height && this.height != '0px' ? this.height : '100vh';
this.height = '100vh';
this.dialogOpened = true;
} else {
this.renderer.removeClass(document.body, 'dialog-opened');
this.height = this.originalHeight;
this.dialogOpened = false;
}
}
ngOnDestroy() {
// Unregister from parent client manager
parentClientManager.unregisterIframeComponent();
this.iframeService.setEmbedContainer(null);
}
}
// app.service.ts (Angular Child using Adapter and EventBridge)
import { Injectable } from '@angular/core';
import {
QuartalAngularAdapter,
ChildClient,
QuartalEventBridge,
INTERNAL_EVENTS
} from '@quartal/client';
@Injectable({ providedIn: 'root' })
export class AppService {
private quartalClient?: ChildClient;
private eventBridge = QuartalEventBridge.getInstance('quartal-invoicing-ui');
constructor(
private router: Router
) {}
initializeClient() {
const quartalAdapter = new QuartalAngularAdapter(
this.router
);
this.quartalClient = quartalAdapter.createChildClient({
debug: false,
trace: false,
appPrefix: 'MyApp',
autoConnect: true
}, {
onInited: (data) => {
console.log('(MyApp) Child client initialized:', data);
this.setAppSettings(data);
this.initializeEventListeners();
},
onOpenUrl: (url: string) => {
console.log('(MyApp) Open URL in parent:', url);
this.openUrl(url);
},
onRedirect: (url: string[]) => {
console.log('(MyApp) Redirect URL in parent:', url);
this.redirectTo(url);
},
onLogout: () => {
console.log('(MyApp) Logout from parent');
localStorage.removeItem('idToken');
}
});
// Set up event forwarding for hybrid apps
this.setupEventForwarding();
}
private setupEventForwarding() {
// Forward internal events to parent using imported INTERNAL_EVENTS
this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, (message) => {
if (this.quartalClient?.isReady()) {
this.quartalClient.sendDialogChanged(message.data.isOpen);
}
});
this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_PENDING_REQUEST, (message) => {
if (this.quartalClient?.isReady()) {
this.quartalClient.sendPendingRequest(message.data.pending);
}
});
this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_ALERT, (message) => {
if (this.quartalClient?.isReady()) {
this.quartalClient.sendAlert(message.data);
}
});
this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_TITLE_CHANGE, (message) => {
if (this.quartalClient?.isReady()) {
this.quartalClient.sendTitleChange(message.data);
}
});
this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_MOUSE_CLICK, (message) => {
if (this.quartalClient?.isReady()) {
this.quartalClient.sendMouseClick();
}
});
}
}Vue Integration
// Vue Child using QuartalVueAdapter (recommended pattern)
// useAppService.ts
import { getCurrentInstance } from 'vue';
import { useRouter } from 'vue-router';
import type { Router } from 'vue-router';
import { QuartalVueAdapter } from '@quartal/client';
// Global singleton state
let globalVueAdapter: QuartalVueAdapter | null = null;
let routerInstance: Router | null = null;
function initializeVueAdapter(onReady?: () => void): QuartalVueAdapter {
if (globalVueAdapter) {
console.debug('(MyApp) Vue adapter already initialized');
if (onReady) onReady();
return globalVueAdapter;
}
console.debug('(MyApp) Creating new QuartalVueAdapter...');
try {
// QuartalVueAdapter constructor takes (router)
globalVueAdapter = new QuartalVueAdapter(routerInstance);
// Create child client with callbacks
const childClient = globalVueAdapter.createChildClient({
debug: false,
trace: false,
appPrefix: 'QA',
autoConnect: true
}, {
onInited: (data) => {
console.debug('(QA) Child client initialized:', data);
// Handle app settings from parent
if (onReady) onReady();
},
onOpenUrl: (url: string) => {
console.log('(QA) Open URL in parent:', url);
if (routerInstance) {
routerInstance.push(url);
}
},
onRedirect: (url: string[]) => {
console.log('(QA) Redirect URL in parent:', url);
if (routerInstance) {
routerInstance.push(url.join('/'));
}
},
onLogout: () => {
console.log('(QA) Logout from parent');
localStorage.removeItem('idToken');
}
});
console.debug('(MyApp) QuartalVueAdapter initialized successfully');
return globalVueAdapter;
} catch (error) {
console.error('(MyApp) Error initializing QuartalVueAdapter:', error);
throw error;
}
}
// Main composable function
export function useAppService(onReady?: () => void) {
// Get router instance if we're in a Vue component context
const currentInstance = getCurrentInstance();
if (currentInstance && !routerInstance) {
try {
routerInstance = useRouter();
} catch (error) {
console.warn('(MyApp) Could not get router instance:', error);
}
}
// Initialize or update the adapter
const adapter = initializeVueAdapter(onReady);
return {
adapter,
openUrl: (url: string) => {
const client = adapter?.getChildClient();
if (client) {
try {
client.navigate(url);
} catch (error) {
console.warn('(MyApp) Navigation failed:', error);
// Fallback: send as fast action to parent
client.sendOpenFastAction({ link: url });
}
} else {
console.warn('(MyApp) No adapter available for openUrl');
}
},
notifyRouteChange: (url: string) => {
const client = adapter?.getChildClient();
if (client && typeof client.sendUrlChange === 'function') {
client.sendUrlChange(url);
}
},
setMouseClicked: () => {
const client = adapter?.getChildClient();
if (client && typeof client.sendMouseClick === 'function') {
client.sendMouseClick();
}
},
destroy: () => {
if (globalVueAdapter) {
globalVueAdapter.destroy();
globalVueAdapter = null;
}
}
};
}
// Usage in Vue Component
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { useSession } from '@quartal/ui-accounting';
import { useAppService } from './services/useAppService';
const { user } = useSession();
// Initialize app service with Vue adapter
let appService = useAppService(() => {
console.log('(MyApp) Vue child client ready');
// Expose the client globally after it's ready
if (appService.client) {
(window as any).quartalClient = appService.client;
(window as any).childClient = appService.client;
}
});
</script>QuartalEventBridge - For Hybrid Applications
Hybrid applications (that act as both parent and child) can use QuartalEventBridge for internal event forwarding.
Use Cases
- quartal-invoicing-ui: iframe.component (parent) → app.service (child) → partner application
- Future hybrid applications
Basic Example
import { QuartalEventBridge, INTERNAL_EVENTS } from '@quartal/client';
// Create or get instance for application
const eventBridge = QuartalEventBridge.getInstance('partner-app');
// Parent component sends events
export class IFrameComponent {
private eventBridge = QuartalEventBridge.getInstance('partner-app');
private handleAlert(alert: any) {
// Local handling first
this.showLocalAlert(alert);
// Forward to child component internally
this.eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_ALERT, alert, 'iframe-component');
}
}
// Child component listens and forwards upward
export class AppService {
private eventBridge = QuartalEventBridge.getInstance('partner-app');
private unsubscribers: (() => void)[] = [];
constructor() {
this.setupEventForwarding();
}
private setupEventForwarding() {
// Listen to internal events and forward to parent
this.unsubscribers.push(
this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_ALERT, (message) => {
if (this.quartalClient?.isReady()) {
this.quartalClient.sendAlert(message.data);
}
})
);
this.unsubscribers.push(
this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, (message) => {
if (this.quartalClient?.isReady()) {
this.quartalClient.sendDialogChanged(message.data.isOpen);
}
})
);
}
destroy() {
// Cleanup listeners
this.unsubscribers.forEach(unsub => unsub());
}
}Named Instances
// Different instances for different applications
const partnerAppBridge = QuartalEventBridge.getInstance('partner-app');
const invoicingBridge = QuartalEventBridge.getInstance('quartal-invoicing-ui');
const embedBridge = QuartalEventBridge.getInstance('quartal-accounting-embed');
// List active instances
console.log(QuartalEventBridge.getActiveInstances());
// ['quartal-invoicing-ui', 'quartal-accounting-embed']
// Destroy specific instance
QuartalEventBridge.destroyInstance('quartal-accounting-embed');Event Types
// Supported event types - INTERNAL_EVENTS values or custom strings
type EventType = keyof typeof INTERNAL_EVENTS | string;
// Sending events
eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, { isOpen: true }, 'iframe-component');
eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_PENDING_REQUEST, { pending: false }, 'http-interceptor');
eventBridge.sendEvent('custom', { repositoryId: '2025' }, 'period-selector');
// Listening to events
eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, (message) => {
console.log('Dialog event:', message.data, 'from:', message.source);
});
eventBridge.on('custom', (message) => {
if (message.data.repositoryId) {
console.log('Repository changed to:', message.data.repositoryId);
}
});Height Detection and Reporting
Automatic height detection for smooth iframe resizing:
// In child application
function setupHeightDetection(client: ChildClient) {
let lastHeight = 0;
let debounceTimer: any = null;
const detectHeight = () => {
const currentHeight = document.body.scrollHeight;
// Only update if height changed significantly (5px threshold)
if (Math.abs(currentHeight - lastHeight) > 5) {
clearTimeout(debounceTimer);
// Immediate update for significant increases (new content)
if (currentHeight > lastHeight + 50) {
lastHeight = currentHeight;
client.sendHeightChange(currentHeight);
return;
}
// Debounced update for smaller changes
debounceTimer = setTimeout(() => {
lastHeight = currentHeight;
client.sendHeightChange(currentHeight);
}, 800);
}
};
// Monitor DOM changes
const observer = new MutationObserver(detectHeight);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class']
});
window.addEventListener('resize', detectHeight);
// Initial detection
detectHeight();
}HTTP Status Tracking
Track pending requests and notify parent:
// HTTP status composable (Vue)
export function useHttpStatus() {
const pendingRequests = ref(new Set<string>());
const pendingRequest = computed(() => pendingRequests.value.size > 0);
function addRequest(id: string) {
pendingRequests.value.add(id);
}
function removeRequest(id: string) {
pendingRequests.value.delete(id);
}
return {
pendingRequest: readonly(pendingRequest),
addRequest,
removeRequest
};
}
// HTTP interceptor setup
export function setupHttpInterceptor() {
const { addRequest, removeRequest } = useHttpStatus();
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const requestId = `fetch_${Date.now()}_${Math.random()}`;
addRequest(requestId);
try {
const response = await originalFetch(...args);
return response;
} finally {
removeRequest(requestId);
}
};
}Events
Parent → Child (Most Common)
CHILD_PARENT_READY- Parent is readyCHILD_PARENT_LOGOUT- Logout requestCHILD_PARENT_RELOAD- Reload requestCHILD_PARENT_OPEN_URL- Navigate to URLCHILD_PARENT_REDIRECT- Redirect to URLCHILD_PARENT_FAST_ACTIONS- Fast actions available
Child → Parent (Most Common)
PARENT_CHILD_READY- Child is readyPARENT_CHILD_INIT- Child initializedPARENT_CHILD_URL_CHANGE- URL changedPARENT_CHILD_HEIGHT_CHANGE- Height changedPARENT_CHILD_PENDING_REQUEST- HTTP request statusPARENT_CHILD_DIALOG_CHANGED- Dialog state changedPARENT_CHILD_MOUSE_CLICK- Mouse activityPARENT_CHILD_ALERT- Alert messagePARENT_CHILD_ERROR- Error occurred
Note: This list shows the most commonly used events. For a complete list of all available events, see the
QUARTAL_EVENTSconstant in the TypeScript definitions.
Debug Logging
// Enable debug logging with debug: true
const client = new ParentClient({
debug: true,
trace: true, // More detailed logging
instanceName: 'MyApp' // App prefix for logging
});
// Logs show: (MyApp) [inst_1] [DEBUG] MessageNamed Instances
// One instance per instanceName
const client1 = new ParentClient({ instanceName: 'App1', ... });
const client2 = new ParentClient({ instanceName: 'App1', ... }); // Returns same instance
const client3 = new ParentClient({ instanceName: 'App2', ... }); // New instanceDevelopment
# Install dependencies
pnpm install
# Build TypeScript
pnpm build
# Test
pnpm test
# Lint
pnpm lintPublishing
Full Deployment (Recommended for Monorepo)
For complete deployment to both git subtree and npm registry (works only in monorepo environment):
# Deploy to both repositories (recommended workflow in monorepo)
./scripts/deploy-bridge-client.sh [patch|minor|major]
# Preview deployment without making changes
./scripts/deploy-bridge-client.sh patch --dry-run
# Selective deployment
./scripts/deploy-bridge-client.sh patch --git-only # Git subtree only (monorepo only)
./scripts/deploy-bridge-client.sh patch --npm-only # npm registry onlyStandalone Repository Publishing
When bridge-client is cloned as a standalone repository from GitHub:
# Only npm publishing is supported in standalone mode
./scripts/deploy-bridge-client.sh patch --npm-only
# Git operations are automatically disabled in standalone mode
# The script will detect this and only perform npm publishingPackage.json Scripts
# Version-specific deployment (works differently based on environment)
pnpm run deploy:patch # Deploy patch version (git+npm in monorepo, npm-only in standalone)
pnpm run deploy:minor # Deploy minor version (git+npm in monorepo, npm-only in standalone)
pnpm run deploy:major # Deploy major version (git+npm in monorepo, npm-only in standalone)
# Selective deployment
pnpm run deploy:git # Deploy only to git subtree (monorepo only - will fail in standalone)
pnpm run deploy:npm # Deploy only to npm registry (works in both environments)Best Practices
1. Choose the Right Pattern
Parent Applications:
- ✅ Use
parentClientManager.initializeParentClient() - ✅ Framework-agnostic singleton management
- ✅ Handles iframe lifecycle automatically
Angular Child Applications:
- ✅ Use
QuartalAngularAdapter.createChildClient() - ✅ Automatic router and service integration
- ✅ Dependency injection support
Vue Child Applications:
- ✅ Use
QuartalVueAdapter.createChildClient() - ✅ Automatic router and service integration
- ✅ Vue composable pattern, reactive integration
- ✅ Reactive integration with Vue ecosystem
2. Instance Naming
- Use descriptive app prefixes:
'MyApp'(partner's application),'Q'(quartal invoicing),'QA'(quartal accounting) - Consistent naming across all clients in the same application
3. Error Handling
const client = new ChildClient({
callbacks: {
onInited: (config) => {
try {
// Your initialization logic
} catch (error) {
client.sendError(error);
}
}
}
});
// Or with Angular adapter
const adapter = new QuartalAngularAdapter(router);
const client = adapter.createChildClient(config, {
onInited: (data) => {
try {
this.handleInitialization(data);
} catch (error) {
console.error('Initialization failed:', error);
}
}
});4. Cleanup
// Angular parent cleanup
ngOnDestroy() {
// Unregister from parent client manager
parentClientManager.unregisterIframeComponent();
// Clear service references
this.iframeService.setEmbedContainer(null);
// Cleanup event bridges
this.eventBridge?.destroy();
}
// Angular child cleanup
ngOnDestroy() {
// Adapter handles cleanup automatically
this.quartalAdapter?.destroy();
}
// Vue child cleanup
onBeforeUnmount(() => {
childClient?.destroy();
// or if using adapter
adapter?.destroy();
});5. Height Detection
- Use debouncing for performance
- Set meaningful thresholds (5px minimum change)
- Monitor DOM mutations for dynamic content
6. HTTP Status Tracking
- Track all async operations that affect UI state
- Use unique request IDs
- Clean up completed requests immediately
7. Publishing and Versioning
- Always test locally before publishing
- Follow semantic versioning for releases
- Update documentation when adding new features
- Check consuming projects after publishing new versions
- Use
--dry-runflag to preview what would be published
Troubleshooting
Common Issues
Events not received
- Check if client is ready:
client.isReady() - Verify instance names match
- Enable debug logging
- Check if client is ready:
Height not updating
- Ensure MutationObserver is set up
- Check CSS transitions don't interfere
- Verify threshold values
Multiple instances
- Use named instances for isolation
- Clean up properly on component destruction
Debug Tips
// Enable detailed logging
const client = parentClientManager.initializeParentClient({
iframeElement: document.getElementById('quartal-iframe'),
appPrefix: 'MyApp',
debug: true,
trace: true
}, {
onError: (error) => console.error('(MyApp) Parent client error:', error)
});
// Check client state
console.log('Client state:', client.getState());
console.log('Is ready:', client.isReady());
console.log('Is connected:', client.isConnected());
// Monitor events
QuartalEventBridge.getInstance('your-app').on('*', (message) => {
console.log('Event:', message.type, message.data, 'from:', message.source);
});API Reference
💡 Production Examples: For complete, copy-paste ready implementations, see
examples/directory with Angular and Vue examples.
ParentClientManager
import { parentClientManager } from '@quartal/client';
// Initialize parent client with manager
const parentClient = parentClientManager.initializeParentClient(
config: ParentClientConfig,
callbacks: ParentClientCallbacks
): ParentClient;
interface ParentClientConfig {
iframeElement: HTMLIFrameElement;
appPrefix?: string; // Application prefix for logging (e.g., 'MyApp', 'QA')
debug?: boolean;
trace?: boolean;
showNavigation?: boolean;
showTopNavbar?: boolean;
showFooter?: boolean;
showLoading?: boolean;
showMessages?: boolean;
customActions?: CustomActions;
customEntities?: CustomEntity[];
customTabs?: CustomTabs;
customTables?: CustomTables;
user?: {
id: string;
email: string;
name: string;
};
}
interface ParentClientCallbacks {
onInited?: (data: any) => void;
onFastActions?: (actions: any[]) => void;
onFastActionCallback?: (callback: any) => void;
onUrlChange?: (url: string) => void;
onHeightChange?: (height: number) => void;
onAlert?: (alert: any) => void;
onTitleChange?: (title: string) => void;
onMouseClick?: () => void;
onFetchData?: (request: any) => void;
onError?: (error: Error) => void;
onPendingRequest?: (pending: boolean) => void;
onDialogChanged?: (dialogState: any) => void;
onDataChanged?: (data: any) => void;
onUserNotifications?: (notifications: any[]) => void;
onMakePayment?: (payment: any) => void;
onCustomAction?: (action: string, data: any) => void;
}
// Manager methods
parentClientManager.unregisterIframeComponent(): void; // Cleanup on component destroyParentClient (Direct Usage)
interface ParentClientConfig {
iframeElement: HTMLIFrameElement;
debug?: boolean;
trace?: boolean;
instanceName?: string; // For named instances in hybrid apps
callbacks?: {
onReady?: () => void;
onHeightChange?: (height: number) => void;
onPendingRequest?: (pending: boolean) => void;
onDialogChanged?: (isOpen: boolean) => void;
onMouseClick?: () => void;
onUrlChange?: (url: string) => void;
onAlert?: (alert: any) => void;
onError?: (error: Error) => void;
};
}
class ParentClient {
constructor(config: ParentClientConfig);
// Core methods
isReady(): boolean;
isConnected(): boolean;
destroy(): void;
// Adapter management
setAdapters(adapters: Record<string, any>): void;
// Communication methods
sendToChild(message: any): void;
// State management
getState(): ParentClientState;
}ChildClient
interface ChildClientConfig {
debug?: boolean;
trace?: boolean;
instanceName?: string; // For named instances in hybrid apps
callbacks?: {
onInited?: (config: any) => void;
onLogout?: () => void;
onReload?: () => void;
onNavigate?: (url: string) => void;
};
}
class ChildClient {
constructor(config: ChildClientConfig);
// Core methods
isReady(): boolean;
isConnected(): boolean;
destroy(): void;
// Adapter management
setAdapters(adapters: Record<string, any>): void;
// Communication methods
sendInited(data: any): void;
sendHeightChange(height: number): void;
sendPendingRequest(pending: boolean): void;
sendDialogChanged(isOpen: boolean): void;
sendMouseClick(): void;
sendUrlChange(url: string): void;
sendAlert(alert: any): void;
sendMakePayment(payment: any): void;
sendError(error: Error): void;
navigate(url: string): void;
// State management
getState(): ChildClientState;
}Adapters
// Angular Adapter (Used in child applications)
import { QuartalAngularAdapter } from '@quartal/client';
const adapter = new QuartalAngularAdapter(router);
// Create child client with framework integration
const childClient = adapter.createChildClient({
debug: false,
trace: false,
appPrefix: 'Q'
}, {
onInited: (data) => console.log('(MyApp) Child initialized:', data),
onLogout: () => this.authService.logout()
});
// Vue Router Adapter (Used directly or via QuartalVueAdapter)
class VueRouterAdapter {
constructor(router: Router);
onUrlChange(callback: (url: string) => void): () => void;
}
// Vue Adapter (Optional pattern)
import { QuartalVueAdapter } from '@quartal/client';
const adapter = new QuartalVueAdapter(router);
const childClient = adapter.createChildClient(config, callbacks);
// Angular Router Adapter (Used internally by QuartalAngularAdapter)
class AngularRouterAdapter {
constructor(router: Router);
onUrlChange(callback: (url: string) => void): Subscription;
}License
MIT
Contributing
We welcome contributions! Please see CONTRIBUTING.md for development guidelines.
- Fork the repository
- Create a feature branch
- Make your changes and add tests
- Submit a pull request
For Maintainers
Internal deployment notes can be found in package.json scripts.
Changelog
v1.0.0
- 🎉 Initial release
- ✨ Basic parent-child communication
- ✨ Angular and Vue adapters
- ✨ QuartalEventBridge for hybrid applications
- ✨ Named instances support
- ✨ Automatic height detection and reporting
- ✨ HTTP status tracking
- 🐛 Debug logging
