266
docs/STATE_PRESERVATION.md
Normal file
266
docs/STATE_PRESERVATION.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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**
|
||||
|
||||
```javascript
|
||||
// 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**
|
||||
|
||||
```javascript
|
||||
// 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**
|
||||
|
||||
```javascript
|
||||
// Update data while preserving UI state
|
||||
this.viewModel.batchUpdate({
|
||||
members: newMembers,
|
||||
lastUpdateTime: new Date().toISOString()
|
||||
}, { preserveUIState: true });
|
||||
```
|
||||
|
||||
### 4. **Smart Updates**
|
||||
|
||||
```javascript
|
||||
// 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:
|
||||
|
||||
1. **Expand cluster member cards**
|
||||
2. **Change active tabs in node details**
|
||||
3. **Trigger data refresh**
|
||||
4. **Verify state is preserved**
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Old System
|
||||
|
||||
If you're upgrading from the old system:
|
||||
|
||||
1. **Update ViewModel Listeners**: Change from `this.render.bind(this)` to specific update handlers
|
||||
2. **Add State Management**: Use `setUIState()` and `getUIState()` for UI state
|
||||
3. **Implement Partial Updates**: Override `updatePartial()` method for efficient updates
|
||||
4. **Use Smart Updates**: Replace direct data updates with `smartUpdate()` calls
|
||||
|
||||
### Example Migration
|
||||
|
||||
**Old Code:**
|
||||
```javascript
|
||||
this.subscribeToProperty('members', this.render.bind(this));
|
||||
|
||||
async handleRefresh() {
|
||||
await this.viewModel.updateClusterMembers();
|
||||
}
|
||||
```
|
||||
|
||||
**New Code:**
|
||||
```javascript
|
||||
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
|
||||
|
||||
1. **Always Store UI State**: Use `setUIState()` for any user interaction
|
||||
2. **Implement Partial Updates**: Override `updatePartial()` for efficient updates
|
||||
3. **Use Change Detection**: Leverage `hasChanged()` to avoid unnecessary updates
|
||||
4. **Batch Related Updates**: Use `batchUpdate()` for multiple property changes
|
||||
5. **Test State Preservation**: Verify that UI state is maintained during data refreshes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **State Not Preserved**: Ensure you're using `setUIState()` and `getUIState()`
|
||||
2. **Full Re-renders**: Check if `shouldPreserveState()` logic is correct
|
||||
3. **Performance Issues**: Verify you're using partial updates instead of full renders
|
||||
|
||||
### Debug Tips
|
||||
|
||||
1. **Enable Console Logging**: Check browser console for state preservation logs
|
||||
2. **Use State Indicators**: Monitor state changes in the test interface
|
||||
3. **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
|
||||
Reference in New Issue
Block a user