mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: iteration over reactive store list
This commit is contained in:
parent
e5c159f7a5
commit
3515469835
16 changed files with 619 additions and 9 deletions
20
examples/stores/Cargo.toml
Normal file
20
examples/stores/Cargo.toml
Normal 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"
|
3
examples/stores/Makefile.toml
Normal file
3
examples/stores/Makefile.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
]
|
11
examples/stores/README.md
Normal file
11
examples/stores/README.md
Normal 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.
|
8
examples/stores/index.html
Normal file
8
examples/stores/index.html
Normal 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>
|
BIN
examples/stores/public/favicon.ico
Normal file
BIN
examples/stores/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
2
examples/stores/rust-toolchain.toml
Normal file
2
examples/stores/rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "stable" # test change
|
132
examples/stores/src/lib.rs
Normal file
132
examples/stores/src/lib.rs
Normal 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>
|
||||
}
|
||||
}
|
7
examples/stores/src/main.rs
Normal file
7
examples/stores/src/main.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use leptos::prelude::*;
|
||||
use stores::App;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
261
reactive_stores/src/iter.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue