# 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