public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH yew-widget-toolkit v2 2/3] widget: navigation drawer: allow persisting the expanded state
Date: Fri,  4 Sep 2026 12:26:35 +0200	[thread overview]
Message-ID: <20260904102643.3563579-3-d.csapak@proxmox.com> (raw)
In-Reply-To: <20260904102643.3563579-1-d.csapak@proxmox.com>

By exposing a `stateful_id` property that is used to save the
`NavigationDrawerState` in the browser local storage.

This is loaded on component creation and when the stateful id changes.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/widget/nav/navigation_drawer.rs | 60 ++++++++++++++++++++++++++---
 1 file changed, 54 insertions(+), 6 deletions(-)

diff --git a/src/widget/nav/navigation_drawer.rs b/src/widget/nav/navigation_drawer.rs
index eae2ca4..890eb04 100644
--- a/src/widget/nav/navigation_drawer.rs
+++ b/src/widget/nav/navigation_drawer.rs
@@ -1,6 +1,7 @@
 use std::collections::{HashMap, HashSet};
 use std::rc::Rc;
 
+use serde::{Deserialize, Serialize};
 use yew::html::{IntoEventCallback, IntoPropValue};
 use yew::prelude::*;
 use yew::virtual_dom::{Key, VComp, VNode};
@@ -10,9 +11,9 @@ use pwt_macros::builder;
 use crate::css::{OverflowX, OverflowY};
 use crate::props::{
     AsClassesMut, AsCssStylesMut, ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, CssStyles,
-    EventSubscriber, IntoOptionalKey, IntoVTag, WidgetBuilder, WidgetStyleBuilder,
+    EventSubscriber, IntoOptionalKey, IntoVTag, StorageLocation, WidgetBuilder, WidgetStyleBuilder,
 };
-use crate::state::{NavigationContext, NavigationContextExt, Selection};
+use crate::state::{NavigationContext, NavigationContextExt, PersistentState, Selection};
 use crate::{impl_class_prop_builder, impl_yew_std_props_builder};
 
 use crate::dom::focus::roving_tabindex_next_recursive;
@@ -82,6 +83,14 @@ pub struct NavigationDrawer {
     #[builder]
     #[prop_or(true)]
     animated: bool,
+
+    /// If set, saves the open/collapsed state in the browser local storage
+    ///
+    /// The value is used as the storage key, so it has to be unique within the application. Only
+    /// the menu items that deviate from their default state are stored.
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or_default]
+    stateful_id: Option<AttrValue>,
 }
 
 impl AsClassesMut for NavigationDrawer {
@@ -147,8 +156,10 @@ impl NavigationDrawer {
 }
 
 /// A struct that holds the current state that can get saved to local storage
-#[derive(Default, Clone, PartialEq)]
+#[derive(Default, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
 struct NavigationDrawerState {
+    #[serde(default, skip_serializing_if = "HashSet::is_empty")]
     changed_expanded: HashSet<String>, // contains all keys that deviate from their default
 }
 
@@ -185,6 +196,7 @@ pub struct PwtNavigationDrawer {
     selection: Selection,
     default_collapsed: HashMap<Key, bool>,
     state: NavigationDrawerState,
+    stateful_id: Option<AttrValue>, // just a local copy so we don't have pass ctx around
     _nav_ctx_handle: Option<ContextHandle<NavigationContext>>,
 }
 
@@ -474,10 +486,39 @@ impl PwtNavigationDrawer {
         let key_string = key.to_string();
         let default_open = !self.default_collapsed.get(key).unwrap_or(&false);
         // only record the keys that deviate from their default, so the set stays minimal
-        if expanded == default_open {
-            self.state.changed_expanded.remove(&key_string);
+        let modified = if expanded == default_open {
+            self.state.changed_expanded.remove(&key_string)
         } else {
-            self.state.changed_expanded.insert(key_string);
+            self.state.changed_expanded.insert(key_string)
+        };
+        if modified {
+            self.save_state();
+        }
+    }
+
+    /// Loads the deviations from the default state from the local storage, if an `id` is set.
+    fn load_state(&mut self) {
+        if let Some(id) = self.stateful_id.clone() {
+            let state =
+                PersistentState::<Option<NavigationDrawerState>>::new(StorageLocation::Local(id));
+            if let Some(state) = state.into_inner() {
+                self.state = state;
+            }
+        }
+    }
+
+    /// Saves the deviations from the default state to the local storage, if an `id` is set.
+    fn save_state(&self) {
+        if let Some(id) = self.stateful_id.clone() {
+            let mut state =
+                PersistentState::<Option<NavigationDrawerState>>::new(StorageLocation::Local(id));
+
+            let drawer_state = if self.state == NavigationDrawerState::default() {
+                None
+            } else {
+                Some(self.state.clone())
+            };
+            state.update(drawer_state);
         }
     }
 }
@@ -533,9 +574,12 @@ impl Component for PwtNavigationDrawer {
             selection,
             default_collapsed: collect_default_collapse_states(&props.menu),
             state: NavigationDrawerState::default(),
+            stateful_id: props.stateful_id.clone(),
             _nav_ctx_handle,
         };
 
+        this.load_state();
+
         // expand the path to the initially active item, so a deep-linked entry is visible
         if let Some(active) = &active {
             this.open_ancestors(props, active);
@@ -667,6 +711,10 @@ impl Component for PwtNavigationDrawer {
             }
             self.default_collapsed = new_default_collapsed;
         }
+        if props.stateful_id != old_props.stateful_id {
+            self.stateful_id = props.stateful_id.clone();
+            self.load_state();
+        }
         true
     }
 
-- 
2.47.3





  parent reply	other threads:[~2026-09-04 10:26 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-04 10:26 [PATCH datacenter-manager/yew-widget-toolkit v2 0/3] ui: persist the main menus expanded state Dominik Csapak
2026-09-04 10:26 ` [PATCH yew-widget-toolkit v2 1/3] widget: navigation drawer: fix first open for default collapsed menus Dominik Csapak
2026-09-04 10:26 ` Dominik Csapak [this message]
2026-09-04 10:26 ` [PATCH datacenter-manager v2 3/3] ui: main menu: make expanded/collapsed state persistent Dominik Csapak

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260904102643.3563579-3-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal