npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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-client

Quick 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 ChildClient

Integration Patterns

1. Parent Applications (Framework-agnostic)

  • Pattern: Use parentClientManager directly
  • 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

  1. Height Changes:

    • quartal-accounting-embed (DOM observer) → childClient.sendHeightChange()
    • → quartal-invoicing-ui (QuartalEventBridge forwarding)
    • → partner-ui (onHeightChange callback)
  2. Dialog States:

    • quartal-accounting-embed (QDialog component) → childClient.sendDialogChanged()
    • → quartal-invoicing-ui (QuartalEventBridge forwarding)
    • → partner-ui (onDialogChanged callback)
  3. HTTP Status:

    • quartal-accounting-embed (HTTP interceptor) → childClient.sendPendingRequest()
    • → quartal-invoicing-ui (QuartalEventBridge forwarding)
    • → partner-ui (onPendingRequest callback)

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 ready
  • CHILD_PARENT_LOGOUT - Logout request
  • CHILD_PARENT_RELOAD - Reload request
  • CHILD_PARENT_OPEN_URL - Navigate to URL
  • CHILD_PARENT_REDIRECT - Redirect to URL
  • CHILD_PARENT_FAST_ACTIONS - Fast actions available

Child → Parent (Most Common)

  • PARENT_CHILD_READY - Child is ready
  • PARENT_CHILD_INIT - Child initialized
  • PARENT_CHILD_URL_CHANGE - URL changed
  • PARENT_CHILD_HEIGHT_CHANGE - Height changed
  • PARENT_CHILD_PENDING_REQUEST - HTTP request status
  • PARENT_CHILD_DIALOG_CHANGED - Dialog state changed
  • PARENT_CHILD_MOUSE_CLICK - Mouse activity
  • PARENT_CHILD_ALERT - Alert message
  • PARENT_CHILD_ERROR - Error occurred

Note: This list shows the most commonly used events. For a complete list of all available events, see the QUARTAL_EVENTS constant 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] Message

Named 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 instance

Development

# Install dependencies
pnpm install

# Build TypeScript
pnpm build

# Test
pnpm test

# Lint
pnpm lint

Publishing

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 only

Standalone 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 publishing

Package.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-run flag to preview what would be published

Troubleshooting

Common Issues

  1. Events not received

    • Check if client is ready: client.isReady()
    • Verify instance names match
    • Enable debug logging
  2. Height not updating

    • Ensure MutationObserver is set up
    • Check CSS transitions don't interfere
    • Verify threshold values
  3. 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 destroy

ParentClient (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.

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes and add tests
  4. 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