Boxed and mapped signals preparing for stores (#4413)
This PR contains just the breaking changes required for the upcoming store implementation. It adds two new primitives: Mutable mapped signals (without a different subscriber list). This lets you scope a writable value and pass it to a child component. It requires two closures to map the immutable and mutable references to the inner signal. With stores, these closures will be derived Boxed readable and writable values (ReadSignal, and WriteSignal). Any readable or writable types can be converted to a read signal or write signal with the boxed method or built in types are automatically converted into the boxed variants when passing into props * map signals mutably * boxed signals * remove mutable borrow type from boxed writable * improve map mut example * restore boxed_mut helper * make boxed readable/writable types copy * Move away from the ReadOnlySignal alias in examples * automatically convert Write * Fix some tests * ReadSignal and WriteSignal * move read only signal alias * make maps copy * store prototype * implement derive macro * Handle generics in the derive macro * Make Selector boxable * derive boxed conversion * Get rid of store type * Remove hardcoded type from derive macro * selector -> store * Refactor store implementation * more vec methods * todo mvc store example * Implement Storable for HashMap * fix write * restore sorting logic in todomvc * derive readable and writable * improve todomvc store example * Result store * Option store * collect dead subscribers * fix clippy and formatting * recognize some common foreign types automatically * fix foreign stores in props * slice and array selectors * clean up store example * remove stores * writable is child owned * fix typo * fix clippy * fix doc tests
This commit is contained in:
1 parent
6919f72abc
commit
d8d11db403
64 files changed
+1307
-726
No files matched your search
+285
-33
@@ -1,10 +1,13 @@
|
||||
use std::ops::{DerefMut, IndexMut};
|
||||
use std::ops::{Deref, DerefMut, IndexMut};
|
||||
|
||||
use crate::read::Readable;
|
||||
use generational_box::{AnyStorage, UnsyncStorage};
|
||||
|
||||
/// A reference to a value that can be read from.
|
||||
use crate::{read::Readable, read::ReadableExt, MappedMutSignal, WriteSignal};
|
||||
|
||||
/// A reference to a value that can be written to.
|
||||
#[allow(type_alias_bounds)]
|
||||
pub type WritableRef<'a, T: Writable, O = <T as Readable>::Target> = T::Mut<'a, O>;
|
||||
pub type WritableRef<'a, T: Writable, O = <T as Readable>::Target> =
|
||||
WriteLock<'a, O, <T as Readable>::Storage, <T as Writable>::WriteMetadata>;
|
||||
|
||||
/// A trait for states that can be written to like [`crate::Signal`]. You may choose to accept this trait as a parameter instead of the concrete type to allow for more flexibility in your API.
|
||||
///
|
||||
@@ -32,27 +35,8 @@ pub type WritableRef<'a, T: Writable, O = <T as Readable>::Target> = T::Mut<'a,
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Writable: Readable {
|
||||
/// The type of the reference.
|
||||
type Mut<'a, R: ?Sized + 'static>: DerefMut<Target = R>;
|
||||
|
||||
/// Map the reference to a new type.
|
||||
fn map_mut<I: ?Sized, U: ?Sized, F: FnOnce(&mut I) -> &mut U>(
|
||||
ref_: Self::Mut<'_, I>,
|
||||
f: F,
|
||||
) -> Self::Mut<'_, U>;
|
||||
|
||||
/// Try to map the reference to a new type.
|
||||
fn try_map_mut<I: ?Sized, U: ?Sized, F: FnOnce(&mut I) -> Option<&mut U>>(
|
||||
ref_: Self::Mut<'_, I>,
|
||||
f: F,
|
||||
) -> Option<Self::Mut<'_, U>>;
|
||||
|
||||
/// Downcast a mutable reference in a RefMut to a more specific lifetime
|
||||
///
|
||||
/// This function enforces the variance of the lifetime parameter `'a` in Ref.
|
||||
fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'static>(
|
||||
mut_: Self::Mut<'a, T>,
|
||||
) -> Self::Mut<'b, T>;
|
||||
/// Additional data associated with the write reference.
|
||||
type WriteMetadata: 'static;
|
||||
|
||||
/// Get a mutable reference to the value. If the value has been dropped, this will panic.
|
||||
#[track_caller]
|
||||
@@ -63,7 +47,7 @@ pub trait Writable: Readable {
|
||||
/// Try to get a mutable reference to the value.
|
||||
#[track_caller]
|
||||
fn try_write(&mut self) -> Result<WritableRef<'_, Self>, generational_box::BorrowMutError> {
|
||||
self.try_write_unchecked().map(Self::downcast_lifetime_mut)
|
||||
self.try_write_unchecked().map(WriteLock::downcast_lifetime)
|
||||
}
|
||||
|
||||
/// Try to get a mutable reference to the value without checking the lifetime. This will update any subscribers.
|
||||
@@ -80,6 +64,255 @@ pub trait Writable: Readable {
|
||||
fn write_unchecked(&self) -> WritableRef<'static, Self> {
|
||||
self.try_write_unchecked().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// A mutable reference to a writable value. This reference acts similarly to [`std::cell::RefMut`], but it has extra debug information
|
||||
/// and integrates with the reactive system to automatically update dependents.
|
||||
///
|
||||
/// [`WriteLock`] implements [`DerefMut`] which means you can call methods on the inner value just like you would on a mutable reference
|
||||
/// to the inner value. If you need to get the inner reference directly, you can call [`WriteLock::deref_mut`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn app() -> Element {
|
||||
/// let mut value = use_signal(|| String::from("hello"));
|
||||
///
|
||||
/// rsx! {
|
||||
/// button {
|
||||
/// onclick: move |_| {
|
||||
/// let mut mutable_reference = value.write();
|
||||
///
|
||||
/// // You call methods like `push_str` on the reference just like you would with the inner String
|
||||
/// mutable_reference.push_str("world");
|
||||
/// },
|
||||
/// "Click to add world to the string"
|
||||
/// }
|
||||
/// div { "{value}" }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Matching on WriteLock
|
||||
///
|
||||
/// You need to get the inner mutable reference with [`WriteLock::deref_mut`] before you match the inner value. If you try to match
|
||||
/// without calling [`WriteLock::deref_mut`], you will get an error like this:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # use dioxus::prelude::*;
|
||||
/// #[derive(Debug)]
|
||||
/// enum Colors {
|
||||
/// Red(u32),
|
||||
/// Green
|
||||
/// }
|
||||
/// fn app() -> Element {
|
||||
/// let mut value = use_signal(|| Colors::Red(0));
|
||||
///
|
||||
/// rsx! {
|
||||
/// button {
|
||||
/// onclick: move |_| {
|
||||
/// let mut mutable_reference = value.write();
|
||||
///
|
||||
/// match mutable_reference {
|
||||
/// // Since we are matching on the `Write` type instead of &mut Colors, we can't match on the enum directly
|
||||
/// Colors::Red(brightness) => *brightness += 1,
|
||||
/// Colors::Green => {}
|
||||
/// }
|
||||
/// },
|
||||
/// "Click to add brightness to the red color"
|
||||
/// }
|
||||
/// div { "{value:?}" }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// error[E0308]: mismatched types
|
||||
/// --> src/main.rs:18:21
|
||||
/// |
|
||||
/// 16 | match mutable_reference {
|
||||
/// | ----------------- this expression has type `dioxus::prelude::Write<'_, Colors>`
|
||||
/// 17 | // Since we are matching on the `Write` t...
|
||||
/// 18 | Colors::Red(brightness) => *brightness += 1,
|
||||
/// | ^^^^^^^^^^^^^^^^^^^^^^^ expected `Write<'_, Colors>`, found `Colors`
|
||||
/// |
|
||||
/// = note: expected struct `dioxus::prelude::Write<'_, Colors, >`
|
||||
/// found enum `Colors`
|
||||
/// ```
|
||||
///
|
||||
/// Instead, you need to call deref mut on the reference to get the inner value **before** you match on it:
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::ops::DerefMut;
|
||||
/// # use dioxus::prelude::*;
|
||||
/// #[derive(Debug)]
|
||||
/// enum Colors {
|
||||
/// Red(u32),
|
||||
/// Green
|
||||
/// }
|
||||
/// fn app() -> Element {
|
||||
/// let mut value = use_signal(|| Colors::Red(0));
|
||||
///
|
||||
/// rsx! {
|
||||
/// button {
|
||||
/// onclick: move |_| {
|
||||
/// let mut mutable_reference = value.write();
|
||||
///
|
||||
/// // DerefMut converts the `Write` into a `&mut Colors`
|
||||
/// match mutable_reference.deref_mut() {
|
||||
/// // Now we can match on the inner value
|
||||
/// Colors::Red(brightness) => *brightness += 1,
|
||||
/// Colors::Green => {}
|
||||
/// }
|
||||
/// },
|
||||
/// "Click to add brightness to the red color"
|
||||
/// }
|
||||
/// div { "{value:?}" }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Generics
|
||||
/// - T is the current type of the write
|
||||
/// - S is the storage type of the signal. This type determines if the signal is local to the current thread, or it can be shared across threads.
|
||||
/// - D is the additional data associated with the write reference. This is used by signals to track when the write is dropped
|
||||
pub struct WriteLock<'a, T: ?Sized + 'static, S: AnyStorage = UnsyncStorage, D = ()> {
|
||||
write: S::Mut<'a, T>,
|
||||
data: D,
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized, S: AnyStorage> WriteLock<'a, T, S> {
|
||||
/// Create a new write reference
|
||||
pub fn new(write: S::Mut<'a, T>) -> Self {
|
||||
Self { write, data: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized, S: AnyStorage, D> WriteLock<'a, T, S, D> {
|
||||
/// Create a new write reference with additional data.
|
||||
pub fn new_with_metadata(write: S::Mut<'a, T>, data: D) -> Self {
|
||||
Self { write, data }
|
||||
}
|
||||
|
||||
/// Get the inner value of the write reference.
|
||||
pub fn into_inner(self) -> S::Mut<'a, T> {
|
||||
self.write
|
||||
}
|
||||
|
||||
/// Get the additional data associated with the write reference.
|
||||
pub fn data(&self) -> &D {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Split into the inner value and the additional data.
|
||||
pub fn into_parts(self) -> (S::Mut<'a, T>, D) {
|
||||
(self.write, self.data)
|
||||
}
|
||||
|
||||
/// Map the metadata of the write reference to a new type.
|
||||
pub fn map_metadata<O>(self, f: impl FnOnce(D) -> O) -> WriteLock<'a, T, S, O> {
|
||||
WriteLock {
|
||||
write: self.write,
|
||||
data: f(self.data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the mutable reference to the signal's value to a new type.
|
||||
pub fn map<O: ?Sized>(
|
||||
myself: Self,
|
||||
f: impl FnOnce(&mut T) -> &mut O,
|
||||
) -> WriteLock<'a, O, S, D> {
|
||||
let Self { write, data, .. } = myself;
|
||||
WriteLock {
|
||||
write: S::map_mut(write, f),
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to map the mutable reference to the signal's value to a new type
|
||||
pub fn filter_map<O: ?Sized>(
|
||||
myself: Self,
|
||||
f: impl FnOnce(&mut T) -> Option<&mut O>,
|
||||
) -> Option<WriteLock<'a, O, S, D>> {
|
||||
let Self { write, data, .. } = myself;
|
||||
let write = S::try_map_mut(write, f);
|
||||
write.map(|write| WriteLock { write, data })
|
||||
}
|
||||
|
||||
/// Downcast the lifetime of the mutable reference to the signal's value.
|
||||
///
|
||||
/// This function enforces the variance of the lifetime parameter `'a` in Mut. Rust will typically infer this cast with a concrete type, but it cannot with a generic type.
|
||||
pub fn downcast_lifetime<'b>(mut_: Self) -> WriteLock<'b, T, S, D>
|
||||
where
|
||||
'a: 'b,
|
||||
{
|
||||
WriteLock {
|
||||
write: S::downcast_lifetime_mut(mut_.write),
|
||||
data: mut_.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, D> Deref for WriteLock<'_, T, S, D>
|
||||
where
|
||||
S: AnyStorage,
|
||||
T: ?Sized + 'static,
|
||||
{
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.write
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, D> DerefMut for WriteLock<'_, T, S, D>
|
||||
where
|
||||
S: AnyStorage,
|
||||
T: ?Sized + 'static,
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.write
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension trait for [`Writable`] that provides some convenience methods.
|
||||
pub trait WritableExt: Writable {
|
||||
/// Map the readable type to a new type. This lets you provide a view into a readable type without needing to clone the inner value.
|
||||
///
|
||||
/// Anything that subscribes to the readable value will be rerun whenever the original value changes, even if the view does not change. If you want to memorize the view, you can use a [`crate::Memo`] instead.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn List(list: Signal<Vec<i32>>) -> Element {
|
||||
/// rsx! {
|
||||
/// for index in 0..list.len() {
|
||||
/// // We can use the `map` method to provide a view into the single item in the list that the child component will render
|
||||
/// Item { item: list.map_mut(move |v| &v[index], move |v| &mut v[index]) }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // The child component doesn't need to know that the mapped value is coming from a list
|
||||
/// #[component]
|
||||
/// fn Item(item: WriteSignal<i32>) -> Element {
|
||||
/// rsx! {
|
||||
/// button {
|
||||
/// onclick: move |_| *item.write() += 1,
|
||||
/// "{item}"
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn map_mut<O, F, FMut>(self, f: F, f_mut: FMut) -> MappedMutSignal<O, Self, F, FMut>
|
||||
where
|
||||
Self: Clone + Sized + 'static,
|
||||
O: ?Sized + 'static,
|
||||
F: Fn(&Self::Target) -> &O + 'static,
|
||||
FMut: Fn(&mut Self::Target) -> &mut O + 'static,
|
||||
{
|
||||
MappedMutSignal::new(self, f, f_mut)
|
||||
}
|
||||
|
||||
/// Run a function with a mutable reference to the value. If the value has been dropped, this will panic.
|
||||
#[track_caller]
|
||||
@@ -115,7 +348,7 @@ pub trait Writable: Readable {
|
||||
where
|
||||
Self::Target: std::ops::IndexMut<I>,
|
||||
{
|
||||
Self::map_mut(self.write(), |v| v.index_mut(index))
|
||||
WriteLock::map(self.write(), |v| v.index_mut(index))
|
||||
}
|
||||
|
||||
/// Takes the value out of the Signal, leaving a Default in its place.
|
||||
@@ -137,6 +370,25 @@ pub trait Writable: Readable {
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Writable + ?Sized> WritableExt for W {}
|
||||
|
||||
/// An extension trait for [`Writable`] values that can be boxed into a trait object.
|
||||
pub trait WritableBoxedExt: Writable<Storage = UnsyncStorage> {
|
||||
/// Box the writable value into a trait object. This is useful for passing around writable values without knowing their concrete type.
|
||||
fn boxed_mut(self) -> WriteSignal<Self::Target>
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
{
|
||||
WriteSignal::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Writable<Storage = UnsyncStorage> + 'static> WritableBoxedExt for T {
|
||||
fn boxed_mut(self) -> WriteSignal<Self::Target> {
|
||||
WriteSignal::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension trait for [`Writable<Option<T>>`]` that provides some convenience methods.
|
||||
pub trait WritableOptionExt<T: 'static>: Writable<Target = Option<T>> {
|
||||
/// Gets the value out of the Option, or inserts the given value if the Option is empty.
|
||||
@@ -151,16 +403,16 @@ pub trait WritableOptionExt<T: 'static>: Writable<Target = Option<T>> {
|
||||
let is_none = self.read().is_none();
|
||||
if is_none {
|
||||
self.with_mut(|v| *v = Some(default()));
|
||||
Self::map_mut(self.write(), |v| v.as_mut().unwrap())
|
||||
WriteLock::map(self.write(), |v| v.as_mut().unwrap())
|
||||
} else {
|
||||
Self::map_mut(self.write(), |v| v.as_mut().unwrap())
|
||||
WriteLock::map(self.write(), |v| v.as_mut().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to write the inner value of the Option.
|
||||
#[track_caller]
|
||||
fn as_mut(&mut self) -> Option<WritableRef<'_, Self, T>> {
|
||||
Self::try_map_mut(self.write(), |v: &mut Option<T>| v.as_mut())
|
||||
WriteLock::filter_map(self.write(), |v: &mut Option<T>| v.as_mut())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +488,7 @@ pub trait WritableVecExt<T: 'static>: Writable<Target = Vec<T>> {
|
||||
/// Try to mutably get an element from the vector.
|
||||
#[track_caller]
|
||||
fn get_mut(&mut self, index: usize) -> Option<WritableRef<'_, Self, T>> {
|
||||
Self::try_map_mut(self.write(), |v: &mut Vec<T>| v.get_mut(index))
|
||||
WriteLock::filter_map(self.write(), |v: &mut Vec<T>| v.get_mut(index))
|
||||
}
|
||||
|
||||
/// Gets an iterator over the values of the vector.
|
||||
@@ -264,11 +516,11 @@ impl<'a, T: 'static, R: Writable<Target = Vec<T>>> Iterator for WritableValueIte
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let index = self.index;
|
||||
self.index += 1;
|
||||
R::try_map_mut(
|
||||
WriteLock::filter_map(
|
||||
self.value.try_write_unchecked().unwrap(),
|
||||
|v: &mut Vec<T>| v.get_mut(index),
|
||||
)
|
||||
.map(R::downcast_lifetime_mut)
|
||||
.map(WriteLock::downcast_lifetime)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user