UNPKG

@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
// 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> */