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