feat: iteration over reactive store list

This commit is contained in:
Greg Johnston 2024-07-10 22:04:45 -04:00
parent e5c159f7a5
commit 3515469835
16 changed files with 619 additions and 9 deletions

View file

@ -0,0 +1,20 @@
[package]
name = "stores"
version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = 'z'
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
reactive_stores = { path = "../../reactive_stores" }
reactive_stores_macro = { path = "../../reactive_stores_macro" }
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"
web-sys = "0.3"

View file

@ -0,0 +1,3 @@
extend = [
{ path = "../cargo-make/main.toml" },
]

11
examples/stores/README.md Normal file
View file

@ -0,0 +1,11 @@
# Leptos Counter Example
This example creates a simple counter in a client side rendered app with Rust and WASM!
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "stable" # test change

132
examples/stores/src/lib.rs Normal file
View file

@ -0,0 +1,132 @@
use leptos::prelude::*;
use reactive_stores::{
AtIndex, Store, StoreField, StoreFieldIterator, Subfield,
};
use reactive_stores_macro::Store;
#[derive(Debug, Store)]
struct Todos {
user: String,
todos: Vec<Todo>,
}
#[derive(Debug, Store)]
struct Todo {
label: String,
completed: bool,
}
impl Todo {
pub fn new(label: impl ToString) -> Self {
Self {
label: label.to_string(),
completed: false,
}
}
}
fn data() -> Todos {
Todos {
user: "Bob".to_string(),
todos: vec![
Todo {
label: "Create reactive store".to_string(),
completed: true,
},
Todo {
label: "???".to_string(),
completed: false,
},
Todo {
label: "Profit".to_string(),
completed: false,
},
],
}
}
#[component]
pub fn App() -> impl IntoView {
let store = Store::new(data());
let input_ref = NodeRef::new();
let rows = move || {
store
.todos()
.iter()
.enumerate()
.map(|(idx, todo)| view! { <TodoRow store idx todo/> })
.collect_view()
};
view! {
<form on:submit=move |ev| {
ev.prevent_default();
store.todos().write().push(Todo::new(input_ref.get().unwrap().value()));
}>
<label>"Add a Todo" <input type="text" node_ref=input_ref/></label>
<input type="submit"/>
</form>
<ol>{rows}</ol>
<div style="display: flex"></div>
}
}
#[component]
fn TodoRow(
store: Store<Todos>,
idx: usize,
// to be fair, this is gross
todo: AtIndex<Subfield<Store<Todos>, Todos, Vec<Todo>>, Vec<Todo>>,
) -> impl IntoView {
let completed = todo.completed();
let title = todo.label();
let editing = RwSignal::new(false);
view! {
<li
style:text-decoration=move || {
completed.get().then_some("line-through").unwrap_or_default()
}
class:foo=move || completed.get()
>
<p
class:hidden=move || editing.get()
on:click=move |_| {
editing.update(|n| *n = !*n);
}
>
{move || title.get()}
</p>
<input
class:hidden=move || !(editing.get())
type="text"
prop:value=move || title.get()
on:change=move |ev| {
title.set(event_target_value(&ev));
editing.set(false);
}
on:blur=move |_| editing.set(false)
autofocus
/>
<input
type="checkbox"
prop:checked=move || completed.get()
on:click=move |_| { completed.update(|n| *n = !*n) }
/>
<button on:click=move |_| {
store
.todos()
.update(|todos| {
todos.remove(idx);
});
}>"X"</button>
</li>
}
}

View file

@ -0,0 +1,7 @@
use leptos::prelude::*;
use stores::App;
pub fn main() {
console_error_panic_hook::set_once();
mount_to_body(App)
}

View file

@ -430,3 +430,100 @@ where
Display::fmt(&**self, f)
}
}
pub struct MappedMutArc<Inner, U>
where
Inner: Deref,
{
inner: Inner,
map_fn: Arc<dyn Fn(&Inner::Target) -> &U>,
map_fn_mut: Arc<dyn Fn(&mut Inner::Target) -> &mut U>,
}
impl<Inner, U> Clone for MappedMutArc<Inner, U>
where
Inner: Clone + Deref,
{
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
map_fn: self.map_fn.clone(),
map_fn_mut: self.map_fn_mut.clone(),
}
}
}
impl<Inner, U> Debug for MappedMutArc<Inner, U>
where
Inner: Debug + Deref,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MappedMutArc")
.field("inner", &self.inner)
.finish_non_exhaustive()
}
}
impl<Inner, U> UntrackableGuard for MappedMutArc<Inner, U>
where
Inner: UntrackableGuard,
{
fn untrack(&mut self) {
self.inner.untrack();
}
}
impl<Inner, U> MappedMutArc<Inner, U>
where
Inner: Deref,
{
pub fn new(
inner: Inner,
map_fn: impl Fn(&Inner::Target) -> &U + 'static,
map_fn_mut: impl Fn(&mut Inner::Target) -> &mut U + 'static,
) -> Self {
Self {
inner,
map_fn: Arc::new(map_fn),
map_fn_mut: Arc::new(map_fn_mut),
}
}
}
impl<Inner, U> Deref for MappedMutArc<Inner, U>
where
Inner: Deref,
{
type Target = U;
fn deref(&self) -> &Self::Target {
(self.map_fn)(self.inner.deref())
}
}
impl<Inner, U> DerefMut for MappedMutArc<Inner, U>
where
Inner: DerefMut,
{
fn deref_mut(&mut self) -> &mut Self::Target {
(self.map_fn_mut)(self.inner.deref_mut())
}
}
impl<Inner, U: PartialEq> PartialEq for MappedMutArc<Inner, U>
where
Inner: Deref,
{
fn eq(&self, other: &Self) -> bool {
**self == **other
}
}
impl<Inner, U: Display> Display for MappedMutArc<Inner, U>
where
Inner: Deref,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&**self, f)
}
}

View file

@ -115,7 +115,7 @@ impl<T> DefinedAt for ReadSignal<T> {
impl<T: 'static> IsDisposed for ReadSignal<T> {
fn is_disposed(&self) -> bool {
self.inner.exists()
!self.inner.exists()
}
}

View file

@ -244,7 +244,7 @@ impl<T> DefinedAt for RwSignal<T> {
impl<T: 'static> IsDisposed for RwSignal<T> {
fn is_disposed(&self) -> bool {
self.inner.exists()
!self.inner.exists()
}
}

View file

@ -109,7 +109,7 @@ impl<T> DefinedAt for WriteSignal<T> {
impl<T: 'static> IsDisposed for WriteSignal<T> {
fn is_disposed(&self) -> bool {
self.inner.exists()
!self.inner.exists()
}
}

261
reactive_stores/src/iter.rs Normal file
View file

@ -0,0 +1,261 @@
use crate::{
path::{StorePath, StorePathSegment},
store_field::StoreField,
};
use reactive_graph::{
signal::{
guards::{MappedMutArc, WriteGuard},
ArcTrigger,
},
traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard,
Writeable,
},
};
use std::{
iter,
marker::PhantomData,
ops::{DerefMut, Index, IndexMut},
panic::Location,
sync::{Arc, RwLock},
};
#[derive(Debug)]
pub struct AtIndex<Inner, Prev>
where
Inner: StoreField<Prev>,
{
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
inner: Inner,
index: usize,
ty: PhantomData<Prev>,
}
impl<Inner, Prev> Clone for AtIndex<Inner, Prev>
where
Inner: StoreField<Prev> + Clone,
{
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
inner: self.inner.clone(),
index: self.index,
ty: self.ty,
}
}
}
impl<Inner, Prev> Copy for AtIndex<Inner, Prev> where
Inner: StoreField<Prev> + Copy
{
}
impl<Inner, Prev> AtIndex<Inner, Prev>
where
Inner: StoreField<Prev>,
{
#[track_caller]
pub fn new(inner: Inner, index: usize) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
inner,
index,
ty: PhantomData,
}
}
}
impl<Inner, Prev> StoreField<Prev::Output> for AtIndex<Inner, Prev>
where
Inner: StoreField<Prev>,
Prev: IndexMut<usize>,
Prev::Output: Sized,
{
type Orig = Inner::Orig;
type Reader = MappedMutArc<Inner::Reader, Prev::Output>;
type Writer =
MappedMutArc<WriteGuard<ArcTrigger, Inner::Writer>, Prev::Output>;
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.path()
.into_iter()
.chain(iter::once(self.index.into()))
}
fn data(&self) -> Arc<RwLock<Self::Orig>> {
self.inner.data()
}
fn get_trigger(&self, path: StorePath) -> ArcTrigger {
self.inner.get_trigger(path)
}
fn reader(&self) -> Option<Self::Reader> {
let inner = self.inner.reader()?;
let index = self.index;
Some(MappedMutArc::new(
inner,
move |n| &n[index],
move |n| &mut n[index],
))
}
fn writer(&self) -> Option<Self::Writer> {
let trigger = self.get_trigger(self.path().into_iter().collect());
let inner = WriteGuard::new(trigger, self.inner.writer()?);
let index = self.index;
Some(MappedMutArc::new(
inner,
move |n| &n[index],
move |n| &mut n[index],
))
}
}
impl<Inner, Prev> DefinedAt for AtIndex<Inner, Prev>
where
Inner: StoreField<Prev>,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<Inner, Prev> IsDisposed for AtIndex<Inner, Prev>
where
Inner: StoreField<Prev> + IsDisposed,
{
fn is_disposed(&self) -> bool {
self.inner.is_disposed()
}
}
impl<Inner, Prev> Trigger for AtIndex<Inner, Prev>
where
Inner: StoreField<Prev>,
Prev: IndexMut<usize> + 'static,
Prev::Output: Sized,
{
fn trigger(&self) {
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.trigger();
}
}
impl<Inner, Prev> Track for AtIndex<Inner, Prev>
where
Inner: StoreField<Prev> + Send + Sync + Clone + 'static,
Prev: IndexMut<usize> + 'static,
Prev::Output: Sized + 'static,
{
fn track(&self) {
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.track();
}
}
impl<Inner, Prev> ReadUntracked for AtIndex<Inner, Prev>
where
Inner: StoreField<Prev>,
Prev: IndexMut<usize>,
Prev::Output: Sized,
{
type Value = <Self as StoreField<Prev::Output>>::Reader;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.reader()
}
}
impl<Inner, Prev> Writeable for AtIndex<Inner, Prev>
where
Inner: StoreField<Prev>,
Prev: IndexMut<usize> + 'static,
Prev::Output: Sized + 'static,
{
type Value = Prev::Output;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.writer()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.writer().map(|mut writer| {
writer.untrack();
writer
})
}
}
pub trait StoreFieldIterator<Prev>: Sized {
fn iter(self) -> StoreFieldIter<Self, Prev>;
}
impl<Inner, Prev> StoreFieldIterator<Prev> for Inner
where
Inner: StoreField<Prev>,
Prev::Output: Sized,
Prev: IndexMut<usize> + AsRef<[Prev::Output]>,
{
fn iter(self) -> StoreFieldIter<Inner, Prev> {
// reactively track changes to this field
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.track();
// get the current length of the field by accessing slice
let len = self.reader().map(|n| n.as_ref().len()).unwrap_or(0);
// return the iterator
StoreFieldIter {
inner: self,
idx: 0,
len,
prev: PhantomData,
}
}
}
pub struct StoreFieldIter<Inner, Prev> {
inner: Inner,
idx: usize,
len: usize,
prev: PhantomData<Prev>,
}
impl<Inner, Prev> Iterator for StoreFieldIter<Inner, Prev>
where
Inner: StoreField<Prev> + Clone + 'static,
Prev: IndexMut<usize> + 'static,
Prev::Output: Sized + 'static,
{
type Item = AtIndex<Inner, Prev>;
fn next(&mut self) -> Option<Self::Item> {
if self.idx < self.len {
let field = AtIndex {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
index: self.idx,
inner: self.inner.clone(),
ty: PhantomData,
};
self.idx += 1;
Some(field)
} else {
None
}
}
}

View file

@ -13,13 +13,15 @@ use std::{
sync::{Arc, RwLock},
};
mod iter;
mod path;
mod read_store_field;
mod store_field;
mod subfield;
pub use iter::*;
use path::StorePath;
use store_field::StoreField;
pub use store_field::StoreField;
pub use subfield::Subfield;
#[derive(Debug, Default)]
@ -121,7 +123,8 @@ impl<T: 'static> Track for ArcStore<T> {
impl<T: 'static> Trigger for ArcStore<T> {
fn trigger(&self) {
self.get_trigger(self.path().collect()).trigger();
self.get_trigger(self.path().into_iter().collect())
.trigger();
}
}
@ -215,7 +218,7 @@ impl<T: 'static> Trigger for Store<T> {
#[cfg(test)]
mod tests {
use crate::{self as reactive_stores, Store};
use crate::{self as reactive_stores, Store, StoreFieldIterator};
use reactive_graph::{
effect::Effect,
traits::{Read, ReadUntracked, Set, Update, Writeable},
@ -315,4 +318,70 @@ mod tests {
// the effect doesn't read from `todos`, so the count should not have changed
assert_eq!(combined_count.load(Ordering::Relaxed), 4);
}
#[tokio::test]
async fn other_field_does_not_notify() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *store.todos().read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.user().set("Greg".into());
tick().await;
store.user().set("Carol".into());
tick().await;
store.user().update(|name| name.push_str("!!!"));
tick().await;
// the effect reads from `user`, so it should trigger every time
assert_eq!(combined_count.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn iterator_tracks_the_field() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", store.todos().iter().collect::<Vec<_>>());
combined_count.store(1, Ordering::Relaxed);
}
});
tick().await;
store
.todos()
.write()
.push(Todo::new("Create reactive store?"));
tick().await;
store.todos().write().push(Todo::new("???"));
tick().await;
store.todos().write().push(Todo::new("Profit!"));
// the effect reads from `user`, so it should trigger every time
assert_eq!(combined_count.load(Ordering::Relaxed), 1);
}
}

View file

@ -18,7 +18,7 @@ impl StorePath {
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct StorePathSegment(usize);
pub struct StorePathSegment(pub(crate) usize);
impl From<usize> for StorePathSegment {
fn from(value: usize) -> Self {

View file

@ -52,7 +52,7 @@ where
trigger
}
fn path(&self) -> impl Iterator<Item = StorePathSegment> {
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
iter::empty()
}
@ -93,7 +93,7 @@ where
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.try_get_value()
.map(|n| n.path().collect::<Vec<_>>())
.map(|n| n.path().into_iter().collect::<Vec<_>>())
.unwrap_or_else(unwrap_signal!(self))
}