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
next prev 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