fix: clear old attributes when replacing a Vec<AnyAttribute> (closes #4268) (#4270)

This commit is contained in:
Greg Johnston
2025-09-01 07:41:54 -04:00
committed by GitHub
parent f3a053f99b
commit f5ad4f4b88
14 changed files with 197 additions and 25 deletions

View File

@@ -1,6 +1,10 @@
use super::{Attribute, NextAttribute};
use crate::erased::{Erased, ErasedLocal};
use std::{any::TypeId, fmt::Debug};
use crate::{
erased::{Erased, ErasedLocal},
html::attribute::NamedAttributeKey,
renderer::{dom::Element, Rndr},
};
use std::{any::TypeId, fmt::Debug, mem};
#[cfg(feature = "ssr")]
use std::{future::Future, pin::Pin};
@@ -25,6 +29,7 @@ pub struct AnyAttribute {
resolve: fn(Erased) -> Pin<Box<dyn Future<Output = AnyAttribute> + Send>>,
#[cfg(feature = "ssr")]
dry_resolve: fn(&mut Erased),
keys: fn(&Erased) -> Vec<NamedAttributeKey>,
}
impl Clone for AnyAttribute {
@@ -44,6 +49,7 @@ pub struct AnyAttributeState {
type_id: TypeId,
state: ErasedLocal,
el: crate::renderer::types::Element,
keys: Vec<NamedAttributeKey>,
}
/// Converts an [`Attribute`] into [`AnyAttribute`].
@@ -84,6 +90,7 @@ where
) -> AnyAttributeState {
AnyAttributeState {
type_id: TypeId::of::<T>(),
keys: value.get_ref::<T>().keys(),
state: ErasedLocal::new(value.into_inner::<T>().build(&el)),
el,
}
@@ -96,6 +103,7 @@ where
) -> AnyAttributeState {
AnyAttributeState {
type_id: TypeId::of::<T>(),
keys: value.get_ref::<T>().keys(),
state: ErasedLocal::new(
value.into_inner::<T>().hydrate::<true>(&el),
),
@@ -110,6 +118,7 @@ where
) -> AnyAttributeState {
AnyAttributeState {
type_id: TypeId::of::<T>(),
keys: value.get_ref::<T>().keys(),
state: ErasedLocal::new(
value.into_inner::<T>().hydrate::<true>(&el),
),
@@ -140,6 +149,12 @@ where
async move {value.into_inner::<T>().resolve().await.into_any_attr()}.boxed()
}
fn keys<T: Attribute + 'static>(
value: &Erased,
) -> Vec<NamedAttributeKey> {
value.get_ref::<T>().keys()
}
let value = self.into_cloneable_owned();
AnyAttribute {
type_id: TypeId::of::<T::CloneableOwned>(),
@@ -158,6 +173,7 @@ where
resolve: resolve::<T::CloneableOwned>,
#[cfg(feature = "ssr")]
dry_resolve: dry_resolve::<T::CloneableOwned>,
keys: keys::<T::CloneableOwned>,
}
}
}
@@ -268,6 +284,10 @@ impl Attribute for AnyAttribute {
enabled."
);
}
fn keys(&self) -> Vec<NamedAttributeKey> {
(self.keys)(&self.value)
}
}
impl NextAttribute for Vec<AnyAttribute> {
@@ -286,7 +306,7 @@ impl Attribute for Vec<AnyAttribute> {
const MIN_LENGTH: usize = 0;
type AsyncOutput = Vec<AnyAttribute>;
type State = Vec<AnyAttributeState>;
type State = (Element, Vec<AnyAttributeState>);
type Cloneable = Vec<AnyAttribute>;
type CloneableOwned = Vec<AnyAttribute>;
@@ -321,13 +341,19 @@ impl Attribute for Vec<AnyAttribute> {
) -> Self::State {
#[cfg(feature = "hydrate")]
if FROM_SERVER {
self.into_iter()
.map(|attr| attr.hydrate::<true>(el))
.collect()
(
el.clone(),
self.into_iter()
.map(|attr| attr.hydrate::<true>(el))
.collect(),
)
} else {
self.into_iter()
.map(|attr| attr.hydrate::<false>(el))
.collect()
(
el.clone(),
self.into_iter()
.map(|attr| attr.hydrate::<false>(el))
.collect(),
)
}
#[cfg(not(feature = "hydrate"))]
{
@@ -340,13 +366,34 @@ impl Attribute for Vec<AnyAttribute> {
}
fn build(self, el: &crate::renderer::types::Element) -> Self::State {
self.into_iter().map(|attr| attr.build(el)).collect()
(
el.clone(),
self.into_iter().map(|attr| attr.build(el)).collect(),
)
}
fn rebuild(self, state: &mut Self::State) {
for (attr, state) in self.into_iter().zip(state.iter_mut()) {
attr.rebuild(state)
let (el, state) = state;
for old in mem::take(state) {
for key in old.keys {
match key {
NamedAttributeKey::InnerHtml => {
Rndr::set_inner_html(&old.el, "");
}
NamedAttributeKey::Property(prop_name) => {
Rndr::set_property(
&old.el,
&prop_name,
&wasm_bindgen::JsValue::UNDEFINED,
);
}
NamedAttributeKey::Attribute(key) => {
Rndr::remove_attribute(&old.el, &key);
}
}
}
}
*state = self.into_iter().map(|s| s.build(el)).collect();
}
fn into_cloneable(self) -> Self::Cloneable {
@@ -385,4 +432,8 @@ impl Attribute for Vec<AnyAttribute> {
enabled."
);
}
fn keys(&self) -> Vec<NamedAttributeKey> {
self.iter().flat_map(|s| s.keys()).collect()
}
}

View File

@@ -4,7 +4,7 @@ use super::{
use crate::{
html::attribute::{
maybe_next_attr_erasure_macros::next_attr_combine, Attribute,
AttributeValue,
AttributeValue, NamedAttributeKey,
},
view::{add_attr::AddAnyAttr, Position, ToTemplate},
};
@@ -112,6 +112,12 @@ where
value: self.value.resolve().await,
}
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![NamedAttributeKey::Attribute(
self.key.as_ref().to_string().into(),
)]
}
}
impl<K, V> NextAttribute for CustomAttr<K, V>

View File

@@ -15,7 +15,7 @@ pub use key::*;
use maybe_next_attr_erasure_macros::{
next_attr_combine, next_attr_output_type,
};
use std::{fmt::Debug, future::Future};
use std::{borrow::Cow, fmt::Debug, future::Future};
pub use value::*;
/// Defines an attribute: anything that can modify an element.
@@ -75,6 +75,25 @@ pub trait Attribute: NextAttribute + Send {
/// “Resolves” this into a type that is not waiting for any asynchronous data.
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send;
/// Returns a set of attribute keys, associated with this attribute, if any.
///
/// This is only used to manage the removal of type-erased attributes, when needed.
fn keys(&self) -> Vec<NamedAttributeKey> {
// TODO: remove default implementation in 0.9, or fix this whole approach
// by making it easier to remove attributes
vec![]
}
}
/// An attribute key can be used to remove an attribute from an element.
pub enum NamedAttributeKey {
/// An ordinary attribute.
Attribute(Cow<'static, str>),
/// A DOM property.
Property(Cow<'static, str>),
/// The `inner_html` pseudo-attribute.
InnerHtml,
}
/// Adds another attribute to this one, returning a new attribute.
@@ -133,6 +152,10 @@ impl Attribute for () {
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![]
}
}
impl NextAttribute for () {
@@ -249,6 +272,10 @@ where
async fn resolve(self) -> Self::AsyncOutput {
Attr(self.0, self.1.resolve().await)
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![NamedAttributeKey::Attribute(K::KEY.into())]
}
}
impl<K, V> NextAttribute for Attr<K, V>
@@ -353,6 +380,14 @@ macro_rules! impl_attr_for_tuples {
$($ty.resolve()),*
)
}
fn keys(&self) -> Vec<NamedAttributeKey> {
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
let mut buf = $first.keys();
$(buf.extend($ty.keys());)*
buf
}
}
impl<$first, $($ty),*> NextAttribute for ($first, $($ty,)*)
@@ -462,6 +497,14 @@ macro_rules! impl_attr_for_tuples_truncate_additional {
$($ty.resolve()),*
)
}
fn keys(&self) -> Vec<NamedAttributeKey> {
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
let mut buf = $first.keys();
$(buf.extend($ty.keys());)*
buf
}
}
impl<$first, $($ty),*> NextAttribute for ($first, $($ty,)*)
@@ -538,6 +581,10 @@ where
async fn resolve(self) -> Self::AsyncOutput {
(self.0.resolve().await,)
}
fn keys(&self) -> Vec<NamedAttributeKey> {
self.0.keys()
}
}
impl<A> NextAttribute for (A,)

View File

@@ -1,6 +1,6 @@
use super::attribute::{
maybe_next_attr_erasure_macros::next_attr_output_type, Attribute,
NextAttribute,
NamedAttributeKey, NextAttribute,
};
use crate::{
html::attribute::maybe_next_attr_erasure_macros::next_attr_combine,
@@ -97,6 +97,10 @@ where
class: self.class.resolve().await,
}
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![NamedAttributeKey::Attribute("class".into())]
}
}
impl<C> NextAttribute for Class<C>

View File

@@ -3,7 +3,9 @@ use super::attribute::{
NextAttribute,
};
use crate::{
html::attribute::maybe_next_attr_erasure_macros::next_attr_combine,
html::attribute::{
maybe_next_attr_erasure_macros::next_attr_combine, NamedAttributeKey,
},
prelude::AddAnyAttr,
view::{Position, ToTemplate},
};
@@ -160,6 +162,10 @@ where
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![]
}
}
impl<T, D, P> NextAttribute for Directive<T, D, P>

View File

@@ -4,7 +4,7 @@ use crate::{
maybe_next_attr_erasure_macros::{
next_attr_combine, next_attr_output_type,
},
Attribute, NextAttribute,
Attribute, NamedAttributeKey, NextAttribute,
},
renderer::Rndr,
view::add_attr::AddAnyAttr,
@@ -105,6 +105,10 @@ where
value: self.value.resolve().await,
}
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![NamedAttributeKey::InnerHtml]
}
}
impl<T> NextAttribute for InnerHtml<T>

View File

@@ -1,6 +1,7 @@
use crate::{
html::attribute::{
maybe_next_attr_erasure_macros::next_attr_combine, Attribute,
NamedAttributeKey,
},
renderer::{CastFrom, RemoveEventHandler, Rndr},
view::{Position, ToTemplate},
@@ -360,6 +361,10 @@ where
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![]
}
}
impl<E, F> NextAttribute for On<E, F>

View File

@@ -7,7 +7,10 @@ use super::{
};
use crate::{
html::{
attribute::maybe_next_attr_erasure_macros::next_attr_combine,
attribute::{
maybe_next_attr_erasure_macros::next_attr_combine,
NamedAttributeKey,
},
element::HtmlElement,
},
prelude::Render,
@@ -112,6 +115,10 @@ where
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![]
}
}
impl<E, C> NextAttribute for NodeRefAttr<E, C>

View File

@@ -3,7 +3,9 @@ use super::attribute::{
NextAttribute,
};
use crate::{
html::attribute::maybe_next_attr_erasure_macros::next_attr_combine,
html::attribute::{
maybe_next_attr_erasure_macros::next_attr_combine, NamedAttributeKey,
},
renderer::Rndr,
view::{Position, ToTemplate},
};
@@ -124,6 +126,12 @@ where
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![NamedAttributeKey::Property(
self.key.as_ref().to_string().into(),
)]
}
}
impl<K, P> NextAttribute for Property<K, P>

View File

@@ -5,7 +5,9 @@ use super::attribute::{
#[cfg(all(feature = "nightly", rustc_nightly))]
use crate::view::static_types::Static;
use crate::{
html::attribute::maybe_next_attr_erasure_macros::next_attr_combine,
html::attribute::{
maybe_next_attr_erasure_macros::next_attr_combine, NamedAttributeKey,
},
renderer::{dom::CssStyleDeclaration, Rndr},
view::{Position, ToTemplate},
};
@@ -100,6 +102,10 @@ where
style: self.style.resolve().await,
}
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![NamedAttributeKey::Attribute("style".into())]
}
}
impl<S> NextAttribute for Style<S>

View File

@@ -5,7 +5,8 @@ use crate::{
maybe_next_attr_erasure_macros::{
next_attr_combine, next_attr_output_type,
},
Attribute, AttributeKey, AttributeValue, NextAttribute,
Attribute, AttributeKey, AttributeValue, NamedAttributeKey,
NextAttribute,
},
event::{change, input, on},
property::{prop, IntoProperty},
@@ -275,6 +276,10 @@ where
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![]
}
}
impl<Key, T, R, W> NextAttribute for Bind<Key, T, R, W>

View File

@@ -699,7 +699,15 @@ impl Render for AnyViewWithAttrs {
fn rebuild(self, state: &mut Self::State) {
self.view.rebuild(&mut state.view);
self.attrs.rebuild(&mut state.attrs);
let elements = state.elements();
// FIXME this seems wrong but I think the previous version was also broken!
if let Some(element) = elements.first() {
self.attrs.rebuild(&mut (
element.clone(),
std::mem::take(&mut state.attrs),
));
}
}
}
@@ -820,7 +828,7 @@ impl AddAnyAttr for AnyViewWithAttrs {
}
}
/// wip
/// State for any view with attributes spread onto it.
pub struct AnyViewWithAttrsState {
view: AnyViewState,
attrs: Vec<AnyAttributeState>,

View File

@@ -3,7 +3,10 @@ use super::{
Render, RenderHtml,
};
use crate::{
html::attribute::{any_attribute::AnyAttribute, Attribute, NextAttribute},
html::attribute::{
any_attribute::AnyAttribute, Attribute, NamedAttributeKey,
NextAttribute,
},
hydration::Cursor,
ssr::StreamBuilder,
};
@@ -264,6 +267,13 @@ where
Either::Right(right) => Either::Right(right.resolve().await),
}
}
fn keys(&self) -> Vec<NamedAttributeKey> {
match self {
Either::Left(left) => left.keys(),
Either::Right(right) => right.keys(),
}
}
}
impl<A, B> RenderHtml for Either<A, B>

View File

@@ -8,7 +8,8 @@ use crate::{
maybe_next_attr_erasure_macros::{
next_attr_combine, next_attr_output_type,
},
Attribute, AttributeKey, AttributeValue, NextAttribute,
Attribute, AttributeKey, AttributeValue, NamedAttributeKey,
NextAttribute,
},
hydration::Cursor,
renderer::{CastFrom, Rndr},
@@ -111,6 +112,10 @@ where
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn keys(&self) -> Vec<NamedAttributeKey> {
vec![NamedAttributeKey::Attribute(K::KEY.into())]
}
}
impl<K, const V: &'static str> NextAttribute for StaticAttr<K, V>