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 2/3] widget: navigation drawer: allow persisting the expanded state
Date: Thu, 20 Aug 2026 10:17:51 +0200	[thread overview]
Message-ID: <20260820081803.991511-3-d.csapak@proxmox.com> (raw)
In-Reply-To: <20260820081803.991511-1-d.csapak@proxmox.com>

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

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

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

diff --git a/src/widget/nav/navigation_drawer.rs b/src/widget/nav/navigation_drawer.rs
index b2bcd1e..114e463 100644
--- a/src/widget/nav/navigation_drawer.rs
+++ b/src/widget/nav/navigation_drawer.rs
@@ -10,9 +10,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 +82,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 {
@@ -153,18 +161,25 @@ impl NavigationDrawer {
 struct MenuStates {
     default_collapsed: HashSet<String>, // contains all keys that are collapsed by default
     changed: HashSet<String>,           // contains all keys that deviate from their default
+    id: Option<AttrValue>,              // local storage key, if the state should be persisted
 }
 
 impl MenuStates {
     /// Initializes the state from the menu config (default_collapsed).
-    fn new(menu: &Menu) -> Self {
+    ///
+    /// If an `id` is given, the state will be loaded from, and subsequently saved to, the browser
+    /// local storage with that identifier.
+    fn new(menu: &Menu, id: Option<AttrValue>) -> Self {
         let mut default_collapsed = HashSet::new();
         Self::collect_default_collapse_states(&mut default_collapsed, menu);
 
-        Self {
+        let mut this = Self {
             default_collapsed,
             changed: HashSet::new(),
-        }
+            id,
+        };
+        this.load_state();
+        this
     }
 
     // iterates over a menu and saves all submenus that are collapsed by default
@@ -199,10 +214,33 @@ impl MenuStates {
         let key = key.to_string();
         let default_open = !self.default_collapsed.contains(&key);
         // only record the keys that deviate from their default, so the set stays minimal
-        if open == default_open {
-            self.changed.remove(&key);
+        let modified = if open == default_open {
+            self.changed.remove(&key)
         } else {
-            self.changed.insert(key);
+            self.changed.insert(key)
+        };
+        // do not rewrite the local storage if the state did not actually change
+        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.id.clone() {
+            let state = PersistentState::<Option<HashSet<String>>>::new(StorageLocation::Local(id));
+            if let Some(state) = state.into_inner() {
+                self.changed = 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.id.clone() {
+            let mut state =
+                PersistentState::<Option<HashSet<String>>>::new(StorageLocation::Local(id));
+            state.update(Some(self.changed.clone()));
         }
     }
 }
@@ -544,7 +582,7 @@ impl Component for PwtNavigationDrawer {
             node_ref: NodeRef::default(),
             active: active.clone(),
             selection,
-            menu_states: MenuStates::new(&props.menu),
+            menu_states: MenuStates::new(&props.menu, props.stateful_id.clone()),
             _nav_ctx_handle,
         };
 
@@ -663,8 +701,8 @@ impl Component for PwtNavigationDrawer {
         if props.selection != old_props.selection {
             self.selection = Self::init_selection(ctx, props.selection.clone(), &self.active);
         }
-        if props.menu != old_props.menu {
-            self.menu_states = MenuStates::new(&props.menu);
+        if props.menu != old_props.menu || props.stateful_id != old_props.stateful_id {
+            self.menu_states = MenuStates::new(&props.menu, props.stateful_id.clone());
         }
         true
     }
-- 
2.47.3





  parent reply	other threads:[~2026-08-20  8:18 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-20  8:17 [PATCH datacenter-manager/yew-widget-toolkit 0/3] ui: persist the main menus expanded state Dominik Csapak
2026-08-20  8:17 ` [PATCH yew-widget-toolkit 1/3] widget: navigation drawer: fix first open for default collapsed menus Dominik Csapak
2026-08-20  8:17 ` Dominik Csapak [this message]
2026-08-20  8:17 ` [PATCH datacenter-manager 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=20260820081803.991511-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