8.4 KiB
8.4 KiB
SPORE UI State Preservation System
Overview
The SPORE UI framework now includes an advanced state preservation system that prevents UI state loss during data refreshes. This system ensures that user interactions like expanded cards, active tabs, and other UI state are maintained when data is updated from the server.
Key Features
1. UI State Persistence
- Expanded Cards: When cluster member cards are expanded, their state is preserved across data refreshes
- Active Tabs: Active tab selections within node detail views are maintained
- User Interactions: All user-initiated UI changes are stored and restored automatically
2. Smart Data Updates
- Change Detection: The system detects when data has actually changed and only updates what's necessary
- Partial Updates: Components can update specific data without re-rendering the entire UI
- State Preservation: UI state is automatically preserved during all data operations
3. Efficient Rendering
- No Full Re-renders: Components avoid unnecessary full re-renders when only data changes
- Granular Updates: Only changed properties trigger UI updates
- Performance Optimization: Reduced DOM manipulation and improved user experience
Architecture
Enhanced ViewModel Class
The base ViewModel class now includes:
class ViewModel {
// UI State Management
setUIState(key, value) // Store UI state
getUIState(key) // Retrieve UI state
getAllUIState() // Get all stored UI state
clearUIState(key) // Clear specific or all UI state
// Change Detection
hasChanged(property) // Check if property changed
getPrevious(property) // Get previous value
// Batch Updates
batchUpdate(updates, options) // Update multiple properties with state preservation
}
Enhanced Component Class
The base Component class now includes:
class Component {
// UI State Management
setUIState(key, value) // Store local UI state
getUIState(key) // Get local or view model state
getAllUIState() // Get merged state
restoreUIState() // Restore state from view model
// Partial Updates
updatePartial(property, newValue, previousValue) // Handle partial updates
}
Implementation Examples
1. Cluster Members Component
The ClusterMembersComponent demonstrates state preservation:
class ClusterMembersComponent extends Component {
setupViewModelListeners() {
// Listen with change detection
this.subscribeToProperty('members', this.handleMembersUpdate.bind(this));
}
handleMembersUpdate(newMembers, previousMembers) {
if (this.shouldPreserveState(newMembers, previousMembers)) {
// Partial update preserves UI state
this.updateMembersPartially(newMembers, previousMembers);
} else {
// Full re-render only when necessary
this.render();
}
}
shouldPreserveState(newMembers, previousMembers) {
// Check if member structure allows state preservation
if (newMembers.length !== previousMembers.length) return false;
const newIps = new Set(newMembers.map(m => m.ip));
const prevIps = new Set(previousMembers.map(m => m.ip));
return newIps.size === prevIps.size &&
[...newIps].every(ip => prevIps.has(ip));
}
}
2. Node Details Component
The NodeDetailsComponent preserves active tab state:
class NodeDetailsComponent extends Component {
setupViewModelListeners() {
this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this));
}
handleActiveTabUpdate(newTab, previousTab) {
// Update tab UI without full re-render
this.updateActiveTab(newTab, previousTab);
}
updateActiveTab(newTab) {
// Update only the tab UI, preserving other state
const tabButtons = this.findAllElements('.tab-button');
const tabContents = this.findAllElements('.tab-content');
tabButtons.forEach(btn => btn.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
const activeButton = this.findElement(`[data-tab="${newTab}"]`);
const activeContent = this.findElement(`#${newTab}-tab`);
if (activeButton) activeButton.classList.add('active');
if (activeContent) activeContent.classList.add('active');
}
}
Usage Patterns
1. Storing UI State
// In a component
this.setUIState('expandedCard', memberIp);
this.setUIState('activeTab', 'firmware');
// In a view model
this.setUIState('userPreferences', { theme: 'dark', layout: 'compact' });
2. Retrieving UI State
// Get specific state
const expandedCard = this.getUIState('expandedCard');
const activeTab = this.getUIState('activeTab');
// Get all state
const allState = this.getAllUIState();
3. Batch Updates with State Preservation
// Update data while preserving UI state
this.viewModel.batchUpdate({
members: newMembers,
lastUpdateTime: new Date().toISOString()
}, { preserveUIState: true });
4. Smart Updates
// Use smart update to preserve state
await this.viewModel.smartUpdate();
Benefits
1. Improved User Experience
- Users don't lose their place in the interface
- Expanded cards remain expanded
- Active tabs stay selected
- No jarring UI resets
2. Better Performance
- Reduced unnecessary DOM manipulation
- Efficient partial updates
- Optimized rendering cycles
3. Maintainable Code
- Clear separation of concerns
- Consistent state management patterns
- Easy to extend and modify
Testing
Use the test-state-preservation.html file to test the state preservation system:
- Expand cluster member cards
- Change active tabs in node details
- Trigger data refresh
- Verify state is preserved
Migration Guide
From Old System
If you're upgrading from the old system:
- Update ViewModel Listeners: Change from
this.render.bind(this)to specific update handlers - Add State Management: Use
setUIState()andgetUIState()for UI state - Implement Partial Updates: Override
updatePartial()method for efficient updates - Use Smart Updates: Replace direct data updates with
smartUpdate()calls
Example Migration
Old Code:
this.subscribeToProperty('members', this.render.bind(this));
async handleRefresh() {
await this.viewModel.updateClusterMembers();
}
New Code:
this.subscribeToProperty('members', this.handleMembersUpdate.bind(this));
async handleRefresh() {
await this.viewModel.smartUpdate();
}
handleMembersUpdate(newMembers, previousMembers) {
if (this.shouldPreserveState(newMembers, previousMembers)) {
this.updateMembersPartially(newMembers, previousMembers);
} else {
this.render();
}
}
Best Practices
- Always Store UI State: Use
setUIState()for any user interaction - Implement Partial Updates: Override
updatePartial()for efficient updates - Use Change Detection: Leverage
hasChanged()to avoid unnecessary updates - Batch Related Updates: Use
batchUpdate()for multiple property changes - Test State Preservation: Verify that UI state is maintained during data refreshes
Troubleshooting
Common Issues
- State Not Preserved: Ensure you're using
setUIState()andgetUIState() - Full Re-renders: Check if
shouldPreserveState()logic is correct - Performance Issues: Verify you're using partial updates instead of full renders
Debug Tips
- Enable Console Logging: Check browser console for state preservation logs
- Use State Indicators: Monitor state changes in the test interface
- Verify Change Detection: Ensure
hasChanged()is working correctly
Future Enhancements
- State Synchronization: Real-time state sync across multiple browser tabs
- Advanced Change Detection: Deep object comparison for complex data structures
- State Persistence: Save UI state to localStorage for session persistence
- State Rollback: Ability to revert to previous UI states