Repository: yewstack/yew (Rust)

Issue:
Define prop execution order
The execution order of prop value expressions is in alphabetic order based on the prop key. Let's look at an example:

```rust
/// Gen is used here to simulate a stateful expression
#[derive(Default)]
struct Gen {
    state: usize,
}
impl Gen {
    fn next(&mut self) -> usize {
        self.state += 1;
        self.state
    }
}

fn main() {
    // the order in which `g.next()` is called matters!
    let mut g = Gen::default();
    let props = yew::props!(Props {
        my_first: g.next(),
        second: g.next(),
        last: g.next(),
    });

    // different order!
    assert_eq!(props.last, 1);
    assert_eq!(props.my_first, 2);
    assert_eq!(props.second, 3);
}
```

Normally Clippy warns about execution order in struct expressions, but we don't have the tools to do this, especially not in the `html!` macro. The easiest way to avoid confusion here is to define the execution order as the one defined by the user.

To achieve this for components we just need to pre-evaluate the prop values before using them in the builder.
Elements are a bit more painful because of the different special props but the same idea can still be used.

This isn't a high priority issue right now but it could save a few headaches down the line.


Discussion:

Patch:
diff --git a/packages/yew-macro/src/props/prop.rs b/packages/yew-macro/src/props/prop.rs
index 82ae6375ff1..5ec04323ec0 100644
--- a/packages/yew-macro/src/props/prop.rs
+++ b/packages/yew-macro/src/props/prop.rs
@@ -1,4 +1,3 @@
-use std::cmp::Ordering;
 use std::convert::TryFrom;
 use std::ops::{Deref, DerefMut};
 
@@ -9,7 +8,6 @@ use syn::spanned::Spanned;
 use syn::token::Brace;
 use syn::{braced, Block, Expr, ExprBlock, ExprPath, ExprRange, Stmt, Token};
 
-use super::CHILDREN_LABEL;
 use crate::html_tree::HtmlDashedName;
 use crate::stringify::Stringify;
 
@@ -24,6 +22,7 @@ pub struct Prop {
     /// Punctuation between `label` and `value`.
     pub value: Expr,
 }
+
 impl Parse for Prop {
     fn parse(input: ParseStream) -> syn::Result<Self> {
         let directive = input
@@ -216,36 +215,21 @@ fn advance_until_next_dot2(input: &ParseBuffer) -> syn::Result<()> {
 ///
 /// The list may contain multiple props with the same label.
 /// Use `check_no_duplicates` to ensure that there are no duplicates.
-pub struct SortedPropList(Vec<Prop>);
-impl SortedPropList {
+pub struct PropList(Vec<Prop>);
+impl PropList {
     /// Create a new `SortedPropList` from a vector of props.
     /// The given `props` doesn't need to be sorted.
-    pub fn new(mut props: Vec<Prop>) -> Self {
-        props.sort_by(|a, b| Self::cmp_label(&a.label.to_string(), &b.label.to_string()));
+    pub fn new(props: Vec<Prop>) -> Self {
         Self(props)
     }
 
-    fn cmp_label(a: &str, b: &str) -> Ordering {
-        if a == b {
-            Ordering::Equal
-        } else if a == CHILDREN_LABEL {
-            Ordering::Greater
-        } else if b == CHILDREN_LABEL {
-            Ordering::Less
-        } else {
-            a.cmp(b)
-        }
-    }
-
     fn position(&self, key: &str) -> Option<usize> {
-        self.0
-            .binary_search_by(|prop| Self::cmp_label(prop.label.to_string().as_str(), key))
-            .ok()
+        self.0.iter().position(|it| it.label.to_string() == key)
     }
 
     /// Get the first prop with the given key.
     pub fn get_by_label(&self, key: &str) -> Option<&Prop> {
-        self.position(key).and_then(|i| self.0.get(i))
+        self.0.iter().find(|it| it.label.to_string() == key)
     }
 
     /// Pop the first prop with the given key.
@@ -312,7 +296,7 @@ impl SortedPropList {
         }))
     }
 }
-impl Parse for SortedPropList {
+impl Parse for PropList {
     fn parse(input: ParseStream) -> syn::Result<Self> {
         let mut props: Vec<Prop> = Vec::new();
         // Stop parsing props if a base expression preceded by `..` is reached
@@ -323,7 +307,7 @@ impl Parse for SortedPropList {
         Ok(Self::new(props))
     }
 }
-impl Deref for SortedPropList {
+impl Deref for PropList {
     type Target = [Prop];
 
     fn deref(&self) -> &Self::Target {
@@ -340,7 +324,7 @@ impl SpecialProps {
     const KEY_LABEL: &'static str = "key";
     const REF_LABEL: &'static str = "ref";
 
-    fn pop_from(props: &mut SortedPropList) -> syn::Result<Self> {
+    fn pop_from(props: &mut PropList) -> syn::Result<Self> {
         let node_ref = props.pop_unique(Self::REF_LABEL)?;
         let key = props.pop_unique(Self::KEY_LABEL)?;
         Ok(Self { node_ref, key })
@@ -386,15 +370,15 @@ impl SpecialProps {
 
 pub struct Props {
     pub special: SpecialProps,
-    pub prop_list: SortedPropList,
+    pub prop_list: PropList,
 }
 impl Parse for Props {
     fn parse(input: ParseStream) -> syn::Result<Self> {
-        Self::try_from(input.parse::<SortedPropList>()?)
+        Self::try_from(input.parse::<PropList>()?)
     }
 }
 impl Deref for Props {
-    type Target = SortedPropList;
+    type Target = PropList;
 
     fn deref(&self) -> &Self::Target {
         &self.prop_list
@@ -406,10 +390,10 @@ impl DerefMut for Props {
     }
 }
 
-impl TryFrom<SortedPropList> for Props {
+impl TryFrom<PropList> for Props {
     type Error = syn::Error;
 
-    fn try_from(mut prop_list: SortedPropList) -> Result<Self, Self::Error> {
+    fn try_from(mut prop_list: PropList) -> Result<Self, Self::Error> {
         let special = SpecialProps::pop_from(&mut prop_list)?;
         Ok(Self { special, prop_list })
     }
diff --git a/packages/yew-macro/src/props/prop_macro.rs b/packages/yew-macro/src/props/prop_macro.rs
index 1ff4312276b..95f57727cc0 100644
--- a/packages/yew-macro/src/props/prop_macro.rs
+++ b/packages/yew-macro/src/props/prop_macro.rs
@@ -8,7 +8,7 @@ use syn::spanned::Spanned;
 use syn::token::Brace;
 use syn::{Expr, Token, TypePath};
 
-use super::{ComponentProps, Prop, Props, SortedPropList};
+use super::{ComponentProps, Prop, PropList, Props};
 use crate::html_tree::HtmlDashedName;
 
 /// Pop from `Punctuated` without leaving it in a state where it has trailing punctuation.
@@ -106,7 +106,7 @@ pub struct PropsMacroInput {
 impl Parse for PropsMacroInput {
     fn parse(input: ParseStream) -> syn::Result<Self> {
         let PropsExpr { ty, fields, .. } = input.parse()?;
-        let prop_list = SortedPropList::new(fields.into_iter().map(Into::into).collect());
+        let prop_list = PropList::new(fields.into_iter().map(Into::into).collect());
         let props: Props = prop_list.try_into()?;
         props.special.check_all(|prop| {
             let label = &prop.label;
diff --git a/website/docs/concepts/function-components/properties.mdx b/website/docs/concepts/function-components/properties.mdx
index e50b3539573..8761b3fe6a6 100644
--- a/website/docs/concepts/function-components/properties.mdx
+++ b/website/docs/concepts/function-components/properties.mdx
@@ -263,6 +263,24 @@ fn App() -> Html {
 }
 ```
 
+## Evaluation Order
+
+Props are evaluated in the order they're specified, as shown by the following example:
+
+```rust
+#[derive(yew::Properties, PartialEq)]
+struct Props { first: usize, second: usize, last: usize }
+
+fn main() {
+    let mut g = 1..=3;
+    let props = yew::props!(Props { first: g.next().unwrap(), second: g.next().unwrap(), last: g.next().unwrap() });
+
+    assert_eq!(props.first, 1);
+    assert_eq!(props.second, 2);
+    assert_eq!(props.last, 3);
+}
+```
+
 ## Anti Patterns
 
 While almost any Rust type can be passed as properties, there are some anti-patterns that should be avoided.


Test patch:
diff --git a/packages/yew-macro/tests/html_macro/component-fail.stderr b/packages/yew-macro/tests/html_macro/component-fail.stderr
index 33f60e1a6a3..512a4fe965d 100644
--- a/packages/yew-macro/tests/html_macro/component-fail.stderr
+++ b/packages/yew-macro/tests/html_macro/component-fail.stderr
@@ -143,10 +143,10 @@ error: `with` doesn't have a value. (hint: set the value to `true` or `false` fo
    |                                               ^^^^
 
 error: `ref` can only be specified once
-  --> tests/html_macro/component-fail.rs:67:20
+  --> tests/html_macro/component-fail.rs:67:29
    |
 67 |     html! { <Child ref={()} ref={()} value=1 ..props  /> };
-   |                    ^^^
+   |                             ^^^
 
 error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes)
   --> tests/html_macro/component-fail.rs:68:20
diff --git a/packages/yew-macro/tests/html_macro/element-fail.stderr b/packages/yew-macro/tests/html_macro/element-fail.stderr
index 08072df2e7f..bce83a6b975 100644
--- a/packages/yew-macro/tests/html_macro/element-fail.stderr
+++ b/packages/yew-macro/tests/html_macro/element-fail.stderr
@@ -101,16 +101,16 @@ error: `class` can only be specified once but is given here again
    |                                ^^^^^
 
 error: `ref` can only be specified once
-  --> tests/html_macro/element-fail.rs:33:20
+  --> tests/html_macro/element-fail.rs:33:29
    |
 33 |     html! { <input ref={()} ref={()} /> };
-   |                    ^^^
+   |                             ^^^
 
 error: `ref` can only be specified once
-  --> tests/html_macro/element-fail.rs:63:20
+  --> tests/html_macro/element-fail.rs:63:29
    |
 63 |     html! { <input ref={()} ref={()} /> };
-   |                    ^^^
+   |                             ^^^
 
 error: the tag `<input>` is a void element and cannot have children (hint: rewrite this as `<input/>`)
   --> tests/html_macro/element-fail.rs:66:13
diff --git a/packages/yew-macro/tests/props_macro_test.rs b/packages/yew-macro/tests/props_macro_test.rs
index 83234005a47..47b96990d19 100644
--- a/packages/yew-macro/tests/props_macro_test.rs
+++ b/packages/yew-macro/tests/props_macro_test.rs
@@ -5,3 +5,24 @@ fn props_macro() {
     t.pass("tests/props_macro/*-pass.rs");
     t.compile_fail("tests/props_macro/*-fail.rs");
 }
+
+#[test]
+fn props_order() {
+    #[derive(yew::Properties, PartialEq)]
+    struct Props {
+        first: usize,
+        second: usize,
+        last: usize,
+    }
+
+    let mut g = 1..=3;
+    let props = yew::props!(Props {
+        first: g.next().unwrap(),
+        second: g.next().unwrap(),
+        last: g.next().unwrap()
+    });
+
+    assert_eq!(props.first, 1);
+    assert_eq!(props.second, 2);
+    assert_eq!(props.last, 3);
+}