Compare commits

..

10 Commits

Author SHA1 Message Date
Tyler Earls
2e09f3d102 fix: make class attribute overwrite behavior consistent between SSR and CSR (closes #4248) (#4439)
Fixes #4248

During SSR, multiple `class` attributes were incorrectly concatenating
instead of overwriting like they do in browsers. This inconsistency
caused code that appeared to work in SSR to fail in CSR/hydration.

The fix distinguishes between two types of class attributes:
- `class="..."` attributes should overwrite (clear previous values)
- `class:name=value` directives should merge (append to existing classes)

Implementation:
- Added `should_overwrite()` method to `IntoClass` trait (defaults to `false`)
- Modified `Class::to_html()` to clear buffer before rendering if `should_overwrite()` returns `true`
- Implemented `should_overwrite() -> true` for string types (`&str`, `String`, `Cow<'_, str>`, `Arc<str>`)
- Tuple type `(&'static str, bool)` keeps default `false` for merge behavior

Added comprehensive tests to verify:
- `class="foo" class:bar=true` produces `"foo bar"` (merge)
- `class:foo=true` works standalone
- Correct behavior with macro attribute sorting
- Global class application

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-22 13:12:10 -05:00
tqq1994516
e6fe7fef07 fix: add response headers for leptos_axum static files #4377 (#4394) 2025-11-22 13:11:53 -05:00
Greg Johnston
629f4f9d0f fix: do not unescape query and hash in URLs when clicking links (closes (#4454) 2025-11-21 13:16:24 -05:00
Ægir Örn Símonarson
ff5b612e12 chore: removed duplicate workspace member oco (#4445) 2025-11-19 19:59:14 -05:00
Greg Johnston
61571ed24b fix: improve marker-node filtering when using islands router (closes #4443) (#4446) 2025-11-19 19:56:44 -05:00
Greg Johnston
4f3a26ce88 fix: track resources in Suspense that are read conditionally behind other resource reads (see #4430) (#4444) 2025-11-17 21:36:56 -05:00
Greg Johnston
83a848b5ec chore: clean up up warning behavior for resources that depend on other resources (#4415) (closes #3372) 2025-11-17 21:00:41 -05:00
Greg Johnston
eec9edf517 Update README.md 2025-11-11 15:51:21 -05:00
Marco Kuoni
861dcf354c docs: add --split to command for lazy_routes example (#4440) 2025-11-09 20:23:40 -05:00
Greg Johnston
af3d6cba22 fix: remove possibility of SendWrapper errors on server by using conditional compilation instead of overloading .dry_resolve() (closes #4432, #4402) (#4433) 2025-11-07 13:43:18 -05:00
17 changed files with 304 additions and 137 deletions

View File

@@ -2,7 +2,6 @@
resolver = "2"
members = [
# utilities
"oco",
"any_spawner",
"const_str_slice_concat",
"either_of",

View File

@@ -9,6 +9,3 @@ routing when you use islands.
This uses *only* server rendering, with no actual islands, but still maintains client-side state across page navigations.
It does this by building on the fact that we now have a statically-typed view tree to do pretty smart updates with
new HTML from the client, with extremely minimal diffing.
The demo itself works, but the feature that supports it is incomplete. A couple people have accidentally
used it and broken their applications in ways they don't understand, so I've renamed the feature to `dont-use-islands-router`.

View File

@@ -5,4 +5,4 @@ test cases that typically happens at integration.
## Quick Start
Run `cargo leptos watch` to run this example.
Run `cargo leptos watch --split` to run this example.

View File

@@ -1,7 +1,10 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use axum::{
http::{HeaderName, HeaderValue},
Router,
};
use leptos::{logging::log, prelude::*};
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::app::*;
@@ -17,7 +20,24 @@ async fn main() {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.fallback(leptos_axum::file_and_error_handler_with_context(
move || {
// if you want to add custom headers to the static file handler response,
// you can do that by providing `ResponseOptions` via context
let opts = use_context::<leptos_axum::ResponseOptions>()
.unwrap_or_default();
opts.insert_header(
HeaderName::from_static("cross-origin-opener-policy"),
HeaderValue::from_static("same-origin"),
);
opts.insert_header(
HeaderName::from_static("cross-origin-embedder-policy"),
HeaderValue::from_static("require-corp"),
);
provide_context(opts);
},
shell,
))
.with_state(leptos_options);
// run our app with hyper

View File

@@ -2050,7 +2050,20 @@ where
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
let owner = Owner::new();
owner.with(|| {
additional_context();
let res = res.into_response();
if let Some(response_options) =
use_context::<ResponseOptions>()
{
let mut res = AxumResponse(res);
res.extend_response(&response_options);
res.0
} else {
res
}
})
} else {
let mut res = handle_response_inner(
move || {

View File

@@ -6,6 +6,7 @@ use crate::{
use futures::{channel::oneshot, select, FutureExt};
use hydration_context::SerializedDataId;
use leptos_macro::component;
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{
suspense::{LocalResourceNotifier, SuspenseContext},
@@ -14,10 +15,10 @@ use reactive_graph::{
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Dispose, Get, Read, Track, With, WriteValue},
traits::{Dispose, Get, Read, ReadUntracked, Track, With, WriteValue},
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use tachys::{
either::Either,
html::attribute::{any_attribute::AnyAttribute, Attribute},
@@ -320,23 +321,66 @@ where
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
let children = Arc::new(Mutex::new(Some(self.children)));
// check the set of tasks to see if it is empty, now or later
let eff = reactive_graph::effect::Effect::new_isomorphic({
move |_| {
tasks.track();
if let Some(tasks) = tasks.try_read() {
if tasks.is_empty() {
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
if let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
let children = Arc::clone(&children);
move |double_checking: Option<bool>| {
// on the first run, always track the tasks
if double_checking.is_none() {
tasks.track();
}
if let Some(curr_tasks) = tasks.try_read_untracked() {
if curr_tasks.is_empty() {
if double_checking == Some(true) {
// we have finished loading, and checking the children again told us there are
// no more pending tasks. so we can render both the children and the error boundary
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
if let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
} else {
// release the read guard on tasks, as we'll be updating it again
drop(curr_tasks);
// check the children for additional pending tasks
// the will catch additional resource reads nested inside a conditional depending on initial resource reads
if let Some(children) =
children.lock().or_poisoned().as_mut()
{
children.dry_resolve();
}
if tasks
.try_read()
.map(|n| n.is_empty())
.unwrap_or(false)
{
// there are no additional pending tasks, and we can simply return
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
if let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
}
// tell ourselves that we're just double-checking
return true;
}
} else {
tasks.track();
}
}
false
}
});
@@ -362,12 +406,17 @@ where
None
}
_ = tasks_rx => {
let children = {
let mut children_lock = children.lock().or_poisoned();
children_lock.take().expect("children should not be removed until we render here")
};
// if we ran this earlier, reactive reads would always be registered as None
// this is fine in the case where we want to use Suspend and .await on some future
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
// want to be able to 1) synchronously read a resource's value, but still 2) wait
// for it to load before we render everything
let mut children = Box::pin(self.children.resolve().fuse());
let mut children = Box::pin(children.resolve().fuse());
// we continue racing the children against the "do we have any local
// resources?" Future

View File

@@ -103,6 +103,76 @@ fn test_classes() {
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_class_with_class_directive_merge() {
use leptos::prelude::*;
// class= followed by class: should merge
let rendered: View<HtmlElement<_, _, _>> = view! {
<div class="foo" class:bar=true></div>
};
assert_eq!(rendered.to_html(), "<div class=\"foo bar\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_solo_class_directive() {
use leptos::prelude::*;
// Solo class: directive should work without class attribute
let rendered: View<HtmlElement<_, _, _>> = view! {
<div class:foo=true></div>
};
assert_eq!(rendered.to_html(), "<div class=\"foo\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_class_directive_with_static_class() {
use leptos::prelude::*;
// class:foo comes after class= due to macro sorting
// The class= clears buffer, then class:foo appends
let rendered: View<HtmlElement<_, _, _>> = view! {
<div class:foo=true class="bar"></div>
};
// After macro sorting: class="bar" class:foo=true
// Expected: "bar foo"
assert_eq!(rendered.to_html(), "<div class=\"bar foo\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_global_class_applied() {
use leptos::prelude::*;
// Test that a global class is properly applied
let rendered: View<HtmlElement<_, _, _>> = view! { class="global",
<div></div>
};
assert_eq!(rendered.to_html(), "<div class=\"global\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_multiple_class_attributes_overwrite() {
use leptos::prelude::*;
// When multiple class attributes are applied, the last one should win (browser behavior)
// This simulates what happens when attributes are combined programmatically
let el = leptos::html::div().class("first").class("second");
let html = el.to_html();
// The second class attribute should overwrite the first
assert_eq!(html, "<div class=\"second\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_with_styles() {

View File

@@ -188,6 +188,39 @@ where
}
}
#[cfg(debug_assertions)]
thread_local! {
static RESOURCE_SOURCE_SIGNAL_ACTIVE: AtomicBool = const { AtomicBool::new(false) };
}
#[cfg(debug_assertions)]
/// Returns whether the current thread is currently running a resource source signal.
pub fn in_resource_source_signal() -> bool {
RESOURCE_SOURCE_SIGNAL_ACTIVE
.with(|scope| scope.load(std::sync::atomic::Ordering::Relaxed))
}
/// Set a static to true whilst running the given function.
/// [`is_in_effect_scope`] will return true whilst the function is running.
fn run_in_resource_source_signal<T>(fun: impl FnOnce() -> T) -> T {
#[cfg(debug_assertions)]
{
// For the theoretical nested case, set back to initial value rather than false:
let initial = RESOURCE_SOURCE_SIGNAL_ACTIVE.with(|scope| {
scope.swap(true, std::sync::atomic::Ordering::Relaxed)
});
let result = fun();
RESOURCE_SOURCE_SIGNAL_ACTIVE.with(|scope| {
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
});
result
}
#[cfg(not(debug_assertions))]
{
fun()
}
}
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
where
T: 'static,
@@ -202,7 +235,9 @@ where
computed::suspense::SuspenseContext, effect::in_effect_scope,
owner::use_context,
};
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
if !in_effect_scope()
&& !in_resource_source_signal()
&& use_context::<SuspenseContext>().is_none()
{
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(
@@ -271,7 +306,7 @@ where
let refetch = ArcRwSignal::new(0);
let source = ArcMemo::new({
let refetch = refetch.clone();
move |_| (refetch.get(), source())
move |_| (refetch.get(), run_in_resource_source_signal(&source))
});
let fun = {
let source = source.clone();
@@ -909,7 +944,9 @@ where
computed::suspense::SuspenseContext, effect::in_effect_scope,
owner::use_context,
};
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
if !in_effect_scope()
&& !in_resource_source_signal()
&& use_context::<SuspenseContext>().is_none()
{
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(

View File

@@ -110,10 +110,12 @@ fn effect_base() -> (Receiver, Owner, Arc<RwLock<EffectInner>>) {
(rx, owner, inner)
}
#[cfg(debug_assertions)]
thread_local! {
static EFFECT_SCOPE_ACTIVE: AtomicBool = const { AtomicBool::new(false) };
}
#[cfg(debug_assertions)]
/// Returns whether the current thread is currently running an effect.
pub fn in_effect_scope() -> bool {
EFFECT_SCOPE_ACTIVE
@@ -123,14 +125,22 @@ pub fn in_effect_scope() -> bool {
/// Set a static to true whilst running the given function.
/// [`is_in_effect_scope`] will return true whilst the function is running.
fn run_in_effect_scope<T>(fun: impl FnOnce() -> T) -> T {
// For the theoretical nested case, set back to initial value rather than false:
let initial = EFFECT_SCOPE_ACTIVE
.with(|scope| scope.swap(true, std::sync::atomic::Ordering::Relaxed));
let result = fun();
EFFECT_SCOPE_ACTIVE.with(|scope| {
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
});
result
#[cfg(debug_assertions)]
{
// For the theoretical nested case, set back to initial value rather than false:
let initial = EFFECT_SCOPE_ACTIVE.with(|scope| {
scope.swap(true, std::sync::atomic::Ordering::Relaxed)
});
let result = fun();
EFFECT_SCOPE_ACTIVE.with(|scope| {
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
});
result
}
#[cfg(not(debug_assertions))]
{
fun()
}
}
impl<S> Effect<S>

View File

@@ -57,6 +57,10 @@ where
_style: &mut String,
_inner_html: &mut String,
) {
// If this is a class="..." attribute (not class:name=value), clear previous value
if self.class.should_overwrite() {
class.clear();
}
class.push(' ');
self.class.to_html(class);
}
@@ -156,6 +160,12 @@ pub trait IntoClass: Send {
/// Renders the class to HTML.
fn to_html(self, class: &mut String);
/// Whether this class attribute should overwrite previous class values.
/// Returns `true` for `class="..."` attributes, `false` for `class:name=value` directives.
fn should_overwrite(&self) -> bool {
false
}
/// Renders the class to HTML for a `<template>`.
#[allow(unused)] // it's used with `nightly` feature
fn to_template(class: &mut String) {}
@@ -289,6 +299,10 @@ impl IntoClass for &str {
class.push_str(self);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
@@ -346,6 +360,10 @@ impl IntoClass for Cow<'_, str> {
IntoClass::to_html(&*self, class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
@@ -403,6 +421,10 @@ impl IntoClass for String {
IntoClass::to_html(self.as_str(), class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
@@ -460,6 +482,10 @@ impl IntoClass for Arc<str> {
IntoClass::to_html(self.as_ref(), class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,

View File

@@ -47,11 +47,13 @@ pub fn directive<T, P, D>(handler: D, param: P) -> Directive<T, D, P>
where
D: IntoDirective<T, P>,
{
Directive(Some(SendWrapper::new(DirectiveInner {
handler,
param,
t: PhantomData,
})))
Directive((!cfg!(feature = "ssr")).then(|| {
SendWrapper::new(DirectiveInner {
handler,
param,
t: PhantomData,
})
}))
}
/// Custom logic that runs in the browser when the element is created or hydrated.
@@ -151,13 +153,7 @@ where
Directive(inner)
}
fn dry_resolve(&mut self) {
// dry_resolve() only runs during SSR, and we should use it to
// synchronously remove and drop the SendWrapper value
// we don't need this value during SSR and leaving it here could drop it
// from a different thread
self.0.take();
}
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self

View File

@@ -113,7 +113,7 @@ where
event,
#[cfg(feature = "reactive_graph")]
owner: reactive_graph::owner::Owner::current().unwrap_or_default(),
cb: Some(SendWrapper::new(cb)),
cb: (!cfg!(feature = "ssr")).then(|| SendWrapper::new(cb)),
}
}
@@ -352,13 +352,7 @@ where
}
}
fn dry_resolve(&mut self) {
// dry_resolve() only runs during SSR, and we should use it to
// synchronously remove and drop the SendWrapper value
// we don't need this value during SSR and leaving it here could drop it
// from a different thread
self.cb.take();
}
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self

View File

@@ -22,7 +22,7 @@ where
{
Property {
key,
value: Some(SendWrapper::new(value)),
value: (!cfg!(feature = "ssr")).then(|| SendWrapper::new(value)),
}
}
@@ -115,13 +115,7 @@ where
}
}
fn dry_resolve(&mut self) {
// dry_resolve() only runs during SSR, and we should use it to
// synchronously remove and drop the SendWrapper value
// we don't need this value during SSR and leaving it here could drop it
// from a different thread
self.value.take();
}
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self

View File

@@ -7,6 +7,9 @@ use std::cell::Cell;
use std::{cell::RefCell, panic::Location, rc::Rc};
use web_sys::{Comment, Element, Node, Text};
#[cfg(feature = "mark_branches")]
const COMMENT_NODE: u16 = 8;
/// Hydration works by walking over the DOM, adding interactivity as needed.
///
/// This cursor tracks the location in the DOM that is currently being hydrated. Each that type
@@ -43,13 +46,27 @@ where
///
/// Does nothing if there is no child.
pub fn child(&self) {
//crate::log("advancing to next child of ");
//Rndr::log_node(&self.current());
let mut inner = self.0.borrow_mut();
if let Some(node) = Rndr::first_child(&inner) {
*inner = node;
}
//drop(inner);
#[cfg(feature = "mark_branches")]
{
while inner.node_type() == COMMENT_NODE {
if let Some(content) = inner.text_content() {
if content.starts_with("bo") || content.starts_with("bc") {
if let Some(sibling) = Rndr::next_sibling(&inner) {
*inner = sibling;
continue;
}
}
}
break;
}
}
// //drop(inner);
//crate::log(">> which is ");
//Rndr::log_node(&self.current());
}
@@ -58,12 +75,25 @@ where
///
/// Does nothing if there is no sibling.
pub fn sibling(&self) {
//crate::log("advancing to next sibling of ");
//Rndr::log_node(&self.current());
let mut inner = self.0.borrow_mut();
if let Some(node) = Rndr::next_sibling(&inner) {
*inner = node;
}
#[cfg(feature = "mark_branches")]
{
while inner.node_type() == COMMENT_NODE {
if let Some(content) = inner.text_content() {
if content.starts_with("bo") || content.starts_with("bc") {
if let Some(sibling) = Rndr::next_sibling(&inner) {
*inner = sibling;
continue;
}
}
}
break;
}
}
//drop(inner);
//crate::log(">> which is ");
//Rndr::log_node(&self.current());

View File

@@ -575,15 +575,7 @@ impl RenderHtml for AnyView {
#[cfg(feature = "hydrate")]
{
if FROM_SERVER {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state =
(self.hydrate_from_server)(self.value, cursor, position);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
(self.hydrate_from_server)(self.value, cursor, position)
} else {
panic!(
"hydrating AnyView from inside a ViewTemplate is not \
@@ -609,14 +601,8 @@ impl RenderHtml for AnyView {
) -> Self::State {
#[cfg(feature = "hydrate")]
{
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state =
(self.hydrate_async)(self.value, cursor, position).await;
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
}
#[cfg(not(feature = "hydrate"))]

View File

@@ -411,21 +411,14 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
match self {
Either::Left(left) => {
Either::Left(left.hydrate::<FROM_SERVER>(cursor, position))
}
Either::Right(right) => {
Either::Right(right.hydrate::<FROM_SERVER>(cursor, position))
}
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
}
async fn hydrate_async(
@@ -433,21 +426,14 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
match self {
Either::Left(left) => {
Either::Left(left.hydrate_async(cursor, position).await)
}
Either::Right(right) => {
Either::Right(right.hydrate_async(cursor, position).await)
}
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
}
fn into_owned(self) -> Self::Owned {
@@ -973,17 +959,11 @@ macro_rules! tuples {
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
$([<EitherOf $num>]::$ty(this) => {
[<EitherOf $num>]::$ty(this.hydrate::<FROM_SERVER>(cursor, position))
})*
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
Self::State { state }
}
@@ -993,17 +973,11 @@ macro_rules! tuples {
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
$([<EitherOf $num>]::$ty(this) => {
[<EitherOf $num>]::$ty(this.hydrate_async(cursor, position).await)
})*
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
Self::State { state }
}

View File

@@ -322,10 +322,6 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
// get parent and position
let current = cursor.current();
let parent = if position.get() == Position::FirstChild {
@@ -346,22 +342,12 @@ where
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let item = view.hydrate::<FROM_SERVER>(cursor, position);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
rendered_items.push(Some((set_index, item)));
}
let marker = cursor.next_placeholder(position);
position.set(Position::NextChild);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
KeyedState {
parent: Some(parent),
marker,
@@ -375,10 +361,6 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
// get parent and position
let current = cursor.current();
let parent = if position.get() == Position::FirstChild {
@@ -399,22 +381,12 @@ where
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let item = view.hydrate_async(cursor, position).await;
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
rendered_items.push(Some((set_index, item)));
}
let marker = cursor.next_placeholder(position);
position.set(Position::NextChild);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
KeyedState {
parent: Some(parent),
marker,