Morten Sæther

← Blog

Episode 18: JavaScript Frameworks Integration

4 May 2026

Building Modern Interactive Experiences with React, Vue.js, and Angular

Introduction

Modern web applications demand rich, interactive user experiences that traditional server-side rendering alone cannot deliver. JavaScript frameworks like React, Vue.js, and Angular provide powerful tools for creating dynamic, responsive interfaces within Power Pages portals. This episode teaches you how to successfully integrate these frameworks while maintaining Power Pages’ authentication, security, and data integration capabilities.

Key Concepts

Learning Objectives

By the end of this episode, you will be able to:

Why JavaScript Frameworks Matter

JavaScript frameworks enable creation of modern, app-like experiences within Power Pages portals. They provide powerful state management, component reusability, and rich interactions that significantly enhance user engagement. Proper integration allows you to leverage the best of both worlds: Power Pages’ robust backend capabilities with modern frontend development patterns.

Step-by-Step Guide

1. React Integration with Power Pages

Setting Up React Components in Power Pages:

<!-- React Component Container -->
<div id="react-dashboard-app" 
     data-user-id="{{ user.id }}" 
     data-api-base="{{ site.api_base_url }}"
     data-csrf-token="{{ csrf_token }}">
    <!-- Server-side fallback content -->
    <div class="loading-state">
        <div class="spinner"></div>
        <p>Loading dashboard...</p>
    </div>
</div>

<!-- React Application Script -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

<script type="text/babel">
    // Power Pages React Integration
    const { useState, useEffect, useCallback } = React;
    
    // Dashboard Component
    function Dashboard({ userId, apiBase, csrfToken }) {
        const [dashboardData, setDashboardData] = useState(null);
        const [loading, setLoading] = useState(true);
        const [error, setError] = useState(null);
        
        // API Client with Power Pages integration
        const apiClient = useCallback(async (endpoint, options = {}) => {
            const defaultOptions = {
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': csrfToken,
                    ...options.headers
                },
                credentials: 'same-origin'
            };
            
            const response = await fetch(`${apiBase}${endpoint}`, {
                ...defaultOptions,
                ...options
            });
            
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            
            return response.json();
        }, [apiBase, csrfToken]);
        
        // Load dashboard data
        useEffect(() => {
            async function loadDashboardData() {
                try {
                    setLoading(true);
                    const data = await apiClient(`/users/${userId}/dashboard`);
                    setDashboardData(data);
                } catch (err) {
                    setError(err.message);
                    console.error('Dashboard loading error:', err);
                } finally {
                    setLoading(false);
                }
            }
            
            loadDashboardData();
        }, [userId, apiClient]);
        
        // Task completion handler
        const handleTaskComplete = useCallback(async (taskId) => {
            try {
                await apiClient(`/tasks/${taskId}/complete`, {
                    method: 'POST'
                });
                
                // Update local state
                setDashboardData(prev => ({
                    ...prev,
                    tasks: prev.tasks.map(task => 
                        task.id === taskId 
                            ? { ...task, status: 'completed', completedDate: new Date().toISOString() }
                            : task
                    ),
                    statistics: {
                        ...prev.statistics,
                        completedTasks: prev.statistics.completedTasks + 1,
                        pendingTasks: prev.statistics.pendingTasks - 1
                    }
                }));
                
                // Show success notification
                showNotification('Task completed successfully!', 'success');
            } catch (err) {
                showNotification('Failed to complete task', 'error');
                console.error('Task completion error:', err);
            }
        }, [apiClient]);
        
        if (loading) {
            return (
                <div className="dashboard-loading">
                    <div className="spinner"></div>
                    <p>Loading your dashboard...</p>
                </div>
            );
        }
        
        if (error) {
            return (
                <div className="dashboard-error">
                    <h3>Unable to load dashboard</h3>
                    <p>{error}</p>
                    <button onClick={() => window.location.reload()}>
                        Retry
                    </button>
                </div>
            );
        }
        
        return (
            <div className="react-dashboard">
                <DashboardHeader statistics={dashboardData.statistics} />
                <DashboardContent 
                    tasks={dashboardData.tasks}
                    notifications={dashboardData.notifications}
                    onTaskComplete={handleTaskComplete}
                />
            </div>
        );
    }
    
    // Dashboard Header Component
    function DashboardHeader({ statistics }) {
        return (
            <div className="dashboard-header">
                <h2>Your Dashboard</h2>
                <div className="dashboard-stats">
                    <div className="stat-item">
                        <span className="stat-value">{statistics.pendingTasks}</span>
                        <span className="stat-label">Pending Tasks</span>
                    </div>
                    <div className="stat-item">
                        <span className="stat-value">{statistics.completedTasks}</span>
                        <span className="stat-label">Completed</span>
                    </div>
                    <div className="stat-item">
                        <span className="stat-value">{statistics.unreadNotifications}</span>
                        <span className="stat-label">Notifications</span>
                    </div>
                </div>
            </div>
        );
    }
    
    // Dashboard Content Component
    function DashboardContent({ tasks, notifications, onTaskComplete }) {
        const [activeTab, setActiveTab] = useState('tasks');
        
        return (
            <div className="dashboard-content">
                <div className="dashboard-tabs">
                    <button 
                        className={`tab-button ${activeTab === 'tasks' ? 'active' : ''}`}
                        onClick={() => setActiveTab('tasks')}
                    >
                        Tasks ({tasks.filter(t => t.status === 'pending').length})
                    </button>
                    <button 
                        className={`tab-button ${activeTab === 'notifications' ? 'active' : ''}`}
                        onClick={() => setActiveTab('notifications')}
                    >
                        Notifications ({notifications.filter(n => !n.read).length})
                    </button>
                </div>
                
                <div className="tab-content">
                    {activeTab === 'tasks' && (
                        <TasksList tasks={tasks} onTaskComplete={onTaskComplete} />
                    )}
                    {activeTab === 'notifications' && (
                        <NotificationsList notifications={notifications} />
                    )}
                </div>
            </div>
        );
    }
    
    // Tasks List Component
    function TasksList({ tasks, onTaskComplete }) {
        const pendingTasks = tasks.filter(task => task.status === 'pending');
        
        return (
            <div className="tasks-list">
                {pendingTasks.length === 0 ? (
                    <div className="empty-state">
                        <h4>No pending tasks</h4>
                        <p>Great job! You're all caught up.</p>
                    </div>
                ) : (
                    pendingTasks.map(task => (
                        <TaskItem 
                            key={task.id} 
                            task={task} 
                            onComplete={onTaskComplete}
                        />
                    ))
                )}
            </div>
        );
    }
    
    // Individual Task Component
    function TaskItem({ task, onComplete }) {
        const [completing, setCompleting] = useState(false);
        
        const handleComplete = async () => {
            setCompleting(true);
            try {
                await onComplete(task.id);
            } finally {
                setCompleting(false);
            }
        };
        
        return (
            <div className="task-item">
                <div className="task-info">
                    <h4>{task.title}</h4>
                    <p>{task.description}</p>
                    <div className="task-meta">
                        <span className="task-priority priority-{task.priority}">
                            {task.priority} priority
                        </span>
                        <span className="task-due-date">
                            Due: {new Date(task.dueDate).toLocaleDateString()}
                        </span>
                    </div>
                </div>
                <div className="task-actions">
                    <button 
                        className="btn btn-success"
                        onClick={handleComplete}
                        disabled={completing}
                    >
                        {completing ? 'Completing...' : 'Complete'}
                    </button>
                    <button className="btn btn-outline-secondary">
                        View Details
                    </button>
                </div>
            </div>
        );
    }
    
    // Notifications List Component
    function NotificationsList({ notifications }) {
        const unreadNotifications = notifications.filter(n => !n.read);
        
        return (
            <div className="notifications-list">
                {unreadNotifications.length === 0 ? (
                    <div className="empty-state">
                        <h4>No new notifications</h4>
                        <p>You're all up to date!</p>
                    </div>
                ) : (
                    unreadNotifications.map(notification => (
                        <NotificationItem 
                            key={notification.id} 
                            notification={notification} 
                        />
                    ))
                )}
            </div>
        );
    }
    
    // Individual Notification Component
    function NotificationItem({ notification }) {
        return (
            <div className="notification-item">
                <div className="notification-icon">
                    {notification.type === 'info' && '💼'}
                    {notification.type === 'warning' && '⚠️'}
                    {notification.type === 'success' && '✅'}
                    {notification.type === 'error' && '❌'}
                </div>
                <div className="notification-content">
                    <h5>{notification.title}</h5>
                    <p>{notification.message}</p>
                    <span className="notification-time">
                        {new Date(notification.createdDate).toLocaleString()}
                    </span>
                </div>
            </div>
        );
    }
    
    // Utility function for notifications
    function showNotification(message, type = 'info') {
        // Integration with Power Pages notification system
        if (window.showPortalNotification) {
            window.showPortalNotification(message, type);
        } else {
            // Fallback notification
            const notification = document.createElement('div');
            notification.className = `notification ${type}`;
            notification.textContent = message;
            document.body.appendChild(notification);
            
            setTimeout(() => {
                notification.remove();
            }, 3000);
        }
    }
    
    // Initialize React application
    document.addEventListener('DOMContentLoaded', function() {
        const container = document.getElementById('react-dashboard-app');
        if (container) {
            const userId = container.dataset.userId;
            const apiBase = container.dataset.apiBase;
            const csrfToken = container.dataset.csrfToken;
            
            const root = ReactDOM.createRoot(container);
            root.render(
                React.createElement(Dashboard, {
                    userId,
                    apiBase,
                    csrfToken
                })
            );
        }
    });
</script>

2. Vue.js Integration Implementation

Vue.js Component Integration:

<!-- Vue.js Application Container -->
<div id="vue-product-configurator" 
     data-product-id="{{ product.id }}"
     data-user-preferences="{{ user.preferences | json | escape }}"
     data-api-endpoint="{{ site.api_base_url }}">
    <!-- Server-side fallback -->
    <div class="configurator-fallback">
        <h3>Product Configuration</h3>
        <p>Please enable JavaScript to use the interactive configurator.</p>
        <a href="/products/{{ product.id }}/configure-basic" class="btn btn-primary">
            Use Basic Configurator
        </a>
    </div>
</div>

<!-- Vue.js Integration -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<script>
    const { createApp, ref, computed, onMounted, watch } = Vue;
    
    // Product Configurator Application
    const ProductConfiguratorApp = {
        setup() {
            // Reactive state
            const productId = ref(null);
            const configuration = ref({
                size: '',
                color: '',
                features: [],
                quantity: 1
            });
            const availableOptions = ref({
                sizes: [],
                colors: [],
                features: []
            });
            const pricing = ref({
                basePrice: 0,
                totalPrice: 0,
                discounts: [],
                taxes: 0
            });
            const loading = ref(true);
            const saving = ref(false);
            const errors = ref([]);
            
            // API client for Power Pages integration
            const apiClient = {
                async request(endpoint, options = {}) {
                    const defaultOptions = {
                        headers: {
                            'Content-Type': 'application/json',
                            'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
                        },
                        credentials: 'same-origin'
                    };
                    
                    const response = await fetch(endpoint, {
                        ...defaultOptions,
                        ...options
                    });
                    
                    if (!response.ok) {
                        throw new Error(`API Error: ${response.status}`);
                    }
                    
                    return response.json();
                },
                
                async getProductOptions(productId) {
                    return this.request(`/api/products/${productId}/options`);
                },
                
                async calculatePricing(productId, configuration) {
                    return this.request(`/api/products/${productId}/pricing`, {
                        method: 'POST',
                        body: JSON.stringify(configuration)
                    });
                },
                
                async saveConfiguration(productId, configuration) {
                    return this.request(`/api/products/${productId}/configure`, {
                        method: 'POST',
                        body: JSON.stringify(configuration)
                    });
                }
            };
            
            // Computed properties
            const isConfigurationValid = computed(() => {
                return configuration.value.size && 
                       configuration.value.color && 
                       configuration.value.quantity > 0;
            });
            
            const configurationSummary = computed(() => {
                const config = configuration.value;
                return {
                    size: config.size,
                    color: config.color,
                    features: config.features.join(', ') || 'None',
                    quantity: config.quantity,
                    unitPrice: pricing.value.basePrice,
                    totalPrice: pricing.value.totalPrice
                };
            });
            
            // Methods
            const loadProductOptions = async () => {
                try {
                    loading.value = true;
                    const options = await apiClient.getProductOptions(productId.value);
                    availableOptions.value = options;
                    
                    // Set default values
                    if (options.sizes.length > 0) {
                        configuration.value.size = options.sizes[0].value;
                    }
                    if (options.colors.length > 0) {
                        configuration.value.color = options.colors[0].value;
                    }
                } catch (error) {
                    errors.value.push('Failed to load product options');
                    console.error('Product options loading error:', error);
                } finally {
                    loading.value = false;
                }
            };
            
            const updatePricing = async () => {
                try {
                    const pricingData = await apiClient.calculatePricing(
                        productId.value, 
                        configuration.value
                    );
                    pricing.value = pricingData;
                } catch (error) {
                    errors.value.push('Failed to calculate pricing');
                    console.error('Pricing calculation error:', error);
                }
            };
            
            const saveConfiguration = async () => {
                try {
                    saving.value = true;
                    errors.value = [];
                    
                    const result = await apiClient.saveConfiguration(
                        productId.value,
                        configuration.value
                    );
                    
                    // Show success message
                    showSuccessMessage('Configuration saved successfully!');
                    
                    // Optionally redirect to cart or checkout
                    if (result.redirectUrl) {
                        window.location.href = result.redirectUrl;
                    }
                } catch (error) {
                    errors.value.push('Failed to save configuration');
                    console.error('Configuration save error:', error);
                } finally {
                    saving.value = false;
                }
            };
            
            const toggleFeature = (featureId) => {
                const features = configuration.value.features;
                const index = features.indexOf(featureId);
                
                if (index > -1) {
                    features.splice(index, 1);
                } else {
                    features.push(featureId);
                }
            };
            
            const clearErrors = () => {
                errors.value = [];
            };
            
            // Watchers
            watch(configuration, updatePricing, { deep: true });
            
            // Lifecycle
            onMounted(async () => {
                // Get product ID from container
                const container = document.getElementById('vue-product-configurator');
                productId.value = container.dataset.productId;
                
                // Load user preferences if available
                const userPreferences = container.dataset.userPreferences;
                if (userPreferences) {
                    try {
                        const preferences = JSON.parse(userPreferences);
                        if (preferences.defaultConfiguration) {
                            Object.assign(configuration.value, preferences.defaultConfiguration);
                        }
                    } catch (error) {
                        console.warn('Failed to parse user preferences');
                    }
                }
                
                await loadProductOptions();
                await updatePricing();
            });
            
            return {
                configuration,
                availableOptions,
                pricing,
                loading,
                saving,
                errors,
                isConfigurationValid,
                configurationSummary,
                saveConfiguration,
                toggleFeature,
                clearErrors
            };
        },
        
        template: `
            <div class="vue-product-configurator">
                <div v-if="loading" class="loading-state">
                    <div class="spinner"></div>
                    <p>Loading configurator...</p>
                </div>
                
                <div v-else class="configurator-content">
                    <!-- Error Messages -->
                    <div v-if="errors.length > 0" class="error-messages">
                        <div class="alert alert-danger">
                            <h5>Configuration Errors:</h5>
                            <ul>
                                <li v-for="error in errors" :key="error">{{ error }}</li>
                            </ul>
                            <button @click="clearErrors" class="btn btn-sm btn-outline-light">
                                Dismiss
                            </button>
                        </div>
                    </div>
                    
                    <!-- Configuration Form -->
                    <div class="configuration-form">
                        <div class="form-section">
                            <h4>Size Selection</h4>
                            <div class="size-options">
                                <label v-for="size in availableOptions.sizes" 
                                       :key="size.value" 
                                       class="size-option">
                                    <input type="radio" 
                                           v-model="configuration.size" 
                                           :value="size.value">
                                    <span class="size-label">{{ size.label }}</span>
                                    <span class="size-description">{{ size.description }}</span>
                                </label>
                            </div>
                        </div>
                        
                        <div class="form-section">
                            <h4>Color Selection</h4>
                            <div class="color-options">
                                <label v-for="color in availableOptions.colors" 
                                       :key="color.value" 
                                       class="color-option">
                                    <input type="radio" 
                                           v-model="configuration.color" 
                                           :value="color.value">
                                    <span class="color-swatch" 
                                          :style="{ backgroundColor: color.hex }"></span>
                                    <span class="color-name">{{ color.label }}</span>
                                </label>
                            </div>
                        </div>
                        
                        <div class="form-section">
                            <h4>Optional Features</h4>
                            <div class="feature-options">
                                <label v-for="feature in availableOptions.features" 
                                       :key="feature.value" 
                                       class="feature-option">
                                    <input type="checkbox" 
                                           :checked="configuration.features.includes(feature.value)"
                                           @change="toggleFeature(feature.value)">
                                    <span class="feature-name">{{ feature.label }}</span>
                                    <span class="feature-price">+${{ feature.price }}</span>
                                    <span class="feature-description">{{ feature.description }}</span>
                                </label>
                            </div>
                        </div>
                        
                        <div class="form-section">
                            <h4>Quantity</h4>
                            <div class="quantity-selector">
                                <button @click="configuration.quantity = Math.max(1, configuration.quantity - 1)" 
                                        class="quantity-btn">-</button>
                                <input type="number" 
                                       v-model.number="configuration.quantity" 
                                       min="1" 
                                       max="99" 
                                       class="quantity-input">
                                <button @click="configuration.quantity = Math.min(99, configuration.quantity + 1)" 
                                        class="quantity-btn">+</button>
                            </div>
                        </div>
                    </div>
                    
                    <!-- Configuration Summary -->
                    <div class="configuration-summary">
                        <h4>Configuration Summary</h4>
                        <div class="summary-details">
                            <div class="summary-item">
                                <span class="label">Size:</span>
                                <span class="value">{{ configurationSummary.size }}</span>
                            </div>
                            <div class="summary-item">
                                <span class="label">Color:</span>
                                <span class="value">{{ configurationSummary.color }}</span>
                            </div>
                            <div class="summary-item">
                                <span class="label">Features:</span>
                                <span class="value">{{ configurationSummary.features }}</span>
                            </div>
                            <div class="summary-item">
                                <span class="label">Quantity:</span>
                                <span class="value">{{ configurationSummary.quantity }}</span>
                            </div>
                        </div>
                        
                        <!-- Pricing Information -->
                        <div class="pricing-summary">
                            <div class="price-item">
                                <span class="label">Unit Price:</span>
                                <span class="value">\${{ pricing.basePrice.toFixed(2) }}</span>
                            </div>
                            <div v-if="pricing.discounts.length > 0" class="price-item discounts">
                                <span class="label">Discounts:</span>
                                <span class="value">
                                    -\${{ pricing.discounts.reduce((sum, d) => sum + d.amount, 0).toFixed(2) }}
                                </span>
                            </div>
                            <div class="price-item">
                                <span class="label">Taxes:</span>
                                <span class="value">\${{ pricing.taxes.toFixed(2) }}</span>
                            </div>
                            <div class="price-item total">
                                <span class="label">Total Price:</span>
                                <span class="value">\${{ pricing.totalPrice.toFixed(2) }}</span>
                            </div>
                        </div>
                    </div>
                    
                    <!-- Action Buttons -->
                    <div class="configurator-actions">
                        <button @click="saveConfiguration" 
                                :disabled="!isConfigurationValid || saving"
                                class="btn btn-primary btn-large">
                            {{ saving ? 'Adding to Cart...' : 'Add to Cart' }}
                        </button>
                        <button class="btn btn-outline-secondary">
                            Save for Later
                        </button>
                    </div>
                </div>
            </div>
        `
    };
    
    // Utility function for success messages
    function showSuccessMessage(message) {
        // Integration with Power Pages notification system
        if (window.showPortalNotification) {
            window.showPortalNotification(message, 'success');
        } else {
            alert(message);
        }
    }
    
    // Initialize Vue application when DOM is ready
    document.addEventListener('DOMContentLoaded', function() {
        const container = document.getElementById('vue-product-configurator');
        if (container) {
            const app = createApp(ProductConfiguratorApp);
            app.mount('#vue-product-configurator');
        }
    });
</script>

3. Angular Integration Strategy

Angular Component Integration:

<!-- Angular Application Container -->
<div id="angular-document-manager" 
     data-user-permissions="{{ user.permissions | json | escape }}"
     data-workspace-id="{{ workspace.id }}"
     data-api-base="{{ site.api_base_url }}">
    <!-- Progressive Enhancement Fallback -->
    <div class="document-manager-fallback">
        <h3>Document Management</h3>
        <div class="basic-actions">
            <a href="/documents/upload" class="btn btn-primary">Upload Document</a>
            <a href="/documents/list" class="btn btn-outline-secondary">View All Documents</a>
        </div>
    </div>
</div>

<!-- Angular Integration Scripts -->
<script src="https://unpkg.com/@angular/core@15/bundles/core.umd.js"></script>
<script src="https://unpkg.com/@angular/common@15/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@15/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@15/bundles/platform-browser-dynamic.umd.js"></script>
<script src="https://unpkg.com/@angular/forms@15/bundles/forms.umd.js"></script>
<script src="https://unpkg.com/@angular/common@15/bundles/http.umd.js"></script>

<script>
    // Angular Document Manager Application
    (function() {
        const { Component, NgModule, Injectable, Input, Output, EventEmitter } = ng.core;
        const { CommonModule } = ng.common;
        const { FormsModule, ReactiveFormsModule, FormBuilder, Validators } = ng.forms;
        const { BrowserModule } = ng.platformBrowser;
        const { HttpClientModule, HttpClient, HttpHeaders } = ng.common.http;
        const { platformBrowserDynamic } = ng.platformBrowserDynamic;
        
        // Document Service
        @Injectable({ providedIn: 'root' })
        class DocumentService {
            constructor(http) {
                this.http = http;
                this.apiBase = document.getElementById('angular-document-manager').dataset.apiBase;
                this.workspaceId = document.getElementById('angular-document-manager').dataset.workspaceId;
            }
            
            getHttpOptions() {
                const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
                return {
                    headers: new HttpHeaders({
                        'Content-Type': 'application/json',
                        'X-CSRF-Token': csrfToken
                    }),
                    withCredentials: true
                };
            }
            
            getDocuments() {
                return this.http.get(`${this.apiBase}/workspaces/${this.workspaceId}/documents`, 
                                   this.getHttpOptions());
            }
            
            uploadDocument(file, metadata) {
                const formData = new FormData();
                formData.append('file', file);
                formData.append('metadata', JSON.stringify(metadata));
                
                const options = {
                    ...this.getHttpOptions(),
                    headers: new HttpHeaders({
                        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
                    })
                };
                
                return this.http.post(`${this.apiBase}/workspaces/${this.workspaceId}/documents/upload`, 
                                    formData, options);
            }
            
            deleteDocument(documentId) {
                return this.http.delete(`${this.apiBase}/documents/${documentId}`, 
                                      this.getHttpOptions());
            }
            
            shareDocument(documentId, shareSettings) {
                return this.http.post(`${this.apiBase}/documents/${documentId}/share`, 
                                    shareSettings, this.getHttpOptions());
            }
        }
        
        // Document List Component
        @Component({
            selector: 'document-list',
            template: `
                <div class="document-list">
                    <div class="list-header">
                        <h4>Documents ({{ documents.length }})</h4>
                        <div class="list-controls">
                            <input type="text" 
                                   [(ngModel)]="searchTerm" 
                                   placeholder="Search documents..."
                                   class="search-input">
                            <select [(ngModel)]="sortBy" class="sort-select">
                                <option value="name">Sort by Name</option>
                                <option value="date">Sort by Date</option>
                                <option value="size">Sort by Size</option>
                            </select>
                        </div>
                    </div>
                    
                    <div class="document-items" *ngIf="filteredDocuments.length > 0">
                        <div class="document-item" 
                             *ngFor="let doc of filteredDocuments" 
                             [class.selected]="selectedDocuments.includes(doc.id)">
                            <div class="document-icon">
                                <i [class]="getDocumentIcon(doc.type)"></i>
                            </div>
                            <div class="document-info">
                                <h5>{{ doc.name }}</h5>
                                <p class="document-details">
                                    {{ doc.size | number }} bytes | 
                                    {{ doc.uploadDate | date:'short' }} |
                                    {{ doc.uploadedBy }}
                                </p>
                                <div class="document-tags" *ngIf="doc.tags.length > 0">
                                    <span class="tag" *ngFor="let tag of doc.tags">{{ tag }}</span>
                                </div>
                            </div>
                            <div class="document-actions">
                                <button (click)="downloadDocument(doc)" 
                                        class="btn btn-sm btn-outline-primary">
                                    Download
                                </button>
                                <button (click)="shareDocument(doc)" 
                                        class="btn btn-sm btn-outline-secondary">
                                    Share
                                </button>
                                <button (click)="deleteDocument(doc)" 
                                        class="btn btn-sm btn-outline-danger"
                                        *ngIf="canDeleteDocument(doc)">
                                    Delete
                                </button>
                            </div>
                        </div>
                    </div>
                    
                    <div class="empty-state" *ngIf="filteredDocuments.length === 0">
                        <i class="icon-folder-empty"></i>
                        <h4>No documents found</h4>
                        <p *ngIf="searchTerm">Try adjusting your search criteria.</p>
                        <p *ngIf="!searchTerm">Upload your first document to get started.</p>
                    </div>
                </div>
            `
        })
        class DocumentListComponent {
            @Input() documents = [];
            @Output() documentDeleted = new EventEmitter();
            @Output() documentShared = new EventEmitter();
            
            searchTerm = '';
            sortBy = 'date';
            selectedDocuments = [];
            userPermissions = [];
            
            constructor() {
                // Get user permissions from container
                const container = document.getElementById('angular-document-manager');
                this.userPermissions = JSON.parse(container.dataset.userPermissions || '[]');
            }
            
            get filteredDocuments() {
                let filtered = this.documents;
                
                // Apply search filter
                if (this.searchTerm) {
                    const term = this.searchTerm.toLowerCase();
                    filtered = filtered.filter(doc => 
                        doc.name.toLowerCase().includes(term) ||
                        doc.tags.some(tag => tag.toLowerCase().includes(term))
                    );
                }
                
                // Apply sorting
                filtered.sort((a, b) => {
                    switch (this.sortBy) {
                        case 'name':
                            return a.name.localeCompare(b.name);
                        case 'size':
                            return b.size - a.size;
                        case 'date':
                        default:
                            return new Date(b.uploadDate) - new Date(a.uploadDate);
                    }
                });
                
                return filtered;
            }
            
            getDocumentIcon(type) {
                const iconMap = {
                    'pdf': 'icon-file-pdf',
                    'doc': 'icon-file-word',
                    'docx': 'icon-file-word',
                    'xls': 'icon-file-excel',
                    'xlsx': 'icon-file-excel',
                    'ppt': 'icon-file-powerpoint',
                    'pptx': 'icon-file-powerpoint',
                    'txt': 'icon-file-text',
                    'image': 'icon-file-image'
                };
                return iconMap[type] || 'icon-file';
            }
            
            canDeleteDocument(document) {
                return this.userPermissions.includes('delete_documents') || 
                       (this.userPermissions.includes('delete_own_documents') && 
                        document.uploadedBy === this.getCurrentUserId());
            }
            
            getCurrentUserId() {
                // Get current user ID from Power Pages context
                return window.currentUser?.id;
            }
            
            downloadDocument(document) {
                window.open(`/api/documents/${document.id}/download`, '_blank');
            }
            
            shareDocument(document) {
                this.documentShared.emit(document);
            }
            
            deleteDocument(document) {
                if (confirm(`Are you sure you want to delete "${document.name}"?`)) {
                    this.documentDeleted.emit(document);
                }
            }
        }
        
        // Document Upload Component
        @Component({
            selector: 'document-upload',
            template: `
                <div class="document-upload">
                    <div class="upload-area" 
                         [class.dragover]="isDragOver"
                         (dragover)="onDragOver($event)"
                         (dragleave)="onDragLeave($event)"
                         (drop)="onDrop($event)"
                         (click)="fileInput.click()">
                        <div class="upload-content">
                            <i class="icon-cloud-upload"></i>
                            <h4>Drop files here or click to browse</h4>
                            <p>Supported formats: PDF, Word, Excel, PowerPoint, Images</p>
                        </div>
                        <input #fileInput 
                               type="file" 
                               multiple 
                               (change)="onFileSelected($event)"
                               style="display: none;">
                    </div>
                    
                    <div class="upload-queue" *ngIf="uploadQueue.length > 0">
                        <h5>Upload Queue</h5>
                        <div class="upload-item" *ngFor="let item of uploadQueue">
                            <div class="upload-info">
                                <span class="file-name">{{ item.file.name }}</span>
                                <span class="file-size">({{ item.file.size | number }} bytes)</span>
                            </div>
                            <div class="upload-progress">
                                <div class="progress-bar">
                                    <div class="progress-fill" 
                                         [style.width.%]="item.progress"></div>
                                </div>
                                <span class="progress-text">{{ item.progress }}%</span>
                            </div>
                            <div class="upload-status">
                                <span [class]="'status-' + item.status">{{ item.status }}</span>
                                <button *ngIf="item.status === 'error'" 
                                        (click)="retryUpload(item)"
                                        class="btn btn-sm btn-outline-primary">
                                    Retry
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            `
        })
        class DocumentUploadComponent {
            @Output() documentUploaded = new EventEmitter();
            
            isDragOver = false;
            uploadQueue = [];
            
            constructor(documentService) {
                this.documentService = documentService;
            }
            
            onDragOver(event) {
                event.preventDefault();
                this.isDragOver = true;
            }
            
            onDragLeave(event) {
                event.preventDefault();
                this.isDragOver = false;
            }
            
            onDrop(event) {
                event.preventDefault();
                this.isDragOver = false;
                
                const files = Array.from(event.dataTransfer.files);
                this.processFiles(files);
            }
            
            onFileSelected(event) {
                const files = Array.from(event.target.files);
                this.processFiles(files);
                event.target.value = ''; // Reset file input
            }
            
            processFiles(files) {
                files.forEach(file => {
                    const uploadItem = {
                        file: file,
                        progress: 0,
                        status: 'pending'
                    };
                    
                    this.uploadQueue.push(uploadItem);
                    this.uploadFile(uploadItem);
                });
            }
            
            async uploadFile(uploadItem) {
                try {
                    uploadItem.status = 'uploading';
                    
                    // Create metadata for the file
                    const metadata = {
                        name: uploadItem.file.name,
                        type: uploadItem.file.type,
                        size: uploadItem.file.size,
                        tags: [] // Could be enhanced with tag input
                    };
                    
                    // Upload with progress tracking
                    const result = await this.documentService.uploadDocument(
                        uploadItem.file, 
                        metadata
                    ).toPromise();
                    
                    uploadItem.progress = 100;
                    uploadItem.status = 'completed';
                    
                    // Emit success event
                    this.documentUploaded.emit(result);
                    
                    // Remove from queue after delay
                    setTimeout(() => {
                        const index = this.uploadQueue.indexOf(uploadItem);
                        if (index > -1) {
                            this.uploadQueue.splice(index, 1);
                        }
                    }, 2000);
                    
                } catch (error) {
                    uploadItem.status = 'error';
                    uploadItem.error = error.message;
                    console.error('Upload error:', error);
                }
            }
            
            retryUpload(uploadItem) {
                uploadItem.progress = 0;
                uploadItem.status = 'pending';
                uploadItem.error = null;
                this.uploadFile(uploadItem);
            }
        }
        
        // Main Document Manager Component
        @Component({
            selector: 'document-manager',
            template: `
                <div class="angular-document-manager">
                    <div class="manager-header">
                        <h3>Document Management</h3>
                        <div class="manager-controls">
                            <button (click)="showUpload = !showUpload" 
                                    class="btn btn-primary">
                                {{ showUpload ? 'Hide Upload' : 'Upload Documents' }}
                            </button>
                            <button (click)="refreshDocuments()" 
                                    class="btn btn-outline-secondary">
                                Refresh
                            </button>
                        </div>
                    </div>
                    
                    <div class="upload-section" *ngIf="showUpload">
                        <document-upload (documentUploaded)="onDocumentUploaded($event)">
                        </document-upload>
                    </div>
                    
                    <div class="documents-section">
                        <div *ngIf="loading" class="loading-state">
                            <div class="spinner"></div>
                            <p>Loading documents...</p>
                        </div>
                        
                        <document-list *ngIf="!loading"
                                     [documents]="documents"
                                     (documentDeleted)="onDocumentDeleted($event)"
                                     (documentShared)="onDocumentShared($event)">
                        </document-list>
                    </div>
                </div>
            `
        })
        class DocumentManagerComponent {
            documents = [];
            loading = true;
            showUpload = false;
            
            constructor(documentService) {
                this.documentService = documentService;
            }
            
            ngOnInit() {
                this.loadDocuments();
            }
            
            async loadDocuments() {
                try {
                    this.loading = true;
                    const result = await this.documentService.getDocuments().toPromise();
                    this.documents = result.documents || [];
                } catch (error) {
                    console.error('Failed to load documents:', error);
                    this.showError('Failed to load documents');
                } finally {
                    this.loading = false;
                }
            }
            
            onDocumentUploaded(document) {
                this.documents.unshift(document);
                this.showSuccess('Document uploaded successfully!');
            }
            
            async onDocumentDeleted(document) {
                try {
                    await this.documentService.deleteDocument(document.id).toPromise();
                    const index = this.documents.findIndex(d => d.id === document.id);
                    if (index > -1) {
                        this.documents.splice(index, 1);
                    }
                    this.showSuccess('Document deleted successfully!');
                } catch (error) {
                    console.error('Failed to delete document:', error);
                    this.showError('Failed to delete document');
                }
            }
            
            onDocumentShared(document) {
                // Implementation for document sharing
                this.showSuccess(`Sharing options for "${document.name}" would open here`);
            }
            
            refreshDocuments() {
                this.loadDocuments();
            }
            
            showSuccess(message) {
                if (window.showPortalNotification) {
                    window.showPortalNotification(message, 'success');
                } else {
                    alert(message);
                }
            }
            
            showError(message) {
                if (window.showPortalNotification) {
                    window.showPortalNotification(message, 'error');
                } else {
                    alert(message);
                }
            }
        }
        
        // Angular Module
        @NgModule({
            imports: [
                BrowserModule,
                CommonModule,
                FormsModule,
                ReactiveFormsModule,
                HttpClientModule
            ],
            declarations: [
                DocumentManagerComponent,
                DocumentListComponent,
                DocumentUploadComponent
            ],
            providers: [DocumentService],
            bootstrap: [DocumentManagerComponent]
        })
        class DocumentManagerModule {}
        
        // Bootstrap Angular application
        document.addEventListener('DOMContentLoaded', function() {
            const container = document.getElementById('angular-document-manager');
            if (container) {
                platformBrowserDynamic()
                    .bootstrapModule(DocumentManagerModule)
                    .catch(err => console.error('Angular bootstrap error:', err));
            }
        });
    })();
</script>

Practical Example: Multi-Framework Portal Dashboard

Let’s implement a comprehensive dashboard that combines multiple frameworks:

<!-- multi-framework-dashboard.html -->
<div class="multi-framework-dashboard">
    <!-- React Analytics Section -->
    <div class="dashboard-section">
        <div id="react-analytics-widget" 
             data-user-id="{{ user.id }}"
             data-date-range="30"
             data-api-endpoint="/api/analytics">
            <div class="widget-fallback">
                <h4>Analytics</h4>
                <p>Loading analytics data...</p>
            </div>
        </div>
    </div>
    
    <!-- Vue.js Task Management Section -->
    <div class="dashboard-section">
        <div id="vue-task-manager" 
             data-project-id="{{ current_project.id }}"
             data-user-role="{{ user.primary_role }}">
            <div class="widget-fallback">
                <h4>Task Management</h4>
                <p>Loading task manager...</p>
            </div>
        </div>
    </div>
    
    <!-- Angular Communication Hub -->
    <div class="dashboard-section">
        <div id="angular-communication-hub" 
             data-team-id="{{ user.team.id }}"
             data-notification-preferences="{{ user.notification_preferences | json | escape }}">
            <div class="widget-fallback">
                <h4>Communication</h4>
                <p>Loading communication hub...</p>
            </div>
        </div>
    </div>
</div>

<!-- Cross-Framework Communication -->
<script>
    // Global event bus for framework communication
    window.FrameworkEventBus = {
        events: {},
        
        on(event, callback) {
            if (!this.events[event]) {
                this.events[event] = [];
            }
            this.events[event].push(callback);
        },
        
        emit(event, data) {
            if (this.events[event]) {
                this.events[event].forEach(callback => callback(data));
            }
        },
        
        off(event, callback) {
            if (this.events[event]) {
                const index = this.events[event].indexOf(callback);
                if (index > -1) {
                    this.events[event].splice(index, 1);
                }
            }
        }
    };
    
    // Shared data store
    window.SharedDataStore = {
        data: {
            user: {
                id: '{{ user.id }}',
                name: '{{ user.fullname }}',
                role: '{{ user.primary_role }}',
                permissions: {{ user.permissions | json | default: '[]' }}
            },
            project: {
                id: '{{ current_project.id | default: "" }}',
                name: '{{ current_project.name | default: "" }}'
            }
        },
        
        get(key) {
            return this.data[key];
        },
        
        set(key, value) {
            this.data[key] = value;
            window.FrameworkEventBus.emit('dataChanged', { key, value });
        },
        
        update(key, updates) {
            if (this.data[key]) {
                Object.assign(this.data[key], updates);
                window.FrameworkEventBus.emit('dataChanged', { key, value: this.data[key] });
            }
        }
    };
    
    // Performance monitoring for framework integration
    window.FrameworkPerformance = {
        timings: {},
        
        start(framework, operation) {
            const key = `${framework}.${operation}`;
            this.timings[key] = { start: performance.now() };
        },
        
        end(framework, operation) {
            const key = `${framework}.${operation}`;
            if (this.timings[key]) {
                this.timings[key].end = performance.now();
                this.timings[key].duration = this.timings[key].end - this.timings[key].start;
                
                // Report performance metrics
                this.reportMetric(framework, operation, this.timings[key].duration);
            }
        },
        
        reportMetric(framework, operation, duration) {
            // Send to analytics
            if (window.gtag) {
                gtag('event', 'framework_performance', {
                    framework: framework,
                    operation: operation,
                    duration: Math.round(duration),
                    custom_parameter: 'milliseconds'
                });
            }
            
            console.log(`${framework} ${operation}: ${duration.toFixed(2)}ms`);
        }
    };
</script>

<!-- Load Framework-Specific Components -->
<script>
    // Initialize all frameworks when DOM is ready
    document.addEventListener('DOMContentLoaded', function() {
        // Track initialization start
        window.FrameworkPerformance.start('dashboard', 'initialization');
        
        // Load React Analytics Widget
        if (document.getElementById('react-analytics-widget')) {
            loadReactAnalytics();
        }
        
        // Load Vue.js Task Manager
        if (document.getElementById('vue-task-manager')) {
            loadVueTaskManager();
        }
        
        // Load Angular Communication Hub
        if (document.getElementById('angular-communication-hub')) {
            loadAngularCommunication();
        }
        
        // Set up cross-framework communication
        setupFrameworkCommunication();
        
        // Track initialization end
        window.FrameworkPerformance.end('dashboard', 'initialization');
    });
    
    function setupFrameworkCommunication() {
        // Example: Task completion in Vue affects analytics in React
        window.FrameworkEventBus.on('taskCompleted', function(taskData) {
            window.FrameworkEventBus.emit('refreshAnalytics', {
                metric: 'tasks_completed',
                increment: 1
            });
        });
        
        // Example: Communication events affect task priorities
        window.FrameworkEventBus.on('urgentMessage', function(messageData) {
            window.FrameworkEventBus.emit('updateTaskPriorities', {
                relatedProject: messageData.projectId,
                urgencyLevel: messageData.urgency
            });
        });
    }
    
    // Error boundary for framework integration
    window.addEventListener('error', function(event) {
        // Determine which framework caused the error
        let framework = 'unknown';
        if (event.filename && event.filename.includes('react')) {
            framework = 'react';
        } else if (event.filename && event.filename.includes('vue')) {
            framework = 'vue';
        } else if (event.filename && event.filename.includes('angular')) {
            framework = 'angular';
        }
        
        // Log framework-specific errors
        console.error(`Framework Error (${framework}):`, {
            message: event.message,
            filename: event.filename,
            lineno: event.lineno,
            colno: event.colno
        });
        
        // Report to error tracking
        if (window.reportError) {
            window.reportError({
                type: 'framework_error',
                framework: framework,
                message: event.message,
                stack: event.error ? event.error.stack : null
            });
        }
    });
</script>

Troubleshooting Common Issues

JavaScript Framework Integration Debugging:

  1. Framework Conflicts: Implement proper namespace isolation and version management
  2. Performance Issues: Monitor bundle sizes and implement code splitting strategies
  3. State Synchronization: Establish clear data flow patterns between frameworks and Power Pages
  4. Authentication Integration: Ensure framework components respect Power Pages authentication state
  5. SEO and Accessibility: Implement progressive enhancement and server-side rendering fallbacks

Framework Integration Best Practices

Establishing Robust Integration Architecture:

  1. Progressive Enhancement: Ensure basic functionality works without JavaScript frameworks
  2. Performance Monitoring: Track framework loading times and runtime performance
  3. Error Boundaries: Implement comprehensive error handling for framework failures
  4. Version Management: Maintain consistent framework versions across development and production
  5. Testing Strategy: Develop comprehensive testing approaches for framework integrations

Key Takeaways

JavaScript framework integration enables creation of modern, interactive user experiences within Power Pages portals. Focus on progressive enhancement, performance optimization, robust error handling, and clear communication patterns between frameworks and Power Pages to build scalable, maintainable portal solutions that deliver exceptional user experiences.

In the next episode, you’ll learn about Advanced Security & Authentication to implement sophisticated security measures and custom authentication flows for enterprise-grade portal protection.

4. Documentation and Maintenance

Set yourself up for long-term success:

Practical Example

Consider implementing this episode’s techniques for a customer portal where users need enhanced functionality beyond basic templates. The implementation should:

Apply the specific techniques from this episode while following the general principles of good portal development.

Common Implementation Patterns

Pattern When to Use Benefits Considerations
Progressive Enhancement Adding advanced features Ensures basic functionality always works Requires careful planning
Responsive Design Multi-device support Consistent experience across devices May increase complexity
Modular Development Complex customizations Easier maintenance and updates Requires good architecture
Performance Monitoring All implementations Early detection of issues Ongoing effort required

Troubleshooting Tips

Advanced Considerations

As you implement these intermediate techniques, consider:

Summary

Effective search functionality is often the difference between a portal that users love and one they abandon. Invest time in configuring search properly, tune your indexes for your content types, and provide multiple ways for users to discover information. Next, you’ll connect your portal to Power Automate for workflow automation.