Compare commits

..

5 Commits

Author SHA1 Message Date
Greg Johnston
23529b2e0b fix: use unkeyed paths for all store patching (closes #4486) 2025-12-14 17:12:21 -05:00
Greg Johnston
8535a10bd7 chore: add a regression test to ensure correct behavior for keyed store fields (see discussion in #4473) (#4483) 2025-12-14 15:47:48 -05:00
Greg Johnston
7864a12967 chore: resolve new warnings (#4485) 2025-12-13 14:00:19 -05:00
dependabot[bot]
9733cdcfe1 chore(deps): bump actions/checkout from 5 to 6 (#4461)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-28 12:28:14 -05:00
Marc-Stefan Cassola
1aaa716dfc examples: use ShowLet where appropriate in examples (#4467) 2025-11-28 12:00:26 -05:00
31 changed files with 367 additions and 121 deletions

View File

@@ -18,7 +18,7 @@ jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: {toolchain: "nightly-2025-07-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
- name: Install Glib

View File

@@ -63,6 +63,6 @@ jobs:
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2

View File

@@ -19,7 +19,7 @@ jobs:
matrix: ${{ steps.set-example-changed.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get example files that changed

View File

@@ -17,7 +17,7 @@ jobs:
EXCLUDED_EXAMPLES: cargo-make
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install jq
run: sudo apt-get install jq
- name: Set Matrix

View File

@@ -13,7 +13,7 @@ jobs:
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get source files that changed

View File

@@ -13,7 +13,7 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install jq
run: sudo apt-get install jq
- name: Set Matrix

View File

@@ -12,7 +12,7 @@ jobs:
contents: write # To push a branch
pull-requests: write # To create a PR from that branch
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install mdbook

View File

@@ -53,7 +53,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:

View File

@@ -27,7 +27,7 @@ tokio = { version = "1.39", features = [
], optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
wasm-bindgen = "0.2.92"
wasm-bindgen = "0.2.105"
web-sys = { version = "0.3.69", features = [
"AddEventListenerOptions",
"Document",

View File

@@ -510,11 +510,9 @@ if (window.hljs) {
});
view! {
<pre><code class="language-rust">{code.await}</code></pre>
{
move || script.get().map(|script| {
view! { <Script>{script}</Script> }
})
}
<ShowLet some=script let:script>
<Script>{script}</Script>
</ShowLet>
}
})
};
@@ -567,11 +565,9 @@ if (window.hljs) {
});
view! {
<pre><code class="language-rust">{code.await}</code></pre>
{
move || script.get().map(|script| {
view! { <Script>{script}</Script> }
})
}
<ShowLet some=script let:script>
<Script>{script}</Script>
</ShowLet>
}
})
};

View File

@@ -25,7 +25,7 @@ log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
wasm-bindgen = "0.2.93"
wasm-bindgen = "0.2.105"
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
send_wrapper = "0.6.0"
@@ -46,12 +46,12 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"

View File

@@ -145,14 +145,11 @@ fn Story(story: api::Story) -> impl IntoView {
Either::Left(
view! {
<span>
{"by "}
{story
.user
.map(|user| {
view! {
<A href=format!("/users/{user}")>{user.clone()}</A>
}
})} {format!(" {} | ", story.time_ago)}
"by "
<ShowLet some=story.user let:user>
<A href=format!("/users/{user}")>{user.clone()}</A>
</ShowLet>
{format!(" {} | ", story.time_ago)}
<A href=format!(
"/stories/{}",
story.id,

View File

@@ -30,17 +30,13 @@ pub fn Story() -> impl IntoView {
<h1>{story.title}</h1>
</a>
<span class="host">"(" {story.domain} ")"</span>
{story
.user
.map(|user| {
view! {
<p class="meta">
{story.points} " points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>
}
})}
<ShowLet some=story.user let:user>
<p class="meta">
{story.points} " points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>
</ShowLet>
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">

View File

@@ -26,7 +26,7 @@ tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }
http = { version = "1.1", optional = true }
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2.93"
wasm-bindgen = "0.2.105"
send_wrapper = { version = "0.6.0", features = ["futures"] }
[features]

View File

@@ -133,7 +133,9 @@ fn Story(story: api::Story) -> impl IntoView {
Either::Left(view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
<ShowLet some=story.user let:user>
<A href=format!("/users/{user}")>{user.clone()}</A>
</ShowLet>
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {

View File

@@ -40,18 +40,20 @@ impl LazyRoute for StoryRoute {
<Meta name="description" content=story.title.clone()/>
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
<ShowLet some=story.user let:user>
<p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>
</ShowLet>
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">

View File

@@ -143,8 +143,10 @@ fn Story(story: api::Story) -> impl IntoView {
{if story.story_type != "job" {
Either::Left(view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
"by "
<ShowLet some=story.user let:user>
<A href=format!("/users/{user}")>{user.clone()}</A>
</ShowLet>
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {

View File

@@ -32,18 +32,20 @@ pub fn Story() -> impl IntoView {
<Meta name="description" content=story.title.clone()/>
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
<ShowLet some=story.user let:user>
<p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>
</ShowLet>
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">

View File

@@ -139,14 +139,11 @@ fn Story(story: api::Story) -> impl IntoView {
Either::Left(
view! {
<span>
{"by "}
{story
.user
.map(|user| {
view! {
<A href=format!("/users/{user}")>{user.clone()}</A>
}
})} {format!(" {} | ", story.time_ago)}
"by "
<ShowLet some=story.user let:user>
<A href=format!("/users/{user}")>{user.clone()}</A>
</ShowLet>
{format!(" {} | ", story.time_ago)}
<A href=format!(
"/stories/{}",
story.id,

View File

@@ -35,17 +35,13 @@ pub fn Story() -> impl IntoView {
<h1>{story.title}</h1>
</a>
<span class="host">"("{story.domain}")"</span>
{story
.user
.map(|user| {
view! {
<p class="meta">
{story.points} " points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>
}
})}
<ShowLet some=story.user let:user>
<p class="meta">
{story.points} " points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>
</ShowLet>
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">

View File

@@ -564,17 +564,12 @@ pub fn FileUploadWithProgress() -> impl IntoView {
<input type="submit" />
</form>
{move || filename.get().map(|filename| view! { <p>Uploading {filename}</p> })}
{move || {
max.get()
.map(|max| {
view! {
<progress
max=max
value=move || current.get().unwrap_or_default()
></progress>
}
})
}}
<ShowLet some=max let:max>
<progress
max=max
value=move || current.get().unwrap_or_default()
></progress>
</ShowLet>
}
}
#[component]

View File

@@ -663,26 +663,24 @@ impl From<Vec<FieldNavItem>> for FieldNavCtx {
#[component]
pub fn FieldNavPortlet() -> impl IntoView {
let ctx = expect_context::<ReadSignal<Option<FieldNavCtx>>>();
move || {
let ctx = ctx.get();
ctx.map(|ctx| {
view! {
<div id="FieldNavPortlet">
<span>"FieldNavPortlet:"</span>
<nav>
{ctx
.0
.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! { <A href=href>{text}</A> }
})
.collect_view()
})}
</nav>
</div>
}
})
view! {
<ShowLet some=ctx let:ctx>
<div id="FieldNavPortlet">
<span>"FieldNavPortlet:"</span>
<nav>
{ctx
.0
.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! { <A href=href>{text}</A> }
})
.collect_view()
})}
</nav>
</div>
</ShowLet>
}
}

View File

@@ -160,3 +160,16 @@ where
OptionGetter(Arc::new(move || cloned.get()))
}
}
/// Marker type for creating an `OptionGetter` from a static value.
/// Used so that the compiler doesn't complain about double implementations of the trait `IntoOptionGetter`.
pub struct StaticMarker;
impl<T> IntoOptionGetter<T, StaticMarker> for Option<T>
where
T: Clone + Send + Sync + 'static,
{
fn into_option_getter(self) -> OptionGetter<T> {
OptionGetter(Arc::new(move || self.clone()))
}
}

View File

@@ -29,6 +29,7 @@ where
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
path: Arc<dyn Fn() -> StorePath + Send + Sync>,
path_unkeyed: Arc<dyn Fn() -> StorePath + Send + Sync>,
get_trigger: Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
get_trigger_unkeyed:
Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
@@ -113,6 +114,10 @@ impl<T> StoreField for ArcField<T> {
(self.path)()
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
(self.path_unkeyed)()
}
fn reader(&self) -> Option<Self::Reader> {
(self.read)().map(StoreFieldReader::new)
}
@@ -137,6 +142,9 @@ where
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
path: Arc::new(move || value.path().into_iter().collect()),
path_unkeyed: Arc::new(move || {
value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new(move |path| value.get_trigger(path)),
get_trigger_unkeyed: Arc::new(move |path| {
value.get_trigger_unkeyed(path)
@@ -163,6 +171,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -211,6 +223,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -258,6 +274,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -306,6 +326,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -358,6 +382,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -396,6 +424,7 @@ impl<T> Clone for ArcField<T> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
path: self.path.clone(),
path_unkeyed: self.path_unkeyed.clone(),
get_trigger: Arc::clone(&self.get_trigger),
get_trigger_unkeyed: Arc::clone(&self.get_trigger_unkeyed),
read: Arc::clone(&self.read),

View File

@@ -76,6 +76,11 @@ where
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner.path()
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner.path_unkeyed()
}
fn reader(&self) -> Option<Self::Reader> {
let inner = self.inner.reader()?;
Some(Mapped::new_with_guard(inner, |n| n.deref()))

View File

@@ -73,6 +73,13 @@ where
.unwrap_or_default()
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.try_get_value()
.map(|inner| inner.path_unkeyed().into_iter().collect::<Vec<_>>())
.unwrap_or_default()
}
fn reader(&self) -> Option<Self::Reader> {
self.inner.try_get_value().and_then(|inner| inner.reader())
}

View File

@@ -80,6 +80,13 @@ where
.chain(iter::once(self.index.into()))
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.path_unkeyed()
.into_iter()
.chain(iter::once(self.index.into()))
}
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}

View File

@@ -106,6 +106,13 @@ where
.chain(iter::once(self.path_segment))
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.path_unkeyed()
.into_iter()
.chain(iter::once(self.path_segment))
}
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}
@@ -444,6 +451,24 @@ where
inner.into_iter().chain(this)
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
let inner =
self.inner.path_unkeyed().into_iter().collect::<StorePath>();
let keys = self
.inner
.keys()
.expect("using keys on a store with no keys");
let this = keys
.with_field_keys(
inner.clone(),
|keys| (keys.get(&self.key), vec![]),
|| self.inner.latest_keys(),
)
.flatten()
.map(|(_, idx)| StorePathSegment(idx));
inner.into_iter().chain(this)
}
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}
@@ -713,3 +738,145 @@ where
.map(|key| AtKeyed::new(self.inner.clone(), key))
}
}
#[cfg(test)]
mod tests {
use crate::{self as reactive_stores, tests::tick, AtKeyed, Store};
use reactive_graph::{
effect::Effect,
traits::{GetUntracked, ReadUntracked, Set, Track, Write},
};
use reactive_stores::Patch;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
#[derive(Debug, Store, Default, Patch)]
struct Todos {
#[store(key: usize = |todo| todo.id)]
todos: Vec<Todo>,
}
#[derive(Debug, Store, Default, Clone, PartialEq, Eq, Patch)]
struct Todo {
id: usize,
label: String,
}
impl Todo {
pub fn new(id: usize, label: impl ToString) -> Self {
Self {
id,
label: label.to_string(),
}
}
}
fn data() -> Todos {
Todos {
todos: vec![
Todo {
id: 10,
label: "A".to_string(),
},
Todo {
id: 11,
label: "B".to_string(),
},
Todo {
id: 12,
label: "C".to_string(),
},
],
}
}
#[tokio::test]
async fn keyed_fields_can_be_moved() {
_ = any_spawner::Executor::init_tokio();
let store = Store::new(data());
assert_eq!(store.read_untracked().todos.len(), 3);
// create an effect to read from each keyed field
let a_count = Arc::new(AtomicUsize::new(0));
let b_count = Arc::new(AtomicUsize::new(0));
let c_count = Arc::new(AtomicUsize::new(0));
let a = AtKeyed::new(store.todos(), 10);
let b = AtKeyed::new(store.todos(), 11);
let c = AtKeyed::new(store.todos(), 12);
Effect::new_sync({
let a_count = Arc::clone(&a_count);
move || {
a.track();
a_count.fetch_add(1, Ordering::Relaxed);
}
});
Effect::new_sync({
let b_count = Arc::clone(&b_count);
move || {
b.track();
b_count.fetch_add(1, Ordering::Relaxed);
}
});
Effect::new_sync({
let c_count = Arc::clone(&c_count);
move || {
c.track();
c_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
assert_eq!(a_count.load(Ordering::Relaxed), 1);
assert_eq!(b_count.load(Ordering::Relaxed), 1);
assert_eq!(c_count.load(Ordering::Relaxed), 1);
// writing at a key doesn't notify siblings
*a.label().write() = "Foo".into();
tick().await;
assert_eq!(a_count.load(Ordering::Relaxed), 2);
assert_eq!(b_count.load(Ordering::Relaxed), 1);
assert_eq!(c_count.load(Ordering::Relaxed), 1);
// the keys can be reorganized
store.todos().write().swap(0, 2);
let after = store.todos().get_untracked();
assert_eq!(
after,
vec![Todo::new(12, "C"), Todo::new(11, "B"), Todo::new(10, "Foo")]
);
tick().await;
assert_eq!(a_count.load(Ordering::Relaxed), 2);
assert_eq!(b_count.load(Ordering::Relaxed), 1);
assert_eq!(c_count.load(Ordering::Relaxed), 1);
// and after we move the keys around, they still update the moved items
a.label().set("Bar".into());
let after = store.todos().get_untracked();
assert_eq!(
after,
vec![Todo::new(12, "C"), Todo::new(11, "B"), Todo::new(10, "Bar")]
);
tick().await;
assert_eq!(a_count.load(Ordering::Relaxed), 3);
assert_eq!(b_count.load(Ordering::Relaxed), 1);
assert_eq!(c_count.load(Ordering::Relaxed), 1);
// we can remove a key and add a new one
store.todos().write().pop();
store.todos().write().push(Todo::new(13, "New"));
let after = store.todos().get_untracked();
assert_eq!(
after,
vec![Todo::new(12, "C"), Todo::new(11, "B"), Todo::new(13, "New")]
);
tick().await;
assert_eq!(a_count.load(Ordering::Relaxed), 3);
assert_eq!(b_count.load(Ordering::Relaxed), 1);
assert_eq!(c_count.load(Ordering::Relaxed), 1);
}
}

View File

@@ -30,7 +30,7 @@ where
type Value = T::Value;
fn patch(&self, new: Self::Value) {
let path = self.path().into_iter().collect::<StorePath>();
let path = self.path_unkeyed().into_iter().collect::<StorePath>();
if let Some(mut writer) = self.writer() {
// don't track the writer for the whole store
writer.untrack();

View File

@@ -38,6 +38,13 @@ pub trait StoreField: Sized {
#[track_caller]
fn path(&self) -> impl IntoIterator<Item = StorePathSegment>;
/// The path of this field (see [`StorePath`]). Uses unkeyed indices for any keyed fields.
#[track_caller]
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
// TODO remove default impl next time we do a breaking release
self.path()
}
/// Reactively tracks this field.
#[track_caller]
fn track_field(&self) {
@@ -129,7 +136,9 @@ where
trigger
}
#[track_caller]
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
let caller = std::panic::Location::caller();
let orig_path = path.clone();
let mut path = StorePath::with_capacity(orig_path.len());
@@ -140,7 +149,13 @@ where
let key = self
.keys
.get_key_for_index(&(path.clone(), segment.0))
.expect("could not find key for index");
.unwrap_or_else(|| {
panic!(
"could not find key for index {:?} at {}",
&(path.clone(), segment.0),
caller
)
});
path.push(key);
} else {
path.push(*segment);
@@ -154,6 +169,11 @@ where
iter::empty()
}
#[track_caller]
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
iter::empty()
}
#[track_caller]
fn reader(&self) -> Option<Self::Reader> {
Plain::try_new(Arc::clone(&self.value))
@@ -205,6 +225,14 @@ where
.unwrap_or_default()
}
#[track_caller]
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.try_get_value()
.map(|n| n.path_unkeyed().into_iter().collect::<Vec<_>>())
.unwrap_or_default()
}
#[track_caller]
fn reader(&self) -> Option<Self::Reader> {
self.inner.try_get_value().and_then(|n| n.reader())

View File

@@ -84,6 +84,13 @@ where
.chain(iter::once(self.path_segment))
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.path_unkeyed()
.into_iter()
.chain(iter::once(self.path_segment))
}
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}