@quartal/bridge-client
Version:
Universal client library for embedding applications with URL-configurable transport support (iframe, postMessage) and framework adapters for Angular and Vue
492 lines (425 loc) • 13.1 kB
text/typescript
// Example: Generic Vue parent component using ParentClientManager
// This example shows how to implement a parent component that embeds child applications
// Based on angular-iframe-with-manager.example.ts but adapted for Vue 3 Composition API
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { parentClientManager, ParentClient } from '@quartal/client'
// Define interfaces for type safety
interface User {
id: string;
email: string;
name: string;
}
interface CustomAction {
id: string;
name: string;
icon?: string;
[key: string]: any;
}
interface IframeConfig {
appPrefix?: string;
showNavigation?: boolean;
showTopNavbar?: boolean;
showFooter?: boolean;
showLoading?: boolean;
showMessages?: boolean;
user?: User;
customActions?: CustomAction[];
customEntities?: any[];
customTabs?: any;
customTables?: any;
}
// Composable for managing iframe with parent client
export function useParentIframe(config: {
iframeUrl: string;
iframeConfig?: Partial<IframeConfig>;
onReady?: () => void;
onError?: (error: any) => void;
}) {
// Reactive state
const iframeRef = ref<HTMLIFrameElement | null>(null)
const parentClient = ref<ParentClient | null>(null)
const height = ref<string | number>('100vh')
const isInitialized = ref(false)
const isNavigatingFromChild = ref(false)
const alive = ref(true)
// Router for navigation handling
const router = useRouter()
// Computed properties
const url = computed(() => {
return config.iframeUrl || 'https://your-embedded-app.com'
})
const clientStats = computed(() => {
if (!parentClient.value) return null
return parentClientManager.getStats()
})
// Initialize iframe after component is mounted
const initializeIframe = async () => {
await nextTick() // Ensure DOM is ready
const iframeElement = iframeRef.value
if (!iframeElement) {
console.warn('Vue iframe element not available')
return
}
// Check if there's already a client from the manager
const existingClient = parentClientManager.getParentClient()
if (existingClient) {
// Update the iframe element reference in the existing client
const updateSuccess = parentClientManager.updateIframeElement(iframeElement)
if (updateSuccess) {
parentClient.value = existingClient
// Register this component with the manager
parentClientManager.registerIframeComponent()
// Since we're reusing the client, manually trigger navigation to current URL
openUrl(router.currentRoute.value.fullPath)
return
}
}
try {
// Default configuration
const defaultConfig: IframeConfig = {
appPrefix: 'APP',
showNavigation: false,
showTopNavbar: false,
showFooter: false,
showLoading: false,
showMessages: false,
user: getCurrentUser(),
customActions: getCustomActions(),
customEntities: [],
customTabs: {},
customTables: {}
}
// Merge with provided config
const finalConfig = { ...defaultConfig, ...config.iframeConfig }
// Create the parent client using ParentClientManager
parentClient.value = parentClientManager.initializeParentClient(
{
iframeElement,
...finalConfig
},
{
onInited: (data) => {
console.log('Vue parent client inited with data:', data)
isInitialized.value = true
openUrl(router.currentRoute.value.fullPath)
config.onReady?.()
},
onFastActions: (actions) => handleFastActions(actions),
onUrlChange: (url) => handleUrlChange(url),
onHeightChange: (newHeight) => handleHeightChange(newHeight),
onAlert: (alert) => handleAlert(alert),
onTitleChange: (title) => handleTitleChange(title),
onError: (error) => {
console.error('Vue parent client error:', error)
config.onError?.(error)
},
onMouseClick: () => handleMouseClick(),
onPendingRequest: (pending) => handlePendingRequest(pending),
onDialogChanged: (dialogState) => handleDialogChanged(dialogState)
}
)
console.log('Vue parent client initialized successfully')
console.log('Parent client manager stats:', parentClientManager.getStats())
} catch (error) {
console.error('Failed to initialize Vue parent client:', error)
config.onError?.(error)
}
}
// Navigation handling
const openUrl = (url: string) => {
if (!parentClient.value || !alive.value) {
return
}
// Transform parent URL to child route
const childPath = transformUrlToChildPath(url)
parentClient.value.redirect([childPath])
}
const transformUrlToChildPath = (url: string): string => {
// Example: Transform '/app/dashboard' to '/dashboard'
// Customize this based on your URL structure
return url.replace('/app', '') || '/'
}
const transformChildPathToUrl = (childPath: string): string => {
// Example: Transform '/dashboard' to '/app/dashboard'
// Customize this based on your URL structure
return '/app' + (childPath.startsWith('/') ? childPath : '/' + childPath)
}
// Event handlers
const handleFastActions = (actions: CustomAction[]) => {
console.log('Vue fast actions received:', actions)
// Handle fast actions from child
// You can emit events or update reactive state here
}
const handleUrlChange = (url: string) => {
console.log('Vue URL change from child:', url)
// Set flag to prevent sending openUrl back to child
isNavigatingFromChild.value = true
// Navigate in parent application
const parentUrl = transformChildPathToUrl(url)
router.push(parentUrl)
// Reset flag after navigation
setTimeout(() => {
isNavigatingFromChild.value = false
}, 0)
}
const handleHeightChange = (newHeight: number) => {
console.log('Vue height change from child:', newHeight)
// Update iframe height
setTimeout(() => {
height.value = newHeight + 'px'
}, 0)
}
const handleAlert = (alert: any) => {
console.log('Vue alert from child:', alert)
// Handle alerts (success, error, warning, info)
// Integrate with your Vue notification system
// Example: useToast().show(alert.message, alert.type)
}
const handleTitleChange = (title: string) => {
console.log('Vue title change from child:', title)
// Update page title or use Vue's useMeta/useHead
document.title = title
}
const handleMouseClick = () => {
// Handle mouse clicks from child
// Useful for closing dropdowns, modals, etc.
console.log('Vue mouse click from child')
}
const handlePendingRequest = (pending: boolean) => {
console.log('Vue pending request state:', pending)
// Show/hide loading indicators
// You can emit events or update reactive state here
}
const handleDialogChanged = (dialogState: any) => {
console.log('Vue dialog state changed:', dialogState)
// Handle dialog state changes from child
}
// Helper functions (customize these for your app)
const getCurrentUser = (): User => {
// Return current user data if available
return {
id: 'user123',
email: 'user@example.com',
name: 'Example User'
}
}
const getCustomActions = (): CustomAction[] => {
// Return custom actions configuration
return []
}
// Watch for route changes to sync with embedded app
const stopRouteWatcher = router.afterEach((to) => {
if (!isNavigatingFromChild.value && parentClient.value && alive.value) {
openUrl(to.fullPath)
}
})
// Lifecycle
onMounted(() => {
initializeIframe()
})
onUnmounted(() => {
// Stop any ongoing operations
alive.value = false
// Stop route watcher
stopRouteWatcher()
// Unregister from the parent client manager (decrements reference count)
parentClientManager.unregisterIframeComponent()
// Clear component reference to parent client (helps with garbage collection)
parentClient.value = null
console.log('Vue parent iframe component unmounted')
})
return {
// Reactive state
iframeRef,
parentClient: computed(() => parentClient.value),
height: computed(() => height.value),
url,
isInitialized: computed(() => isInitialized.value),
clientStats,
// Methods
initializeIframe,
openUrl,
// Event handlers (expose if needed for custom handling)
handleFastActions,
handleUrlChange,
handleHeightChange,
handleAlert,
handleTitleChange
}
}
// Vue component example using the composable
export default {
name: 'GenericParentIframe',
setup() {
// Configuration for the iframe
const iframeConfig = {
iframeUrl: 'https://your-embedded-app.com',
iframeConfig: {
appPrefix: 'MY_APP',
showNavigation: false,
showTopNavbar: false,
user: {
id: 'user123',
email: 'user@example.com',
name: 'Example User'
}
},
onReady: () => {
console.log('Vue iframe is ready!')
},
onError: (error: any) => {
console.error('Vue iframe error:', error)
}
}
// Use the composable
const {
iframeRef,
height,
url,
isInitialized,
clientStats,
openUrl
} = useParentIframe(iframeConfig)
// Example of handling custom actions
const handleCustomAction = (action: string) => {
console.log('Handling custom action:', action)
// Handle actions specific to your app
switch (action) {
case 'refresh':
// Refresh the iframe content
openUrl(useRouter().currentRoute.value.fullPath)
break
case 'navigate':
// Navigate to a specific route
openUrl('/dashboard')
break
default:
console.log('Unknown action:', action)
}
}
return {
iframeRef,
height,
url,
isInitialized,
clientStats,
handleCustomAction
}
},
template: `
<div class="iframe-container">
<div v-if="!isInitialized" class="loading">
Loading embedded application...
</div>
<iframe
ref="iframeRef"
:src="url"
:style="{ height: height }"
:class="{ 'iframe-hidden': !isInitialized }"
frameborder="0"
width="100%"
/>
<!-- Debug info (remove in production) -->
<div v-if="clientStats" class="debug-info">
Client Stats: {{ clientStats }}
</div>
</div>
`,
style: `
<style scoped>
.iframe-container {
position: relative;
width: 100%;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
}
.iframe-hidden {
opacity: 0;
transition: opacity 0.3s ease;
}
iframe {
border: none;
width: 100%;
transition: opacity 0.3s ease;
}
.debug-info {
font-size: 12px;
color: #666;
padding: 8px;
background-color: #f9f9f9;
border-top: 1px solid #eee;
}
</style>
`
}
/*
// Usage example in your Vue app:
// In your main component or page
<template>
<div>
<h1>My Application</h1>
<GenericParentIframe />
</div>
</template>
<script setup>
import GenericParentIframe from './components/GenericParentIframe.vue'
</script>
// Or using the composable directly in a component:
<template>
<div class="custom-iframe-wrapper">
<iframe
ref="iframeRef"
:src="url"
:style="{ height: height }"
v-if="url"
/>
</div>
</template>
<script setup>
import { useParentIframe } from './composables/useParentIframe'
const { iframeRef, height, url } = useParentIframe({
iframeUrl: 'https://my-embedded-app.com',
iframeConfig: {
appPrefix: 'CUSTOM',
user: getCurrentUser()
},
onReady: () => console.log('Ready!'),
onError: (error) => console.error('Error:', error)
})
</script>
// Advanced usage with custom event handling:
<script setup>
const {
iframeRef,
height,
url,
handleFastActions,
handleAlert
} = useParentIframe({
iframeUrl: 'https://my-app.com',
onReady: () => {
// Custom ready logic
}
})
// Override default handlers
const customFastActionsHandler = (actions) => {
// Custom fast actions handling
console.log('Custom fast actions:', actions)
// You can still call the default handler
handleFastActions(actions)
}
const customAlertHandler = (alert) => {
// Custom alert handling with Vue toast/notification
useToast().show(alert.message, alert.type)
handleAlert(alert)
}
</script>
*/