Episode 18: JavaScript Frameworks Integration
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
- Framework Integration Patterns: Strategic approaches to embedding modern JavaScript frameworks within Power Pages
- Hydration Strategies: Techniques for initializing client-side frameworks with server-side data
- Component Architecture: Building reusable, maintainable UI components that integrate seamlessly with Power Pages
- State Management: Coordinating application state between Power Pages and JavaScript frameworks
- Performance Optimization: Ensuring framework integration doesn’t compromise portal performance
Learning Objectives
By the end of this episode, you will be able to:
- Integrate React, Vue.js, and Angular components within Power Pages portals
- Implement sophisticated state management patterns that bridge server and client-side data
- Create reusable component architectures that enhance development efficiency
- Optimize performance for complex JavaScript framework integrations
- Build progressive enhancement strategies that gracefully degrade for all users
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:
- Framework Conflicts: Implement proper namespace isolation and version management
- Performance Issues: Monitor bundle sizes and implement code splitting strategies
- State Synchronization: Establish clear data flow patterns between frameworks and Power Pages
- Authentication Integration: Ensure framework components respect Power Pages authentication state
- SEO and Accessibility: Implement progressive enhancement and server-side rendering fallbacks
Framework Integration Best Practices
Establishing Robust Integration Architecture:
- Progressive Enhancement: Ensure basic functionality works without JavaScript frameworks
- Performance Monitoring: Track framework loading times and runtime performance
- Error Boundaries: Implement comprehensive error handling for framework failures
- Version Management: Maintain consistent framework versions across development and production
- 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.
- Verify functionality across different browsers and devices
- Validate accessibility compliance
- Performance test under load conditions
4. Documentation and Maintenance
Set yourself up for long-term success:
- Document all customizations and their purpose
- Create maintenance procedures and schedules
- Plan for future updates and changes
- Train team members on your customizations
Practical Example
Consider implementing this episode’s techniques for a customer portal where users need enhanced functionality beyond basic templates. The implementation should:
- Provide an intuitive, branded user experience
- Handle complex business logic appropriately
- Perform well under realistic usage conditions
- Be maintainable by your development team
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
- Performance Issues: Monitor page load times and optimize images, scripts, and database queries
- Browser Compatibility: Test across different browsers and use progressive enhancement
- Mobile Experience: Ensure all functionality works well on mobile devices
- Maintenance Challenges: Keep good documentation and follow consistent coding standards
Advanced Considerations
As you implement these intermediate techniques, consider:
- Scalability: How will your solution perform as usage grows?
- Security: Do your customizations introduce any security vulnerabilities?
- Integration: How do your changes affect other portal functionality?
- Future Updates: Will your customizations survive platform updates?
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.