Readable and Writable helpers for String, HashMap and HashSet (#4834)

* Add more read/write methods for string signals

* add helpers for HashMap and HashSet as well

* fix typo

* fix conflicting string and str extensions

* fix counters example
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-10-27 11:39:48 -07:00
1 parent 3eecfaa0a7
commit e4297d0e09
13 files changed
+308 -33

No files matched your search

+4 -4
View File
@@ -34,15 +34,15 @@ fn app() -> Element {
if val() == "0" {
val.set(String::new());
}
val.write().push_str(num.as_str());
val.push_str(num.as_str());
};
let mut input_operator = move |key: &str| val.write().push_str(key);
let mut input_operator = move |key: &str| val.push_str(key);
let handle_key_down_event = move |evt: KeyboardEvent| match evt.key() {
Key::Backspace => {
if !val().is_empty() {
val.write().pop();
val.pop();
}
}
Key::Character(character) => match character.as_str() {
@@ -104,7 +104,7 @@ fn app() -> Element {
}
button {
class: "calculator-key key-dot",
onclick: move |_| val.write().push('.'),
onclick: move |_| val.push('.'),
"●"
}
for k in 1..10 {
+3 -3
View File
@@ -19,8 +19,8 @@ fn app() -> Element {
Stylesheet { href: STYLE }
div { id: "controls",
button { onclick: move |_| counters.write().push(0), "Add counter" }
button { onclick: move |_| { counters.write().pop(); }, "Remove counter" }
button { onclick: move |_| counters.push(0), "Add counter" }
button { onclick: move |_| { counters.pop(); }, "Remove counter" }
}
h3 { "Total: {sum}" }
@@ -47,7 +47,7 @@ fn app() -> Element {
}
}
button { onclick: move |_| counters.write()[i] += 1, "+1" }
button { onclick: move |_| { counters.write().remove(i); }, "x" }
button { onclick: move |_| { counters.remove(i); }, "x" }
}
}
}
+6 -6
View File
@@ -115,13 +115,13 @@ fn TodoHeader(mut todos: WriteSignal<HashMap<u32, TodoItem>>) -> Element {
let mut todo_id = use_signal(|| 0);
let onkeydown = move |evt: KeyboardEvent| {
if evt.key() == Key::Enter && !draft.read().is_empty() {
if evt.key() == Key::Enter && !draft.is_empty() {
let id = todo_id();
let todo = TodoItem {
checked: false,
contents: draft.to_string(),
};
todos.write().insert(id, todo);
todos.insert(id, todo);
todo_id += 1;
draft.set("".to_string());
}
@@ -169,7 +169,7 @@ fn TodoEntry(mut todos: WriteSignal<HashMap<u32, TodoItem>>, id: u32) -> Element
r#type: "checkbox",
id: "cbg-{id}",
checked: "{checked}",
oninput: move |evt| todos.write().get_mut(&id).unwrap().checked = evt.checked()
oninput: move |evt| todos.get_mut(&id).unwrap().checked = evt.checked()
}
label {
r#for: "cbg-{id}",
@@ -181,7 +181,7 @@ fn TodoEntry(mut todos: WriteSignal<HashMap<u32, TodoItem>>, id: u32) -> Element
class: "destroy",
onclick: move |evt| {
evt.prevent_default();
todos.write().remove(&id);
todos.remove(&id);
},
}
}
@@ -191,7 +191,7 @@ fn TodoEntry(mut todos: WriteSignal<HashMap<u32, TodoItem>>, id: u32) -> Element
input {
class: "edit",
value: "{contents}",
oninput: move |evt| todos.write().get_mut(&id).unwrap().contents = evt.value(),
oninput: move |evt| todos.get_mut(&id).unwrap().contents = evt.value(),
autofocus: "true",
onfocusout: move |_| is_editing.set(false),
onkeydown: move |evt| {
@@ -250,7 +250,7 @@ fn ListFooter(
if show_clear_completed() {
button {
class: "clear-completed",
onclick: move |_| todos.write().retain(|_, todo| !todo.checked),
onclick: move |_| todos.retain(|_, todo| !todo.checked),
"Clear completed"
}
}
+1 -1
View File
@@ -175,7 +175,7 @@ fn TodoHeader(mut todos: Store<TodoState>) -> Element {
let mut todo_id = use_signal(|| 0);
let onkeydown = move |evt: KeyboardEvent| {
if evt.key() == Key::Enter && !draft.read().is_empty() {
if evt.key() == Key::Enter && !draft.is_empty() {
let id = todo_id();
let todo = TodoItem::new(draft.take());
todos.todos().insert(id, todo);
+1 -1
View File
@@ -46,7 +46,7 @@ fn app() -> Element {
use_future(move || async move {
while let Ok(msg) = socket.recv().await {
match msg {
ServerEvent::ReceiveMessage(message) => message_list.write().push(message),
ServerEvent::ReceiveMessage(message) => message_list.push(message),
ServerEvent::Connected { messages } => message_list.set(messages),
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ fn app() -> Element {
// And then poll it for new events, adding them to our signal
while let Some(Ok(event)) = stream.recv().await {
events.write().push(event);
events.push(event);
}
anyhow::Ok(())
+5 -5
View File
@@ -31,23 +31,23 @@ fn app() -> Element {
let mut json_responses = use_signal(Vec::new);
let mut start_text_stream = use_action(move || async move {
text_responses.write().clear();
text_responses.clear();
let mut stream = text_stream(Some(100)).await?;
while let Some(Ok(text)) = stream.next().await {
text_responses.write().push_str(&text);
text_responses.write().push('\n');
text_responses.push_str(&text);
text_responses.push('\n');
}
dioxus::Ok(())
});
let mut start_json_stream = use_action(move || async move {
json_responses.write().clear();
json_responses.clear();
let mut stream = json_stream().await?;
while let Some(Ok(dog)) = stream.next().await {
json_responses.write().push(dog);
json_responses.push(dog);
}
dioxus::Ok(())
+1 -1
View File
@@ -56,7 +56,7 @@ fn app() -> Element {
input {
r#type: "number",
value: "{i}",
onmounted: move |cx| elements.write().push(cx.data()),
onmounted: move |cx| elements.push(cx.data()),
oninput: move |_| running.set(false),
}
}
+3 -3
View File
@@ -27,12 +27,12 @@ fn app() -> Element {
for file in files {
let filename = file.name();
if let Ok(contents) = file.read_string().await {
files_uploaded.write().push(UploadedFile {
files_uploaded.push(UploadedFile {
name: filename,
contents,
});
} else {
files_uploaded.write().push(UploadedFile {
files_uploaded.push(UploadedFile {
name: filename,
contents: "Failed to read file".into(),
});
@@ -45,7 +45,7 @@ fn app() -> Element {
h1 { "File Upload Example" }
p { "Drop a .txt, .rs, or .js file here to read it" }
button { onclick: move |_| files_uploaded.write().clear(), "Clear files" }
button { onclick: move |_| files_uploaded.clear(), "Clear files" }
div {
label { r#for: "directory-upload", "Enable directory upload" }
+1 -1
View File
@@ -15,7 +15,7 @@ fn app() -> Element {
let handle = use_coroutine(move |mut rx: UnboundedReceiver<String>| async move {
use futures_util::StreamExt;
while let Some(message) = rx.next().await {
emails_sent.write().push(message);
emails_sent.push(message);
}
});
+29
View File
@@ -44,3 +44,32 @@ pub mod warnings;
mod boxed;
pub use boxed::*;
/// A macro to define extension methods for signal types that call the method with either `with` or `with_mut` depending on the mutability of self.
macro_rules! ext_methods {
(
$(
$(#[$meta:meta])*
fn $name:ident $(<$($gen:tt),*>)? (&$($self:ident)+ $(, $arg_name:ident: $arg_type:ty )* ) $(-> $ret:ty)? = $expr:expr;
)*
) => {
$(
$(#[$meta])*
#[track_caller]
fn $name$(<$($gen),*>)? (& $($self)+ $(, $arg_name: $arg_type )* ) $(-> $ret)?
{
ext_methods!(@with $($self)+, $($arg_name),*; $expr)
}
)*
};
(@with mut $self:ident, $($arg_name:ident),*; $expr:expr) => {
$self.with_mut(|_self| ($expr)(_self, $($arg_name),*))
};
(@with $self:ident, $($arg_name:ident),*; $expr:expr) => {
$self.with(|_self| ($expr)(_self, $($arg_name),*))
};
}
pub(crate) use ext_methods;
+102 -3
View File
@@ -1,10 +1,13 @@
use std::{mem::MaybeUninit, ops::Index};
use std::collections::{HashMap, HashSet};
use std::{
mem::MaybeUninit,
ops::{Deref, Index},
};
use crate::{ext_methods, MappedSignal, ReadSignal};
use dioxus_core::Subscribers;
use generational_box::{AnyStorage, UnsyncStorage};
use crate::{MappedSignal, ReadSignal};
/// A reference to a value that can be read from.
#[allow(type_alias_bounds)]
pub type ReadableRef<'a, T: Readable, O = <T as Readable>::Target> =
@@ -423,3 +426,99 @@ pub trait ReadableResultExt<T, E>: Readable<Target = Result<T, E>> {
}
impl<T, E, R> ReadableResultExt<T, E> for R where R: Readable<Target = Result<T, E>> {}
/// An extension trait for [`Readable<String>`] that provides some convenience methods.
pub trait ReadableStringExt: Readable<Target = String> {
ext_methods! {
/// Check the capacity of the string.
fn capacity(&self) -> usize = String::capacity;
}
}
impl<W> ReadableStringExt for W where W: Readable<Target = String> {}
/// An extension trait for [`Readable<String>`] and [`Readable<str>`] that provides some convenience methods.
pub trait ReadableStrExt: Readable<Target: Deref<Target = str> + 'static> {
ext_methods! {
/// Check if the string is empty.
fn is_empty(&self) -> bool = |s: &Self::Target| s.deref().is_empty();
/// Get the length of the string.
fn len(&self) -> usize = |s: &Self::Target| s.deref().len();
/// Check if the string contains the given pattern.
fn contains(&self, pat: &str) -> bool = |s: &Self::Target, pat| s.deref().contains(pat);
}
}
impl<W> ReadableStrExt for W where W: Readable<Target: Deref<Target = str> + 'static> {}
/// An extension trait for [`Readable<HashMap<K, V, H>>`] that provides some convenience methods.
pub trait ReadableHashMapExt<K: 'static, V: 'static, H: 'static>:
Readable<Target = HashMap<K, V, H>>
{
ext_methods! {
/// Check if the hashmap is empty.
fn is_empty(&self) -> bool = HashMap::is_empty;
/// Get the length of the hashmap.
fn len(&self) -> usize = HashMap::len;
/// Get the capacity of the hashmap.
fn capacity(&self) -> usize = HashMap::capacity;
}
/// Get the value for the given key.
#[track_caller]
fn get(&self, key: &K) -> Option<ReadableRef<'_, Self, V>>
where
K: std::hash::Hash + Eq,
H: std::hash::BuildHasher,
{
<Self::Storage as AnyStorage>::try_map(self.read(), |v| v.get(key))
}
/// Check if the hashmap contains the given key.
#[track_caller]
fn contains_key(&self, key: &K) -> bool
where
K: std::hash::Hash + Eq,
H: std::hash::BuildHasher,
{
self.with(|v| v.contains_key(key))
}
}
impl<K: 'static, V: 'static, H: 'static, R> ReadableHashMapExt<K, V, H> for R where
R: Readable<Target = HashMap<K, V, H>>
{
}
/// An extension trait for [`Readable<HashSet<V, H>>`] that provides some convenience methods.
pub trait ReadableHashSetExt<V: 'static, H: 'static>: Readable<Target = HashSet<V, H>> {
ext_methods! {
/// Check if the hashset is empty.
fn is_empty(&self) -> bool = HashSet::is_empty;
/// Get the length of the hashset.
fn len(&self) -> usize = HashSet::len;
/// Get the capacity of the hashset.
fn capacity(&self) -> usize = HashSet::capacity;
}
/// Check if the hashset contains the given value.
#[track_caller]
fn contains(&self, value: &V) -> bool
where
V: std::hash::Hash + Eq,
H: std::hash::BuildHasher,
{
self.with(|v| v.contains(value))
}
}
impl<V: 'static, H: 'static, R> ReadableHashSetExt<V, H> for R where
R: Readable<Target = HashSet<V, H>>
{
}
+151 -4
View File
@@ -1,8 +1,11 @@
use std::ops::{Deref, DerefMut, IndexMut};
use std::{
collections::{HashMap, HashSet},
ops::{Deref, DerefMut, IndexMut},
};
use generational_box::{AnyStorage, UnsyncStorage};
use crate::{read::Readable, read::ReadableExt, MappedMutSignal, WriteSignal};
use crate::{ext_methods, read::Readable, read::ReadableExt, MappedMutSignal, WriteSignal};
/// A reference to a value that can be written to.
#[allow(type_alias_bounds)]
@@ -59,7 +62,7 @@ pub trait Writable: Readable {
/// # use dioxus::prelude::*;
/// fn app() -> Element {
/// let mut value = use_signal(|| String::from("hello"));
///
///
/// rsx! {
/// button {
/// onclick: move |_| {
@@ -578,4 +581,148 @@ impl<'a, T: 'static, R: Writable<Target = Vec<T>>> Iterator for WritableValueIte
}
}
impl<T, W> WritableVecExt<T> for W where W: Writable<Target = Vec<T>> {}
impl<W, T> WritableVecExt<T> for W where W: Writable<Target = Vec<T>> {}
/// An extension trait for [`Writable<String>`] that provides some convenience methods.
pub trait WritableStringExt: Writable<Target = String> {
ext_methods! {
/// Pushes a character to the end of the string.
fn push_str(&mut self, s: &str) = String::push_str;
/// Pushes a character to the end of the string.
fn push(&mut self, c: char) = String::push;
/// Pops a character from the end of the string.
fn pop(&mut self) -> Option<char> = String::pop;
/// Inserts a string at the given index.
fn insert_str(&mut self, idx: usize, s: &str) = String::insert_str;
/// Inserts a character at the given index.
fn insert(&mut self, idx: usize, c: char) = String::insert;
/// Remove a character at the given index
fn remove(&mut self, idx: usize) -> char = String::remove;
/// Replace a range of the string with the given string.
fn replace_range(&mut self, range: impl std::ops::RangeBounds<usize>, replace_with: &str) = String::replace_range;
/// Clears the string, removing all characters.
fn clear(&mut self) = String::clear;
/// Extends the string with the given iterator of characters.
fn extend(&mut self, iter: impl IntoIterator<Item = char>) = String::extend;
/// Truncates the string to the given length.
fn truncate(&mut self, len: usize) = String::truncate;
/// Splits the string off at the given index, returning the tail as a new string.
fn split_off(&mut self, at: usize) -> String = String::split_off;
}
}
impl<W> WritableStringExt for W where W: Writable<Target = String> {}
/// An extension trait for [`Writable<HashMap<K, V, H>>`] that provides some convenience methods.
pub trait WritableHashMapExt<K: 'static, V: 'static, H: 'static>:
Writable<Target = HashMap<K, V, H>>
{
ext_methods! {
/// Clears the map, removing all key-value pairs.
fn clear(&mut self) = HashMap::clear;
/// Retains only the key-value pairs that match the given predicate.
fn retain(&mut self, f: impl FnMut(&K, &mut V) -> bool) = HashMap::retain;
}
/// Inserts a key-value pair into the map. If the key was already present, the old value is returned.
#[track_caller]
fn insert(&mut self, k: K, v: V) -> Option<V>
where
K: std::cmp::Eq + std::hash::Hash,
H: std::hash::BuildHasher,
{
self.with_mut(|map: &mut HashMap<K, V, H>| map.insert(k, v))
}
/// Extends the map with the key-value pairs from the given iterator.
#[track_caller]
fn extend(&mut self, iter: impl IntoIterator<Item = (K, V)>)
where
K: std::cmp::Eq + std::hash::Hash,
H: std::hash::BuildHasher,
{
self.with_mut(|map: &mut HashMap<K, V, H>| map.extend(iter))
}
/// Removes a key from the map, returning the value at the key if the key was previously in the map.
#[track_caller]
fn remove(&mut self, k: &K) -> Option<V>
where
K: std::cmp::Eq + std::hash::Hash,
H: std::hash::BuildHasher,
{
self.with_mut(|map: &mut HashMap<K, V, H>| map.remove(k))
}
/// Get a mutable reference to the value at the given key.
#[track_caller]
fn get_mut(&mut self, k: &K) -> Option<WritableRef<'_, Self, V>>
where
K: std::cmp::Eq + std::hash::Hash,
H: std::hash::BuildHasher,
{
WriteLock::filter_map(self.write(), |map: &mut HashMap<K, V, H>| map.get_mut(k))
}
}
impl<K: 'static, V: 'static, H: 'static, R> WritableHashMapExt<K, V, H> for R where
R: Writable<Target = HashMap<K, V, H>>
{
}
/// An extension trait for [`Writable<HashSet<V, H>>`] that provides some convenience methods.
pub trait WritableHashSetExt<V: 'static, H: 'static>: Writable<Target = HashSet<V, H>> {
ext_methods! {
/// Clear the hash set.
fn clear(&mut self) = HashSet::clear;
/// Retain only the elements specified by the predicate.
fn retain(&mut self, f: impl FnMut(&V) -> bool) = HashSet::retain;
}
/// Inserts a value into the set. Returns true if the value was not already present.
#[track_caller]
fn insert(&mut self, k: V) -> bool
where
V: std::cmp::Eq + std::hash::Hash,
H: std::hash::BuildHasher,
{
self.with_mut(|set| set.insert(k))
}
/// Extends the set with the values from the given iterator.
#[track_caller]
fn extend(&mut self, iter: impl IntoIterator<Item = V>)
where
V: std::cmp::Eq + std::hash::Hash,
H: std::hash::BuildHasher,
{
self.with_mut(|set| set.extend(iter))
}
/// Removes a value from the set. Returns true if the value was present.
#[track_caller]
fn remove(&mut self, k: &V) -> bool
where
V: std::cmp::Eq + std::hash::Hash,
H: std::hash::BuildHasher,
{
self.with_mut(|set| set.remove(k))
}
}
impl<V: 'static, H: 'static, R> WritableHashSetExt<V, H> for R where
R: Writable<Target = HashSet<V, H>>
{
}