@quartal/bridge-client
Version:
Universal client library for embedding applications with URL-configurable transport support (iframe, postMessage) and framework adapters for Angular and Vue
715 lines (594 loc) • 19.6 kB
text/typescript
// Example: Generic Vue child client composable using QuartalVueAdapter
// 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 Vue 3 applications
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import {
ChildClient,
QuartalVueAdapter,
FastAction,
FastActionCallbackResponse,
ParentDataResponse,
ClearStorageRequest
} from '@quartal/client'
// Global state for the child client (similar to Angular app.service.ts)
let globalAccountingClient: ChildClient | null = null
let globalVueAdapter: QuartalVueAdapter | null = null
// Define interfaces for type safety
interface AppSettings {
customActions?: any[];
customEntities?: any[];
customTabs?: any;
customTables?: any[];
[key: string]: any;
}
interface HttpStatusService {
hasPendingRequest(): any; // Observable-like
}
// At the application level (main.ts)
export function setupGlobalChildClient() {
// This should be called once when the app starts
// if it detects it's running in an iframe context
if (window !== window.parent) {
console.log('Vue child app detected iframe context')
// Listen for beforeunload to clean up if needed
window.addEventListener('beforeunload', () => {
// Clean up child client on page unload
if (globalAccountingClient) {
globalAccountingClient.destroy()
globalAccountingClient = null
}
if (globalVueAdapter) {
globalVueAdapter.destroy()
globalVueAdapter = null
}
})
}
}
// Vue useChildClient composable - equivalent to Angular app.service.ts
export function useChildClient(
storageService: StorageService,
httpStatusService?: HttpStatusService
) {
const router = useRouter()
// Reactive state
const isConnected = ref(false)
const isInIframe = ref(window !== window.parent)
const isInitialized = ref(false)
// App settings management
const appSettings = ref<AppSettings | null>(null)
// Fast actions management
const fastAction = ref<FastAction | null>(null)
const fastActionCallback = ref<FastActionCallbackResponse | null>(null)
// Parent data management
const parentData = ref<ParentDataResponse | null>(null)
// Custom entities management
const customEntities = ref<any>(null)
// User notifications (optional)
const userNotifications = ref<any>(null)
// Fast actions array
const fastActions = ref<FastAction[]>([])
// Initialize child client (equivalent to Angular's initializeChildClient)
const initializeChildClient = () => {
if (globalVueAdapter || globalAccountingClient) {
console.log('Child client already initialized')
return
}
if (!isInIframe.value) {
console.log('Running as standalone app, no child client needed')
// Set default app settings for standalone mode
const defaultSettings = getDefaultAppSettings()
setAppSettings(defaultSettings)
return
}
try {
// Create Vue adapter with services
globalVueAdapter = new QuartalVueAdapter(
router,
storageService
)
// Create child client
globalAccountingClient = globalVueAdapter.createChildClient({
debug: false,
trace: false,
appPrefix: 'MyApp',
autoConnect: true
}, {
// Called when parent client sends initialization data
onInited: (data: any) => {
console.log('Child client inited with data:', data)
handleInitData(data)
},
// Parent requests URL change in child
onOpenUrl: (url: string) => {
console.log('Parent requested URL change:', url)
handleUrlChange(url)
},
// Parent requests navigation with redirect
onRedirect: (url: string[]) => {
console.log('Parent requested redirect:', url)
handleRedirect(url)
},
// Parent sends action to execute
onOpenAction: (data: any) => {
console.log('Parent sent action:', data)
handleAction(data)
},
// Parent requests cache clear
onClearCache: (data: any) => {
console.log('Parent requested cache clear:', data)
handleClearCache(data)
},
// Parent requests storage clear
onClearStorage: (request: ClearStorageRequest) => {
console.log('Parent requested storage clear:', request)
handleClearStorage(request)
},
// Parent requests logout
onLogout: () => {
console.log('Parent requested logout')
handleLogout()
},
// Parent sends data
onFetchedData: (data: any) => {
console.log('Parent sent data:', data)
setParentData(data)
}
})
console.log('Child client initialized successfully')
isConnected.value = true
} catch (error) {
console.error('Failed to initialize child client:', error)
isConnected.value = false
}
}
// Handle initialization data from parent
const handleInitData = (data: any) => {
// Get app settings from parent or use defaults
const settings: AppSettings = globalAccountingClient?.getAppSettings() || getDefaultAppSettings()
// Merge custom data from parent
settings.customActions = data.customActions || []
settings.customEntities = data.customEntities || []
settings.customTabs = data.customTabs || {}
settings.customTables = data.customTables || []
setAppSettings(settings)
console.log('App settings updated:', settings)
// Initialize event listeners after client is ready
initializeEventListeners()
}
// Initialize event listeners (similar to Angular service)
const initializeEventListeners = () => {
if (!globalAccountingClient || !isInIframe.value || isInitialized.value) {
return
}
isInitialized.value = true
let firstNavigation = true
// Track navigation changes and send to parent
router.afterEach((to) => {
// Skip first navigation to avoid redundant initial URL change
if (!firstNavigation) {
const url = to.fullPath
// Skip certain URLs that shouldn't be sent to parent
if (shouldSkipUrlChange(url)) {
return
}
globalAccountingClient!.sendUrlChange(url)
} else {
firstNavigation = false
}
})
// Track pending HTTP requests (if service provided)
if (httpStatusService?.hasPendingRequest) {
// Assuming it's an observable-like object
httpStatusService.hasPendingRequest().subscribe?.((pending: boolean) => {
if (globalAccountingClient && isInIframe.value) {
globalAccountingClient.sendPendingRequest(pending)
}
})
}
// Track storage changes and sync to parent
if (storageService.dataUpdated$?.subscribe) {
storageService.dataUpdated$.subscribe(({ key, value }: { key: string; value: any }) => {
console.log('Storage updated:', key, value)
// Skip sensitive data that shouldn't be synced
if (shouldSkipStorageSync(key)) {
return
}
if (globalAccountingClient && isInIframe.value) {
globalAccountingClient.sendDataChanged(key, JSON.stringify(value))
}
})
}
// Track fast action callbacks
watch(fastActionCallback, (callback) => {
if (callback && globalAccountingClient && isInIframe.value) {
globalAccountingClient.sendFastActionCallback(callback)
}
})
// Track user notifications
watch(userNotifications, (notifications) => {
if (notifications && globalAccountingClient && isInIframe.value) {
globalAccountingClient.sendUserNotifications(notifications)
}
})
// Initialize user-specific data
initUserSpecificData()
// Signal to parent that child is ready
setInitialized(true)
}
// Handle URL change from parent
const handleUrlChange = (url: string) => {
if (!url) {
router.push('/')
return
}
if (url.includes('?')) {
// Handle URLs with query parameters
const [path, queryString] = url.split('?')
const queryParams = new URLSearchParams(queryString)
const query: any = {}
queryParams.forEach((value, key) => {
query[key] = value
})
router.push({ path, query })
} else {
router.push(url)
}
// Update iframe size after navigation
updateIframeSize()
}
// Handle redirect from parent
const handleRedirect = (route: string[]) => {
// Clear cache before redirect
handleClearCache(null)
// Navigate with redirect pattern to ensure clean state
router.push('/redirect').then(() => {
router.push(route.join('/'))
}).catch(console.error)
}
// Handle action from parent
const handleAction = (data: any) => {
if (data.action) {
fastAction.value = data
}
}
// Handle cache clear from parent
const handleClearCache = (data: any) => {
console.log('Clearing application caches...', data)
// Clear any caches your app uses
clearApplicationCaches()
}
// Handle storage clear from parent
const handleClearStorage = (request: ClearStorageRequest) => {
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
clearApplicationStorage(request)
}
// Handle logout from parent
const handleLogout = () => {
localStorage.removeItem('idToken')
localStorage.removeItem('accessToken')
// Clear any other authentication data
clearAuthenticationData()
// Navigate to login or home
router.push('/')
}
// Public API methods
const setAppSettings = (settings: AppSettings) => {
if (!settings) return
appSettings.value = settings
}
const setFastAction = (action: FastAction) => {
if (action) {
console.log('Setting fast action:', action)
fastAction.value = action
}
}
const setFastActionCallback = (callback: FastActionCallbackResponse) => {
if (callback) {
console.log('Setting fast action callback:', callback)
fastActionCallback.value = callback
}
}
const setParentData = (data: ParentDataResponse) => {
if (data) {
console.log('Setting parent data:', data)
parentData.value = data
}
}
const setUserNotifications = (notifications: any) => {
if (notifications) {
userNotifications.value = notifications
}
}
const setInitialized = (initialized: boolean) => {
console.log('Setting initialized state:', initialized)
if (globalAccountingClient) {
globalAccountingClient.sendInited({
inited: initialized,
version: getAppVersion()
})
}
}
// Update iframe size after content changes
const updateIframeSize = () => {
if (!isInIframe.value) return
// Initial size update
setTimeout(() => {
const height = document.body.scrollHeight
setIframeSize(height)
})
// Secondary update for dynamic content
setTimeout(() => {
const newHeight = document.body.scrollHeight
setIframeSize(newHeight)
}, 400)
}
const setIframeSize = (height: number) => {
if (globalAccountingClient && isInIframe.value) {
globalAccountingClient.sendHeightChange(height)
}
}
// Send dialog state changes to parent
const sendDialogState = (isOpen: boolean) => {
if (globalAccountingClient && isInIframe.value) {
globalAccountingClient.sendDialogChanged(isOpen)
}
}
// Send fast actions to parent
const sendFastActions = (actions: FastAction[]) => {
if (globalAccountingClient && isInIframe.value) {
globalAccountingClient.sendFastActions(actions)
}
}
// Send pending request state to parent
const sendPendingRequest = (pending: boolean) => {
if (globalAccountingClient && isInIframe.value) {
globalAccountingClient.sendPendingRequest(pending)
}
}
// Get client instance
const getChildClient = () => globalAccountingClient
const getVueAdapter = () => globalVueAdapter
// Computed properties for reactive state
const clientStatus = computed(() => ({
isInitialized: !!globalAccountingClient,
isConnected: isConnected.value,
isInIframe: isInIframe.value
}))
// Enhanced cleanup function
const cleanup = () => {
console.log('Cleaning up useChildClient')
// Clean up child client
if (globalAccountingClient) {
globalAccountingClient.destroy()
globalAccountingClient = null
}
if (globalVueAdapter) {
globalVueAdapter.destroy()
globalVueAdapter = null
}
// Reset reactive state
isConnected.value = false
}
// Helper functions - override these in your implementation
const getDefaultAppSettings = (): AppSettings => {
return {
customActions: [],
customEntities: [],
customTabs: {},
customTables: []
}
}
const getAppVersion = (): string => {
return '1.0.0' // Replace with your app version
}
const 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))
}
const shouldSkipStorageSync = (key: string): boolean => {
// Skip sensitive data that shouldn't be synced to parent
const skipKeys = ['idToken', 'accessToken', 'refreshToken']
return skipKeys.includes(key)
}
const clearApplicationCaches = () => {
// Implement your app-specific cache clearing logic
console.log('Clearing application-specific caches...')
}
const clearApplicationStorage = (request: ClearStorageRequest) => {
// Implement your app-specific storage clearing logic
console.log('Clearing application-specific storage...', request)
}
const clearAuthenticationData = () => {
// Clear your app-specific authentication data
console.log('Clearing authentication data...')
}
const initUserSpecificData = () => {
// Initialize user-specific data after client is ready
// Example: Load user notifications, fast actions, etc.
setTimeout(() => {
loadUserNotifications()
loadFastActions()
}, 500)
}
const loadUserNotifications = () => {
// Load and send user notifications if applicable
console.log('Loading user notifications...')
}
const loadFastActions = () => {
// Load and send fast actions if applicable
console.log('Loading fast actions...')
// Example:
// getFastActions().then(actions => {
// fastActions.value = actions
// sendFastActions(actions)
// })
}
// Lifecycle hooks
onMounted(() => {
initializeChildClient()
})
onUnmounted(() => {
cleanup()
})
return {
// State
isConnected: computed(() => isConnected.value),
isInIframe: computed(() => isInIframe.value),
clientStatus,
appSettings: computed(() => appSettings.value),
fastAction: computed(() => fastAction.value),
fastActionCallback: computed(() => fastActionCallback.value),
parentData: computed(() => parentData.value),
customEntities: computed(() => customEntities.value),
userNotifications: computed(() => userNotifications.value),
fastActions: computed(() => fastActions.value),
// Methods
initializeChildClient,
getChildClient,
getVueAdapter,
cleanup,
// Public API
setAppSettings,
setFastAction,
setFastActionCallback,
setParentData,
setUserNotifications,
updateIframeSize,
sendDialogState,
sendFastActions,
sendPendingRequest
}
}
// Usage example in your Vue app:
/*
// In your main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { setupGlobalChildClient } from './composables/useChildClient'
const app = createApp(App)
// Setup global child client detection
setupGlobalChildClient()
app.mount('#app')
// In your main App.vue component
<template>
<div class="app">
<div v-if="!isConnected && isInIframe" class="loading">
Connecting to parent application...
</div>
<div v-if="isConnected || !isInIframe">
<!-- Your app content -->
<router-view />
</div>
<!-- Debug info (remove in production) -->
<div v-if="clientStatus" class="debug-info">
Client Status: {{ clientStatus }}
</div>
</div>
</template>
<script setup>
import { useChildClient } from './composables/useChildClient'
import { storageService, httpStatusService } from './services'
const {
isConnected,
isInIframe,
clientStatus,
appSettings,
fastAction,
setFastActionCallback,
updateIframeSize,
sendDialogState
} = useChildClient(storageService, httpStatusService)
// Listen for app settings changes
watch(appSettings, (settings) => {
if (settings) {
console.log('App settings updated:', settings)
// Update your app based on settings
}
})
// Listen for fast actions
watch(fastAction, (action) => {
if (action) {
console.log('Fast action received:', action)
// Handle the action in your app
handleFastAction(action)
}
})
// Example: Handle fast action
const handleFastAction = (action) => {
switch (action.type) {
case 'refresh':
// Refresh data
break
case 'save':
// Save current state
break
default:
console.log('Unknown action:', action)
}
// Send callback to parent
setFastActionCallback({
success: true,
data: { message: 'Action completed successfully' }
})
}
// Example: Track content changes
const onContentChanged = () => {
// Update iframe size when content changes
updateIframeSize()
}
// Example: Track modal/dialog state
const onDialogOpen = () => {
sendDialogState(true)
}
const onDialogClose = () => {
sendDialogState(false)
}
</script>
// In a component that needs to interact with parent
<template>
<div>
<button @click="sendMessageToParent">Send Message</button>
<button @click="notifyContentChange">Content Changed</button>
</div>
</template>
<script setup>
import { useChildClient } from '@/composables/useChildClient'
const { setFastActionCallback, updateIframeSize, isInIframe } = useChildClient(
storageService
)
const sendMessageToParent = () => {
if (isInIframe.value) {
setFastActionCallback({
success: true,
data: { message: 'Hello from Vue child!' }
})
}
}
const notifyContentChange = () => {
updateIframeSize()
}
</script>
// Advanced usage with custom services
export class CustomStorageService implements StorageService {
dataUpdated$ = reactive({ subscribe: (callback) => { ... } })
// Your storage implementation
}
// Usage with custom services
const customStorageService = new CustomStorageService()
const { ... } = useChildClient(customStorageService)
*/