mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Fix hot reloading components with keys (#2886)
* Fix hot reloading components with keys * include component formatted segment keys, but not dynamic component value * Fix component_literal_dyn_idx index * add a new test for hot reloading components with keys * Even more tests * make clippy happy * fix typo
This commit is contained in:
parent
0e937daaa1
commit
4676171861
7 changed files with 176 additions and 56 deletions
|
@ -69,6 +69,8 @@ impl<'a> DynIdVisitor<'a> {
|
||||||
if let HotLiteral::Fmted(segments) = literal {
|
if let HotLiteral::Fmted(segments) = literal {
|
||||||
self.assign_formatted_segment(segments);
|
self.assign_formatted_segment(segments);
|
||||||
}
|
}
|
||||||
|
// Don't include keys in the component dynamic pool
|
||||||
|
if !property.name.is_likely_key() {
|
||||||
component.component_literal_dyn_idx[index]
|
component.component_literal_dyn_idx[index]
|
||||||
.set(self.component_literal_index);
|
.set(self.component_literal_index);
|
||||||
self.component_literal_index += 1;
|
self.component_literal_index += 1;
|
||||||
|
@ -76,6 +78,7 @@ impl<'a> DynIdVisitor<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -406,6 +406,10 @@ impl AttributeName {
|
||||||
matches!(self, Self::BuiltIn(ident) if ident.to_string().starts_with("on"))
|
matches!(self, Self::BuiltIn(ident) if ident.to_string().starts_with("on"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_likely_key(&self) -> bool {
|
||||||
|
matches!(self, Self::BuiltIn(ident) if ident == "key")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn span(&self) -> proc_macro2::Span {
|
pub fn span(&self) -> proc_macro2::Span {
|
||||||
match self {
|
match self {
|
||||||
Self::Custom(lit) => lit.span(),
|
Self::Custom(lit) => lit.span(),
|
||||||
|
|
|
@ -171,10 +171,10 @@ impl Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_key(&self) -> Option<&AttributeValue> {
|
pub fn get_key(&self) -> Option<&AttributeValue> {
|
||||||
self.fields.iter().find_map(|attr| match &attr.name {
|
self.fields
|
||||||
AttributeName::BuiltIn(key) if key == "key" => Some(&attr.value),
|
.iter()
|
||||||
_ => None,
|
.find(|attr| attr.name.is_likely_key())
|
||||||
})
|
.map(|attr| &attr.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure there's no duplicate props - this will be a compile error but we can move it to a
|
/// Ensure there's no duplicate props - this will be a compile error but we can move it to a
|
||||||
|
@ -252,28 +252,21 @@ impl Component {
|
||||||
self.spreads.first().map(|spread| &spread.expr)
|
self.spreads.first().map(|spread| &spread.expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_field_idents(&self) -> Vec<(TokenStream2, TokenStream2)> {
|
// Iterate over the props of the component (without spreads, key, and custom attributes)
|
||||||
let mut dynamic_literal_index = 0;
|
pub(crate) fn component_props(&self) -> impl Iterator<Item = &Attribute> {
|
||||||
self.fields
|
self.fields
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(move |attr| {
|
.filter(move |attr| !attr.name.is_likely_key())
|
||||||
let Attribute { name, value, .. } = attr;
|
|
||||||
|
|
||||||
let attr = match name {
|
|
||||||
AttributeName::BuiltIn(k) => {
|
|
||||||
if k == "key" {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
quote! { #k }
|
|
||||||
}
|
|
||||||
AttributeName::Custom(_) => return None,
|
|
||||||
AttributeName::Spread(_) => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let release_value = value.to_token_stream();
|
fn make_field_idents(&self) -> Vec<(TokenStream2, TokenStream2)> {
|
||||||
|
let mut dynamic_literal_index = 0;
|
||||||
|
self.component_props()
|
||||||
|
.map(|attribute| {
|
||||||
|
let release_value = attribute.value.to_token_stream();
|
||||||
|
|
||||||
// In debug mode, we try to grab the value from the dynamic literal pool if possible
|
// In debug mode, we try to grab the value from the dynamic literal pool if possible
|
||||||
let value = if let AttributeValue::AttrLiteral(literal) = &value {
|
let value = if let AttributeValue::AttrLiteral(literal) = &attribute.value {
|
||||||
let idx = self.component_literal_dyn_idx[dynamic_literal_index].get();
|
let idx = self.component_literal_dyn_idx[dynamic_literal_index].get();
|
||||||
dynamic_literal_index += 1;
|
dynamic_literal_index += 1;
|
||||||
let debug_value = quote! { __dynamic_literal_pool.component_property(#idx, &*__template_read, #literal) };
|
let debug_value = quote! { __dynamic_literal_pool.component_property(#idx, &*__template_read, #literal) };
|
||||||
|
@ -293,7 +286,7 @@ impl Component {
|
||||||
release_value
|
release_value
|
||||||
};
|
};
|
||||||
|
|
||||||
Some((attr, value))
|
(attribute.name.to_token_stream(), value)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -345,6 +338,7 @@ fn normalize_path(name: &mut syn::Path) -> Option<AngleBracketedGenericArguments
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use prettier_please::PrettyUnparse;
|
use prettier_please::PrettyUnparse;
|
||||||
|
use syn::parse_quote;
|
||||||
|
|
||||||
/// Ensure we can parse a component
|
/// Ensure we can parse a component
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -482,4 +476,23 @@ mod tests {
|
||||||
|
|
||||||
let _parsed: syn::Path = syn::parse2(input).unwrap();
|
let _parsed: syn::Path = syn::parse2(input).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identifies_key() {
|
||||||
|
let input = quote! {
|
||||||
|
Link { key: "{value}", to: Route::List, class: "pure-button", "Go back" }
|
||||||
|
};
|
||||||
|
|
||||||
|
let component: Component = syn::parse2(input).unwrap();
|
||||||
|
|
||||||
|
// The key should exist
|
||||||
|
assert_eq!(component.get_key(), Some(&parse_quote!("{value}")));
|
||||||
|
|
||||||
|
// The key should not be included in the properties
|
||||||
|
let properties = component
|
||||||
|
.component_props()
|
||||||
|
.map(|attr| attr.name.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(properties, ["to", "class"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,7 +237,7 @@ impl Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
if attr.name.to_string() == "key" {
|
if attr.name.is_likely_key() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,15 +301,10 @@ impl Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn key(&self) -> Option<&AttributeValue> {
|
pub(crate) fn key(&self) -> Option<&AttributeValue> {
|
||||||
for attr in &self.raw_attributes {
|
self.raw_attributes
|
||||||
if let AttributeName::BuiltIn(name) = &attr.name {
|
.iter()
|
||||||
if name == "key" {
|
.find(|attr| attr.name.is_likely_key())
|
||||||
return Some(&attr.value);
|
.map(|attr| &attr.value)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn completion_hints(&self) -> TokenStream2 {
|
fn completion_hints(&self) -> TokenStream2 {
|
||||||
|
|
|
@ -406,13 +406,15 @@ impl HotReloadResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check if the fields are the same
|
// Then check if the fields are the same
|
||||||
if new_component.fields.len() != old_component.fields.len() {
|
let new_non_key_fields: Vec<_> = new_component.component_props().collect();
|
||||||
|
let old_non_key_fields: Vec<_> = old_component.component_props().collect();
|
||||||
|
if new_non_key_fields.len() != old_non_key_fields.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut new_fields = new_component.fields.clone();
|
let mut new_fields = new_non_key_fields.clone();
|
||||||
new_fields.sort_by_key(|attribute| attribute.name.to_string());
|
new_fields.sort_by_key(|attribute| attribute.name.to_string());
|
||||||
let mut old_fields = old_component.fields.iter().enumerate().collect::<Vec<_>>();
|
let mut old_fields = old_non_key_fields.iter().enumerate().collect::<Vec<_>>();
|
||||||
old_fields.sort_by_key(|(_, attribute)| attribute.name.to_string());
|
old_fields.sort_by_key(|(_, attribute)| attribute.name.to_string());
|
||||||
|
|
||||||
// The literal component properties for the component in same the order as the original component property literals
|
// The literal component properties for the component in same the order as the original component property literals
|
||||||
|
|
|
@ -320,7 +320,7 @@ impl TemplateBody {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flat_map(|component| {
|
.flat_map(|component| {
|
||||||
component.fields.iter().filter_map(|field| {
|
component.component_props().filter_map(|field| {
|
||||||
if let AttributeValue::AttrLiteral(literal) = &field.value {
|
if let AttributeValue::AttrLiteral(literal) = &field.value {
|
||||||
Some(literal)
|
Some(literal)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1085,6 +1085,109 @@ fn component_with_handlers() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_remove_key() {
|
||||||
|
let a = quote! {
|
||||||
|
Component {
|
||||||
|
key: "{key}",
|
||||||
|
class: 123,
|
||||||
|
id: 456.789,
|
||||||
|
other: true,
|
||||||
|
dynamic1,
|
||||||
|
dynamic2,
|
||||||
|
blah: "hello {world}",
|
||||||
|
onclick: |e| { println!("clicked") },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// changing lit values
|
||||||
|
let b = quote! {
|
||||||
|
Component {
|
||||||
|
class: 456,
|
||||||
|
id: 789.456,
|
||||||
|
other: false,
|
||||||
|
dynamic1,
|
||||||
|
dynamic2,
|
||||||
|
blah: "goodbye {world}",
|
||||||
|
onclick: |e| { println!("clicked") },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let hot_reload = hot_reload_from_tokens(a, b).unwrap();
|
||||||
|
let template = hot_reload.get(&0).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template.component_values,
|
||||||
|
&[
|
||||||
|
HotReloadLiteral::Int(456),
|
||||||
|
HotReloadLiteral::Float(789.456),
|
||||||
|
HotReloadLiteral::Bool(false),
|
||||||
|
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
|
||||||
|
FmtSegment::Literal { value: "goodbye " },
|
||||||
|
FmtSegment::Dynamic { id: 1 }
|
||||||
|
]))
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_modify_key() {
|
||||||
|
let a = quote! {
|
||||||
|
Component {
|
||||||
|
key: "{key}",
|
||||||
|
class: 123,
|
||||||
|
id: 456.789,
|
||||||
|
other: true,
|
||||||
|
dynamic1,
|
||||||
|
dynamic2,
|
||||||
|
blah1: "hello {world123}",
|
||||||
|
blah2: "hello {world}",
|
||||||
|
onclick: |e| { println!("clicked") },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// changing lit values
|
||||||
|
let b = quote! {
|
||||||
|
Component {
|
||||||
|
key: "{key}-{world}",
|
||||||
|
class: 456,
|
||||||
|
id: 789.456,
|
||||||
|
other: false,
|
||||||
|
dynamic1,
|
||||||
|
dynamic2,
|
||||||
|
blah1: "hello {world123}",
|
||||||
|
blah2: "hello {world}",
|
||||||
|
onclick: |e| { println!("clicked") },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let hot_reload = hot_reload_from_tokens(a, b).unwrap();
|
||||||
|
let template = hot_reload.get(&0).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template.key,
|
||||||
|
Some(FmtedSegments::new(vec![
|
||||||
|
FmtSegment::Dynamic { id: 0 },
|
||||||
|
FmtSegment::Literal { value: "-" },
|
||||||
|
FmtSegment::Dynamic { id: 2 },
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
template.component_values,
|
||||||
|
&[
|
||||||
|
HotReloadLiteral::Int(456),
|
||||||
|
HotReloadLiteral::Float(789.456),
|
||||||
|
HotReloadLiteral::Bool(false),
|
||||||
|
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
|
||||||
|
FmtSegment::Literal { value: "hello " },
|
||||||
|
FmtSegment::Dynamic { id: 1 }
|
||||||
|
])),
|
||||||
|
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
|
||||||
|
FmtSegment::Literal { value: "hello " },
|
||||||
|
FmtSegment::Dynamic { id: 2 }
|
||||||
|
]))
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicating_dynamic_nodes() {
|
fn duplicating_dynamic_nodes() {
|
||||||
let a = quote! {
|
let a = quote! {
|
||||||
|
|
Loading…
Reference in a new issue