Skip to content

Fix rsx evaluation order #3944

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/dog_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ fn app() -> Element {

rsx! {
for cur_breed in breeds.message.keys().take(20).cloned() {
button { onclick: move |_| breed.set(cur_breed.clone()),
button {
onclick: {
to_owned![cur_breed];
move |_| breed.set(cur_breed.clone())
},
"{cur_breed}"
}
}
Expand Down
5 changes: 4 additions & 1 deletion examples/weather_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ fn SearchBox(mut country: Signal<WeatherLocation>) -> Element {
if let Some(Ok(locs)) = locations.read().as_ref() {
for wl in locs.iter().take(5).cloned() {
li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900",
onclick: move |_| country.set(wl.clone()),
onclick: {
to_owned![wl];
move |_| country.set(wl.clone())
},
MapIcon {}
b { "{wl.name}" }
" · {wl.country}"
Expand Down
76 changes: 76 additions & 0 deletions packages/core-macro/tests/rsx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,79 @@ mod test_optional_signals {
rsx! { "hi!" }
}
}

/// This test ensures that the rsx macro runs in order.
///
/// These are compile-time tests.
/// See https://github.com/DioxusLabs/dioxus/issues/3737
#[cfg(test)]
#[allow(unused)]
mod rsx_ordering {
mod attribute_then_component {
use dioxus::prelude::*;

#[derive(Clone, PartialEq, Eq)]
struct TestOuter;

impl TestOuter {
fn borrow(&self) -> String {
"".to_string()
}
}

#[component]
fn Child(outer: TestOuter) -> Element {
rsx! {
div { "Hello" }
}
}

#[component]
fn Parent() -> Element {
let outer = TestOuter;

rsx! {
div { width: outer.borrow(),
Child { outer }
}
}
}
}
mod component_then_attribute {
use dioxus::prelude::*;

#[derive(Clone, PartialEq, Eq)]
struct TestOuter;

impl From<&str> for TestOuter {
fn from(_: &str) -> Self {
TestOuter
}
}

impl TestOuter {
fn borrow(&self) -> String {
"".to_string()
}
}

#[component]
fn Child(#[props(into)] outer: TestOuter) -> Element {
rsx! {
div { "Hello" }
}
}

#[component]
fn Parent() -> Element {
let outer = "hello".to_string();

rsx! {
div {
Child { outer: &*outer }
}
div { width: outer }
}
}
}
}
21 changes: 18 additions & 3 deletions packages/rsx/src/assign_dyn_ids.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use syn::parse_quote;

use crate::attribute::Attribute;
use crate::{
AttributeValue, BodyNode, HotLiteral, HotReloadFormattedSegment, Segment, TemplateBody,
Expand Down Expand Up @@ -35,10 +37,10 @@ impl<'a> DynIdVisitor<'a> {
BodyNode::Element(el) => {
for (idx, attr) in el.merged_attributes.iter().enumerate() {
if !attr.is_static_str_literal() {
self.assign_path_to_attribute(attr, idx);
if let AttributeValue::AttrLiteral(HotLiteral::Fmted(lit)) = &attr.value {
self.assign_formatted_segment(lit);
}
self.assign_path_to_attribute(attr, idx);
}
}
// Assign formatted segments to the key which is not included in the merged_attributes
Expand All @@ -52,8 +54,8 @@ impl<'a> DynIdVisitor<'a> {
// Text nodes are dynamic if they contain dynamic segments
BodyNode::Text(txt) => {
if !txt.is_static() {
self.assign_path_to_node(node);
self.assign_formatted_segment(&txt.input);
self.assign_path_to_node(node);
}
}

Expand All @@ -62,7 +64,6 @@ impl<'a> DynIdVisitor<'a> {
self.assign_path_to_node(node)
}
BodyNode::Component(component) => {
self.assign_path_to_node(node);
let mut index = 0;
for property in &component.fields {
if let AttributeValue::AttrLiteral(literal) = &property.value {
Expand All @@ -78,6 +79,7 @@ impl<'a> DynIdVisitor<'a> {
}
}
}
self.assign_path_to_node(node);
}
};
}
Expand Down Expand Up @@ -105,6 +107,12 @@ impl<'a> DynIdVisitor<'a> {

// And then save the current path as the corresponding path
self.body.node_paths.push(self.current_path.clone());

// Finally, save the expression in the expression pool and store the binding
// for later use
let expr = parse_quote!({#node});
let node_binding = self.body.expression_pool.add(expr);
self.body.node_bindings.push(node_binding);
}

/// Assign a path to a attribute and give it its dynamic index
Expand All @@ -117,6 +125,13 @@ impl<'a> DynIdVisitor<'a> {
// Assign the dynamic index to the attribute
attribute.set_dyn_idx(self.body.attr_paths.len());

// get the value of the attribute
let value = &attribute.rendered_as_dynamic_attr();
let expr = parse_quote!({#value});
// add the expression to the pool
let binding = self.body.expression_pool.add(expr);
attribute.set_value_expression_binding(binding);

// And then save the current path as the corresponding path
self.body
.attr_paths
Expand Down
50 changes: 34 additions & 16 deletions packages/rsx/src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
//! ```

use super::literal::HotLiteral;
use crate::{innerlude::*, partial_closure::PartialClosure};
use crate::{expression_pool::OutOfOrderExpression, innerlude::*, partial_closure::PartialClosure};

use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned, ToTokens, TokenStreamExt};
Expand Down Expand Up @@ -56,6 +56,10 @@ pub struct Attribute {
/// The element name of this attribute if it is bound to an element.
/// When parsed for components or freestanding, this will be None
pub el_name: Option<ElementName>,

/// The value temporary binding in the expression pool. We quote this value
/// instead the value to keep a depth first expression execution order
attribute_binding: OutOfOrderExpression,
}

impl Parse for Attribute {
Expand All @@ -72,6 +76,7 @@ impl Parse for Attribute {
comma,
dyn_idx: DynIdx::default(),
el_name: None,
attribute_binding: Default::default(),
});
}

Expand All @@ -91,29 +96,32 @@ impl Parse for Attribute {

let comma = content.parse::<Token![,]>().ok();

let attr = Attribute {
name,
value,
colon,
comma,
dyn_idx: DynIdx::default(),
el_name: None,
};

Ok(attr)
Ok(Self::from_parts(name, colon, value, comma, None))
}
}

impl Attribute {
/// Create a new attribute from a name and value
pub fn from_raw(name: AttributeName, value: AttributeValue) -> Self {
Self::from_parts(name, None, value, None, None)
}

/// Create a new attribute from parts
pub(crate) fn from_parts(
name: AttributeName,
colon: Option<Token![:]>,
value: AttributeValue,
comma: Option<Token![,]>,
el_name: Option<ElementName>,
) -> Self {
Self {
name,
colon: Default::default(),
colon,
value,
comma: Default::default(),
comma,
el_name,
dyn_idx: Default::default(),
el_name: None,
attribute_binding: Default::default(),
}
}

Expand All @@ -127,6 +135,17 @@ impl Attribute {
self.dyn_idx.get()
}

/// Insert the expression binding into an expression pool. This must be called before
/// [`Self::rendered_as_dynamic_attr`]
pub(crate) fn set_value_expression_binding(&self, ident: Ident) {
self.attribute_binding.insert(ident);
}

/// Get the expression binding of this attribute
pub fn get_value_expression_binding(&self) -> Option<&Ident> {
self.attribute_binding.get()
}

pub fn span(&self) -> proc_macro2::Span {
self.name.span()
}
Expand Down Expand Up @@ -166,7 +185,7 @@ impl Attribute {
self.as_static_str_literal().is_some()
}

pub fn rendered_as_dynamic_attr(&self) -> TokenStream2 {
pub(crate) fn rendered_as_dynamic_attr(&self) -> TokenStream2 {
// Shortcut out with spreads
if let AttributeName::Spread(_) = self.name {
let AttributeValue::AttrExpr(expr) = &self.value else {
Expand Down Expand Up @@ -222,7 +241,6 @@ impl Attribute {
let ns = ns(name);
let volatile = volatile(name);
let attribute = attribute(name);
let value = quote! { #value };

quote! {
dioxus_core::Attribute::new(
Expand Down
46 changes: 30 additions & 16 deletions packages/rsx/src/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,15 @@ impl Parse for Element {
// merged attributes in path order stops before we hit the spreads, but spreads are still
// counted as dynamic attributes
for spread in block.spreads.iter() {
element.merged_attributes.push(Attribute {
name: AttributeName::Spread(spread.dots),
colon: None,
value: AttributeValue::AttrExpr(PartialExpr::from_expr(&spread.expr)),
comma: spread.comma,
dyn_idx: spread.dyn_idx.clone(),
el_name: Some(name.clone()),
});
let attribute = Attribute::from_parts(
AttributeName::Spread(spread.dots),
None,
AttributeValue::AttrExpr(PartialExpr::from_expr(&spread.expr)),
spread.comma,
Some(name.clone()),
);
attribute.set_dyn_idx(spread.dyn_idx.get());
element.merged_attributes.push(attribute);
}

Ok(element)
Expand Down Expand Up @@ -289,14 +290,15 @@ impl Element {

let out_lit = HotLiteral::Fmted(out.into());

self.merged_attributes.push(Attribute {
name: attr.name.clone(),
value: AttributeValue::AttrLiteral(out_lit),
colon: attr.colon,
dyn_idx: attr.dyn_idx.clone(),
comma: matching_attrs.last().unwrap().comma,
el_name: attr.el_name.clone(),
});
let attribute = Attribute::from_parts(
attr.name.clone(),
attr.colon,
AttributeValue::AttrLiteral(out_lit),
matching_attrs.last().unwrap().comma,
attr.el_name.clone(),
);
attribute.set_dyn_idx(attr.dyn_idx.get());
self.merged_attributes.push(attribute);
}
}

Expand Down Expand Up @@ -401,6 +403,8 @@ impl Display for ElementName {

#[cfg(test)]
mod tests {
use crate::CallBody;

use super::*;
use prettier_please::PrettyUnparse;

Expand Down Expand Up @@ -479,6 +483,16 @@ mod tests {
dbg!(parsed);
}

#[test]
fn parses_spread() {
let input = quote::quote! {
div { ..attributes, "card" }
};

let parsed: CallBody = syn::parse2(input).unwrap();
dbg!(parsed);
}

#[test]
fn to_tokens_properly() {
let input = quote::quote! {
Expand Down
Loading
Loading