From 503b10d674930768d7d01175a8c0f309f44ba8f7 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 9 Mar 2022 21:06:45 -0600 Subject: [PATCH 01/18] bugfixes text modifer elements --- packages/tui/Cargo.toml | 7 +- packages/tui/src/attributes.rs | 68 +++++++++- packages/tui/src/hooks.rs | 223 +++++++++++++++++++++------------ packages/tui/src/layout.rs | 19 +++ packages/tui/src/lib.rs | 2 +- 5 files changed, 226 insertions(+), 93 deletions(-) diff --git a/packages/tui/Cargo.toml b/packages/tui/Cargo.toml index 972cac0cc..516d77522 100644 --- a/packages/tui/Cargo.toml +++ b/packages/tui/Cargo.toml @@ -2,7 +2,7 @@ name = "dioxus-tui" version = "0.2.0" authors = ["Jonathan Kelley, @dementhos"] -edition = "2018" +edition = "2021" description = "TUI-based renderer for Dioxus" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" @@ -19,10 +19,7 @@ dioxus-html = { path = "../html", version = "^0.2.0" } tui = "0.17.0" crossterm = "0.23.0" anyhow = "1.0.42" -thiserror = "1.0.24" -ctrlc = "3.2.1" -bumpalo = { version = "3.8.0", features = ["boxed"] } tokio = { version = "1.15.0", features = ["full"] } futures = "0.3.19" -stretch2 = "0.4.0" +stretch2 = "0.4.1" diff --git a/packages/tui/src/attributes.rs b/packages/tui/src/attributes.rs index a1ffcb5ea..7533652f1 100644 --- a/packages/tui/src/attributes.rs +++ b/packages/tui/src/attributes.rs @@ -388,6 +388,15 @@ pub enum UnitSystem { Point(f32), } +impl Into for UnitSystem { + fn into(self) -> Dimension { + match self { + Self::Percent(v) => Dimension::Percent(v), + Self::Point(v) => Dimension::Points(v), + } + } +} + fn parse_value(value: &str) -> Option { if value.ends_with("px") { if let Ok(px) = value.trim_end_matches("px").parse::() { @@ -505,11 +514,16 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { } } "border-bottom-style" => { + if style.style.border.bottom == Dimension::default() { + let v = Dimension::Points(1.0); + style.style.border.bottom = v; + } style.tui_modifier.borders.bottom.style = parse_border_style(value) } "border-bottom-width" => { if let Some(v) = parse_value(value) { style.tui_modifier.borders.bottom.width = v; + style.style.border.bottom = v.into(); } } "border-collapse" => {} @@ -547,10 +561,17 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { style.tui_modifier.borders.left.color = Some(c); } } - "border-left-style" => style.tui_modifier.borders.left.style = parse_border_style(value), + "border-left-style" => { + if style.style.border.start == Dimension::default() { + let v = Dimension::Points(1.0); + style.style.border.start = v; + } + style.tui_modifier.borders.left.style = parse_border_style(value) + } "border-left-width" => { if let Some(v) = parse_value(value) { style.tui_modifier.borders.left.width = v; + style.style.border.start = v.into(); } } "border-radius" => { @@ -581,7 +602,11 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { style.tui_modifier.borders.right.color = Some(c); } } - "border-right-style" => style.tui_modifier.borders.right.style = parse_border_style(value), + "border-right-style" => { + let v = Dimension::Points(1.0); + style.style.border.end = v; + style.tui_modifier.borders.right.style = parse_border_style(value) + } "border-right-width" => { if let Some(v) = parse_value(value) { style.tui_modifier.borders.right.width = v; @@ -590,6 +615,22 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { "border-spacing" => {} "border-style" => { let values: Vec<_> = value.split(' ').collect(); + if style.style.border.top == Dimension::default() { + let v = Dimension::Points(1.0); + style.style.border.top = v; + } + if style.style.border.bottom == Dimension::default() { + let v = Dimension::Points(1.0); + style.style.border.bottom = v; + } + if style.style.border.start == Dimension::default() { + let v = Dimension::Points(1.0); + style.style.border.start = v; + } + if style.style.border.end == Dimension::default() { + let v = Dimension::Points(1.0); + style.style.border.end = v; + } if values.len() == 1 { let border_style = parse_border_style(values[0]); style @@ -623,9 +664,16 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { style.tui_modifier.borders.right.radius = v; } } - "border-top-style" => style.tui_modifier.borders.top.style = parse_border_style(value), + "border-top-style" => { + if style.style.border.top == Dimension::default() { + let v = Dimension::Points(1.0); + style.style.border.top = v; + } + style.tui_modifier.borders.top.style = parse_border_style(value) + } "border-top-width" => { if let Some(v) = parse_value(value) { + style.style.border.top = v.into(); style.tui_modifier.borders.top.width = v; } } @@ -633,6 +681,10 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { let values: Vec<_> = value.split(' ').collect(); if values.len() == 1 { if let Some(w) = parse_value(values[0]) { + style.style.border.top = w.into(); + style.style.border.bottom = w.into(); + style.style.border.start = w.into(); + style.style.border.end = w.into(); style .tui_modifier .borders @@ -641,11 +693,19 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { .for_each(|b| b.width = w); } } else { - for (v, b) in values + let border_widths = [ + &mut style.style.border.top, + &mut style.style.border.bottom, + &mut style.style.border.start, + &mut style.style.border.end, + ]; + for ((v, b), width) in values .into_iter() .zip(style.tui_modifier.borders.slice().iter_mut()) + .zip(border_widths) { if let Some(w) = parse_value(v) { + *width = w.into(); b.width = w; } } diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 963fc277b..2224edb16 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -460,26 +460,7 @@ impl RinkInputHandler { // translate crossterm events into dioxus events fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { let (name, data): (&str, EventData) = match evt { - TermEvent::Key(k) => { - let key = translate_key_code(k.code)?; - ( - "keydown", - // from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent - EventData::Keyboard(KeyboardData { - char_code: key.raw_code(), - key: format!("{key:?}"), - key_code: key, - alt_key: k.modifiers.contains(KeyModifiers::ALT), - ctrl_key: k.modifiers.contains(KeyModifiers::CONTROL), - meta_key: false, - shift_key: k.modifiers.contains(KeyModifiers::SHIFT), - locale: Default::default(), - location: 0x00, - repeat: Default::default(), - which: Default::default(), - }), - ) - } + TermEvent::Key(k) => ("keydown", translate_key_event(k)?), TermEvent::Mouse(m) => { let (x, y) = (m.column.into(), m.row.into()); let alt = m.modifiers.contains(KeyModifiers::ALT); @@ -542,69 +523,145 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { Some((name, data)) } -fn translate_key_code(c: TermKeyCode) -> Option { - match c { - TermKeyCode::Backspace => Some(KeyCode::Backspace), - TermKeyCode::Enter => Some(KeyCode::Enter), - TermKeyCode::Left => Some(KeyCode::LeftArrow), - TermKeyCode::Right => Some(KeyCode::RightArrow), - TermKeyCode::Up => Some(KeyCode::UpArrow), - TermKeyCode::Down => Some(KeyCode::DownArrow), - TermKeyCode::Home => Some(KeyCode::Home), - TermKeyCode::End => Some(KeyCode::End), - TermKeyCode::PageUp => Some(KeyCode::PageUp), - TermKeyCode::PageDown => Some(KeyCode::PageDown), - TermKeyCode::Tab => Some(KeyCode::Tab), - TermKeyCode::BackTab => None, - TermKeyCode::Delete => Some(KeyCode::Delete), - TermKeyCode::Insert => Some(KeyCode::Insert), - TermKeyCode::F(fn_num) => match fn_num { - 1 => Some(KeyCode::F1), - 2 => Some(KeyCode::F2), - 3 => Some(KeyCode::F3), - 4 => Some(KeyCode::F4), - 5 => Some(KeyCode::F5), - 6 => Some(KeyCode::F6), - 7 => Some(KeyCode::F7), - 8 => Some(KeyCode::F8), - 9 => Some(KeyCode::F9), - 10 => Some(KeyCode::F10), - 11 => Some(KeyCode::F11), - 12 => Some(KeyCode::F12), - _ => None, - }, - TermKeyCode::Char(c) => match c.to_uppercase().next().unwrap() { - 'A' => Some(KeyCode::A), - 'B' => Some(KeyCode::B), - 'C' => Some(KeyCode::C), - 'D' => Some(KeyCode::D), - 'E' => Some(KeyCode::E), - 'F' => Some(KeyCode::F), - 'G' => Some(KeyCode::G), - 'H' => Some(KeyCode::H), - 'I' => Some(KeyCode::I), - 'J' => Some(KeyCode::J), - 'K' => Some(KeyCode::K), - 'L' => Some(KeyCode::L), - 'M' => Some(KeyCode::M), - 'N' => Some(KeyCode::N), - 'O' => Some(KeyCode::O), - 'P' => Some(KeyCode::P), - 'Q' => Some(KeyCode::Q), - 'R' => Some(KeyCode::R), - 'S' => Some(KeyCode::S), - 'T' => Some(KeyCode::T), - 'U' => Some(KeyCode::U), - 'V' => Some(KeyCode::V), - 'W' => Some(KeyCode::W), - 'X' => Some(KeyCode::X), - 'Y' => Some(KeyCode::Y), - 'Z' => Some(KeyCode::Z), - _ => None, - }, - TermKeyCode::Null => None, - TermKeyCode::Esc => Some(KeyCode::Escape), - } +fn translate_key_event(event: crossterm::event::KeyEvent) -> Option { + let (code, key_str); + if let TermKeyCode::Char(c) = event.code { + code = match c { + 'A'..='Z' | 'a'..='z' => match c.to_ascii_uppercase() { + 'A' => KeyCode::A, + 'B' => KeyCode::B, + 'C' => KeyCode::C, + 'D' => KeyCode::D, + 'E' => KeyCode::E, + 'F' => KeyCode::F, + 'G' => KeyCode::G, + 'H' => KeyCode::H, + 'I' => KeyCode::I, + 'J' => KeyCode::J, + 'K' => KeyCode::K, + 'L' => KeyCode::L, + 'M' => KeyCode::M, + 'N' => KeyCode::N, + 'O' => KeyCode::O, + 'P' => KeyCode::P, + 'Q' => KeyCode::Q, + 'R' => KeyCode::R, + 'S' => KeyCode::S, + 'T' => KeyCode::T, + 'U' => KeyCode::U, + 'V' => KeyCode::V, + 'W' => KeyCode::W, + 'X' => KeyCode::X, + 'Y' => KeyCode::Y, + 'Z' => KeyCode::Z, + _ => return None, + }, + ' ' => KeyCode::Space, + '[' => KeyCode::OpenBracket, + '{' => KeyCode::OpenBracket, + ']' => KeyCode::CloseBraket, + '}' => KeyCode::CloseBraket, + ';' => KeyCode::Semicolon, + ':' => KeyCode::Semicolon, + ',' => KeyCode::Comma, + '<' => KeyCode::Comma, + '.' => KeyCode::Period, + '>' => KeyCode::Period, + '1' => KeyCode::Num1, + '2' => KeyCode::Num2, + '3' => KeyCode::Num3, + '4' => KeyCode::Num4, + '5' => KeyCode::Num5, + '6' => KeyCode::Num6, + '7' => KeyCode::Num7, + '8' => KeyCode::Num8, + '9' => KeyCode::Num9, + '0' => KeyCode::Num0, + '!' => KeyCode::Num1, + '@' => KeyCode::Num2, + '#' => KeyCode::Num3, + '$' => KeyCode::Num4, + '%' => KeyCode::Num5, + '^' => KeyCode::Num6, + '&' => KeyCode::Num7, + '*' => KeyCode::Num8, + '(' => KeyCode::Num9, + ')' => KeyCode::Num0, + // numpad charicter are ambiguous to tui + // '*' => KeyCode::Multiply, + // '/' => KeyCode::Divide, + // '-' => KeyCode::Subtract, + // '+' => KeyCode::Add, + '+' => KeyCode::EqualSign, + '-' => KeyCode::Dash, + '_' => KeyCode::Dash, + '\'' => KeyCode::SingleQuote, + '"' => KeyCode::SingleQuote, + '\\' => KeyCode::BackSlash, + '|' => KeyCode::BackSlash, + '/' => KeyCode::ForwardSlash, + '?' => KeyCode::ForwardSlash, + '=' => KeyCode::EqualSign, + '`' => KeyCode::GraveAccent, + '~' => KeyCode::GraveAccent, + _ => return None, + }; + key_str = c.to_string(); + } else { + code = match event.code { + TermKeyCode::Esc => KeyCode::Escape, + TermKeyCode::Backspace => KeyCode::Backspace, + TermKeyCode::Enter => KeyCode::Enter, + TermKeyCode::Left => KeyCode::LeftArrow, + TermKeyCode::Right => KeyCode::RightArrow, + TermKeyCode::Up => KeyCode::UpArrow, + TermKeyCode::Down => KeyCode::DownArrow, + TermKeyCode::Home => KeyCode::Home, + TermKeyCode::End => KeyCode::End, + TermKeyCode::PageUp => KeyCode::PageUp, + TermKeyCode::PageDown => KeyCode::PageDown, + TermKeyCode::Tab => KeyCode::Tab, + TermKeyCode::Delete => KeyCode::Delete, + TermKeyCode::Insert => KeyCode::Insert, + TermKeyCode::F(fn_num) => match fn_num { + 1 => KeyCode::F1, + 2 => KeyCode::F2, + 3 => KeyCode::F3, + 4 => KeyCode::F4, + 5 => KeyCode::F5, + 6 => KeyCode::F6, + 7 => KeyCode::F7, + 8 => KeyCode::F8, + 9 => KeyCode::F9, + 10 => KeyCode::F10, + 11 => KeyCode::F11, + 12 => KeyCode::F12, + _ => return None, + }, + TermKeyCode::BackTab => return None, + TermKeyCode::Null => return None, + _ => return None, + }; + key_str = if let KeyCode::BackSlash = code { + "\\".to_string() + } else { + format!("{code:?}") + } + }; + // from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent + Some(EventData::Keyboard(KeyboardData { + char_code: code.raw_code(), + key: key_str.to_string(), + key_code: code, + alt_key: event.modifiers.contains(KeyModifiers::ALT), + ctrl_key: event.modifiers.contains(KeyModifiers::CONTROL), + meta_key: false, + shift_key: event.modifiers.contains(KeyModifiers::SHIFT), + locale: Default::default(), + location: 0x00, + repeat: Default::default(), + which: Default::default(), + })) } fn clone_mouse_data(m: &MouseData) -> MouseData { diff --git a/packages/tui/src/layout.rs b/packages/tui/src/layout.rs index 6a8744efb..0d615d1a5 100644 --- a/packages/tui/src/layout.rs +++ b/packages/tui/src/layout.rs @@ -55,6 +55,25 @@ pub fn collect_layout<'a>( tui_modifier: TuiModifier::default(), }; + // handle text modifier elements + if el.namespace.is_none() { + match el.tag { + "b" => apply_attributes("font-weight", "bold", &mut modifier), + "strong" => apply_attributes("font-weight", "bold", &mut modifier), + "u" => apply_attributes("text-decoration", "underline", &mut modifier), + "ins" => apply_attributes("text-decoration", "underline", &mut modifier), + "del" => apply_attributes("text-decoration", "line-through", &mut modifier), + "i" => apply_attributes("font-style", "italic", &mut modifier), + "em" => apply_attributes("font-style", "italic", &mut modifier), + "mark" => apply_attributes( + "background-color", + "rgba(241, 231, 64, 50%)", + &mut modifier, + ), + _ => (), + } + } + for &Attribute { name, value, .. } in el.attributes { apply_attributes(name, value, &mut modifier); } diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 91ff7db4d..f90967802 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -172,7 +172,7 @@ pub fn render_vdom( match evt.as_ref().unwrap() { InputEvent::UserInput(event) => match event { TermEvent::Key(key) => { - if matches!(key.code, KeyCode::Char('c')) + if matches!(key.code, KeyCode::Char('C' | 'c')) && key.modifiers.contains(KeyModifiers::CONTROL) { break; From 5818da5f70d1d6df88dc1a3fe37f785c05c56026 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 15 Mar 2022 16:52:45 +0000 Subject: [PATCH 02/18] auto detect optional props --- packages/core-macro/src/props/mod.rs | 85 ++++++++++++++++------------ 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index d569e711a..6c65c5944 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -43,27 +43,21 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { syn::Fields::Unnamed(_) => { return Err(Error::new( ast.span(), - "TypedBuilder is not supported for tuple structs", + "Props is not supported for tuple structs", )) } syn::Fields::Unit => { return Err(Error::new( ast.span(), - "TypedBuilder is not supported for unit structs", + "Props is not supported for unit structs", )) } }, syn::Data::Enum(_) => { - return Err(Error::new( - ast.span(), - "TypedBuilder is not supported for enums", - )) + return Err(Error::new(ast.span(), "Props is not supported for enums")) } syn::Data::Union(_) => { - return Err(Error::new( - ast.span(), - "TypedBuilder is not supported for unions", - )) + return Err(Error::new(ast.span(), "Props is not supported for unions")) } }; Ok(data) @@ -169,6 +163,7 @@ mod util { } mod field_info { + use crate::props::type_from_inside_option; use proc_macro2::TokenStream; use quote::quote; use syn::parse::Error; @@ -202,6 +197,16 @@ mod field_info { Some(syn::parse(quote!(Default::default()).into()).unwrap()); } + // auto detect optional + let strip_option_auto = builder_attr.strip_option + || !builder_attr.ignore_option + && type_from_inside_option(&field.ty, true).is_some(); + if !builder_attr.strip_option && strip_option_auto { + builder_attr.strip_option = true; + builder_attr.default = + Some(syn::parse(quote!(Default::default()).into()).unwrap()); + } + Ok(FieldInfo { ordinal, name, @@ -236,31 +241,8 @@ mod field_info { .into() } - pub fn type_from_inside_option(&self) -> Option<&syn::Type> { - let path = if let syn::Type::Path(type_path) = self.ty { - if type_path.qself.is_some() { - return None; - } else { - &type_path.path - } - } else { - return None; - }; - let segment = path.segments.last()?; - if segment.ident != "Option" { - return None; - } - let generic_params = - if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments { - generic_params - } else { - return None; - }; - if let syn::GenericArgument::Type(ty) = generic_params.args.first()? { - Some(ty) - } else { - None - } + pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> { + type_from_inside_option(self.ty, check_option_name) } } @@ -271,6 +253,7 @@ mod field_info { pub skip: bool, pub auto_into: bool, pub strip_option: bool, + pub ignore_option: bool, } impl FieldBuilderAttr { @@ -427,8 +410,9 @@ mod field_info { self.auto_into = false; Ok(()) } - "strip_option" => { + "optional" => { self.strip_option = false; + self.ignore_option = true; Ok(()) } _ => Err(Error::new_spanned(path, "Unknown setting".to_owned())), @@ -446,6 +430,33 @@ mod field_info { } } +fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&syn::Type> { + let path = if let syn::Type::Path(type_path) = ty { + if type_path.qself.is_some() { + return None; + } else { + &type_path.path + } + } else { + return None; + }; + let segment = path.segments.last()?; + if check_option_name && segment.ident != "Option" { + return None; + } + let generic_params = + if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments { + generic_params + } else { + return None; + }; + if let syn::GenericArgument::Type(ty) = generic_params.args.first()? { + Some(ty) + } else { + None + } +} + mod struct_info { use proc_macro2::TokenStream; use quote::quote; @@ -766,7 +777,7 @@ Finally, call `.build()` to create the instance of `{name}`. // NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of // nesting is different so we have to do this little dance. let arg_type = if field.builder_attr.strip_option { - let internal_type = field.type_from_inside_option().ok_or_else(|| { + let internal_type = field.type_from_inside_option(false).ok_or_else(|| { Error::new_spanned( &field_type, "can't `strip_option` - field is not `Option<...>`", From be55524e695a7f06fcf78a1e876de8f57cb14823 Mon Sep 17 00:00:00 2001 From: Marc-Stefan Cassola Date: Tue, 15 Mar 2022 19:49:16 +0000 Subject: [PATCH 03/18] Change optional props docs --- docs/guide/src/components/propsmacro.md | 46 ++++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/docs/guide/src/components/propsmacro.md b/docs/guide/src/components/propsmacro.md index 83b01a3ea..a86a453e2 100644 --- a/docs/guide/src/components/propsmacro.md +++ b/docs/guide/src/components/propsmacro.md @@ -131,23 +131,29 @@ Borrowed Props cannot be safely memoized. However, this is not a problem – Dio ## Optional Props -You can easily create optional fields by attaching the `optional` modifier to a field: +You can easily create optional fields by using the `Option<…>` type for a field: ```rust #[derive(Props, PartialEq)] struct MyProps { name: String, - #[props(optional)] description: Option } fn Demo(cx: MyProps) -> Element { - todo!() + let text = match cx.props.description { + Some(d) => d, // if a value is provided + None => "No description" // if the prop is omitted + }; + + cx.render(rsx! { + "{name}": "{text}" + }) } ``` - -Then, we can completely omit the description field when calling the component: +In this example ˋnameˋ is a required prop and ˋdescriptionˋ is optional. +This means we can completely omit the description field when calling the component: ```rust rsx!{ @@ -157,15 +163,39 @@ rsx!{ } } ``` +Additionally if we provide a value we don't have to wrap it with ˋSome(…)ˋ. This is done automatically for us: -The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option`. The full list of modifiers includes: +ˋˋˋrust +rsx!{ + Demo { + name: "Thing".to_string(), + description: "This is explains it".to_string(), + } +} +ˋˋˋ + +If you want to make a prop required even though it is of type ˋOptionˋ you can provide the ˋ!optionalˋ modifier: + +ˋˋˋrust +#[derive(Props, PartialEq)] +struct MyProps { + name: String, + + #[props(!optional)] + description: Option +} +ˋˋˋ + +This can be especially useful if you have a type alias named ˋOptionˋ in the current scope. + +For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new. The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option` and it is automatically detected on ˋOption<…>ˋ types. + +The full list of Dioxus' modifiers includes: - `default` - automatically add the field using its `Default` implementation -- `strip_option` - automatically wrap values at the call site in `Some` - `optional` - alias for `default` and `strip_option` - `into` - automatically call `into` on the value at the callsite -For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new. ## The `inline_props` macro From a2825fb13ce62e6750f920b81e9a26721033433f Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 16 Mar 2022 06:36:39 +0000 Subject: [PATCH 04/18] updated examples with the new optional props --- examples/optional_props.rs | 21 +++++++++++---------- packages/router/src/components/redirect.rs | 1 - packages/router/src/components/router.rs | 2 -- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/optional_props.rs b/examples/optional_props.rs index 9485a2aeb..3c47b3aac 100644 --- a/examples/optional_props.rs +++ b/examples/optional_props.rs @@ -14,37 +14,38 @@ fn app(cx: Scope) -> Element { cx.render(rsx! { Button { a: "asd".to_string(), - c: Some("asd".to_string()), - d: "asd".to_string(), + c: "asd".to_string(), + d: Some("asd".to_string()), e: "asd".to_string(), } }) } +type SthElse = Option; + #[derive(Props, PartialEq)] struct ButtonProps { a: String, #[props(default)] - b: Option, + b: String, - #[props(default)] c: Option, - #[props(default, strip_option)] + #[props(!optional)] d: Option, #[props(optional)] - e: Option, + e: SthElse, } fn Button(cx: Scope) -> Element { cx.render(rsx! { button { - "{cx.props.a}" - "{cx.props.b:?}" - "{cx.props.c:?}" - "{cx.props.d:?}" + "{cx.props.a} | " + "{cx.props.b:?} | " + "{cx.props.c:?} | " + "{cx.props.d:?} | " "{cx.props.e:?}" } }) diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs index a1d2d7727..7a7d85be2 100644 --- a/packages/router/src/components/redirect.rs +++ b/packages/router/src/components/redirect.rs @@ -27,7 +27,6 @@ pub struct RedirectProps<'a> { /// // Relative path /// Redirect { from: "", to: "../" } /// ``` - #[props(optional)] pub from: Option<&'a str>, } diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index d7df377f1..85adfa422 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -20,7 +20,6 @@ pub struct RouterProps<'a> { /// /// This will be used to trim any latent segments from the URL when your app is /// not deployed to the root of the domain. - #[props(optional)] pub base_url: Option<&'a str>, /// Hook into the router when the route is changed. @@ -33,7 +32,6 @@ pub struct RouterProps<'a> { /// /// This is useful if you don't want to repeat the same `active_class` prop value in every Link. /// By default set to `"active"`. - #[props(default, strip_option)] pub active_class: Option<&'a str>, } From f26f704b6b7a5fe8ed8fc41b159a9dbd9592579b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Mar 2022 10:51:23 -0400 Subject: [PATCH 05/18] fix: setnode method for rehydration code --- packages/interpreter/src/bindings.rs | 2 +- packages/interpreter/src/interpreter.js | 3 ++ packages/web/.vscode/settings.json | 3 ++ packages/web/Cargo.toml | 5 +-- packages/web/src/rehydrate.rs | 36 ++++++++++----------- packages/web/tests/hydrate.rs | 42 +++++++++++++++++++------ 6 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 packages/web/.vscode/settings.json diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index aa937728b..3f0dc5ce1 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -12,7 +12,7 @@ extern "C" { pub fn new(arg: Element) -> Interpreter; #[wasm_bindgen(method)] - pub fn set_node(this: &Interpreter, id: usize, node: Node); + pub fn SetNode(this: &Interpreter, id: usize, node: Node); #[wasm_bindgen(method)] pub fn PushRoot(this: &Interpreter, root: u64); diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f6b4fe200..af7c42a96 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -20,6 +20,9 @@ export class Interpreter { pop() { return this.stack.pop(); } + SetNode(id, node) { + this.nodes[id] = node; + } PushRoot(root) { const node = this.nodes[root]; this.stack.push(node); diff --git a/packages/web/.vscode/settings.json b/packages/web/.vscode/settings.json new file mode 100644 index 000000000..8974c7a87 --- /dev/null +++ b/packages/web/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.target": "wasm32-unknown-unknown", +} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 257fd8529..fb4fee77b 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -13,7 +13,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.2.0" } dioxus-html = { path = "../html", version = "^0.2.0" } -dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.0", features = ["web"] } +dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.0", features = [ + "web", +] } js-sys = "0.3.56" wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] } @@ -79,4 +81,3 @@ dioxus-core-macro = { path = "../core-macro" } wasm-bindgen-test = "0.3.29" dioxus-ssr = { path = "../ssr" } wasm-logger = "0.2.0" - diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 387baab59..24e46d11e 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -80,8 +80,7 @@ impl WebsysDom { *last_node_was_text = true; - self.interpreter.set_node(node_id.0, node); - // self.nodes[node_id.0] = Some(node); + self.interpreter.SetNode(node_id.0, node); *cur_place += 1; } @@ -93,21 +92,7 @@ impl WebsysDom { let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - use smallstr::SmallString; - use std::fmt::Write; - - // 8 digits is enough, yes? - // 12 million nodes in one page? - let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); - write!(s, "{}", node_id).unwrap(); - - node.dyn_ref::() - .unwrap() - .set_attribute("dioxus-id", s.as_str()) - .unwrap(); - - self.interpreter.set_node(node_id.0, node.clone()); - // self.nodes[node_id.0] = Some(node.clone()); + self.interpreter.SetNode(node_id.0, node.clone()); *cur_place += 1; @@ -129,6 +114,21 @@ impl WebsysDom { ); } + if !vel.listeners.is_empty() { + use smallstr::SmallString; + use std::fmt::Write; + + // 8 digits is enough, yes? + // 12 million nodes in one page? + let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); + write!(s, "{}", node_id).unwrap(); + + node.dyn_ref::() + .unwrap() + .set_attribute("dioxus-id", s.as_str()) + .unwrap(); + } + place.pop(); nodes.pop(); @@ -145,7 +145,7 @@ impl WebsysDom { let cur_place = place.last_mut().unwrap(); let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - self.interpreter.set_node(node_id.0, node); + self.interpreter.SetNode(node_id.0, node); // self.nodes[node_id.0] = Some(node); diff --git a/packages/web/tests/hydrate.rs b/packages/web/tests/hydrate.rs index b8a034a5b..76f851c83 100644 --- a/packages/web/tests/hydrate.rs +++ b/packages/web/tests/hydrate.rs @@ -2,6 +2,7 @@ use dioxus_core::prelude::*; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; use wasm_bindgen_test::wasm_bindgen_test; +use web_sys::window; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -10,10 +11,12 @@ fn makes_tree() { fn app(cx: Scope) -> Element { cx.render(rsx! { div { - h1 {} - } - div { - h2 {} + div { + h1 {} + } + div { + h2 {} + } } }) } @@ -29,13 +32,34 @@ fn rehydrates() { fn app(cx: Scope) -> Element { cx.render(rsx! { div { - h1 {} - } - div { - h2 {} + div { + h1 { "h1" } + } + div { + h2 { "h2" } + } + button { + onclick: move |_| { + println!("clicked"); + }, + "listener test" + } + false.then(|| rsx!{ "hello" }) } }) } - dioxus_web::launch(app); + let mut dom = VirtualDom::new(app); + let _ = dom.rebuild(); + let out = dioxus_ssr::render_vdom_cfg(&dom, |c| c.pre_render(true)); + + window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap() + .set_inner_html(&format!("
{}
", out)); + + dioxus_web::launch_cfg(app, |c| c.hydrate(true)); } From ab4e37fbf2a5367b43aeb0ec1867a5535cd3a3a5 Mon Sep 17 00:00:00 2001 From: Joshua Kifer Date: Sat, 19 Mar 2022 12:24:29 -0600 Subject: [PATCH 06/18] Capture correct radio button value in a form input event --- packages/web/src/dom.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 3ecfe9913..351189e2b 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -241,25 +241,33 @@ fn virtual_event_from_websys_event( for x in 0..elements.length() { let element = elements.item(x).unwrap(); if let Some(name) = element.get_attribute("name") { - let value: String = (&element) + let value: Option = (&element) .dyn_ref() .map(|input: &web_sys::HtmlInputElement| { + log::info!("Input type: {}", input.type_()); match input.type_().as_str() { "checkbox" => { match input.checked() { - true => "true".to_string(), - false => "false".to_string(), + true => Some("true".to_string()), + false => Some("false".to_string()), } }, - _ => input.value() + "radio" => { + match input.checked() { + true => Some(input.value()), + false => None, + } + } + _ => Some(input.value()) } }) - .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| input.value())) - .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| input.value())) - .or_else(|| target.dyn_ref::().unwrap().text_content()) + .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value()))) + .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value()))) + .or_else(|| Some(target.dyn_ref::().unwrap().text_content())) .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - - values.insert(name, value); + if let Some(value) = value { + values.insert(name, value); + } } } } From 0be88d20ee94cd9a122bc825fcd33e2ca2ca3f7a Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Sat, 19 Mar 2022 19:28:38 +0100 Subject: [PATCH 07/18] docs: remove capitals and periods from errors --- packages/core-macro/src/rsx/component.rs | 2 +- packages/core-macro/src/rsx/element.rs | 12 ++++++------ packages/rsx/src/component.rs | 2 +- packages/rsx/src/element.rs | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 17006e6ac..064e2dad6 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -200,7 +200,7 @@ impl Parse for ComponentField { if input.peek(LitStr) && input.peek2(LitStr) { let item = input.parse::().unwrap(); - proc_macro_error::emit_error!(item, "This attribute is missing a trailing comma") + proc_macro_error::emit_error!(item, "this attribute is missing a trailing comma") } let content = ContentField::ManExpr(input.parse()?); diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index fa8fc9f51..92aa949ce 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -67,7 +67,7 @@ impl Parse for Element { if content.parse::().is_err() { proc_macro_error::emit_error!( ident, - "This attribute is missing a trailing comma" + "this attribute is missing a trailing comma" ) } continue; @@ -128,7 +128,7 @@ impl Parse for Element { if content.parse::().is_err() { proc_macro_error::emit_error!( ident, - "This attribute is missing a trailing comma" + "this attribute is missing a trailing comma" ) } continue; @@ -142,9 +142,9 @@ impl Parse for Element { let ident = content.parse::().unwrap(); let name = ident.value(); proc_macro_error::emit_error!( - ident, "This attribute `{}` is in the wrong place.", name; + ident, "this attribute `{}` is in the wrong place", name; help = -"All attribute fields must be placed above children elements. +"all attribute fields must be placed above children elements div { attr: \"...\", <---- attribute is above children @@ -157,9 +157,9 @@ impl Parse for Element { let ident = content.parse::().unwrap(); let name = ident.to_string(); proc_macro_error::emit_error!( - ident, "This attribute `{}` is in the wrong place.", name; + ident, "this attribute `{}` is in the wrong place", name; help = -"All attribute fields must be placed above children elements. +"all attribute fields must be placed above children elements div { attr: \"...\", <---- attribute is above children diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 17006e6ac..064e2dad6 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -200,7 +200,7 @@ impl Parse for ComponentField { if input.peek(LitStr) && input.peek2(LitStr) { let item = input.parse::().unwrap(); - proc_macro_error::emit_error!(item, "This attribute is missing a trailing comma") + proc_macro_error::emit_error!(item, "this attribute is missing a trailing comma") } let content = ContentField::ManExpr(input.parse()?); diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index fa8fc9f51..92aa949ce 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -67,7 +67,7 @@ impl Parse for Element { if content.parse::().is_err() { proc_macro_error::emit_error!( ident, - "This attribute is missing a trailing comma" + "this attribute is missing a trailing comma" ) } continue; @@ -128,7 +128,7 @@ impl Parse for Element { if content.parse::().is_err() { proc_macro_error::emit_error!( ident, - "This attribute is missing a trailing comma" + "this attribute is missing a trailing comma" ) } continue; @@ -142,9 +142,9 @@ impl Parse for Element { let ident = content.parse::().unwrap(); let name = ident.value(); proc_macro_error::emit_error!( - ident, "This attribute `{}` is in the wrong place.", name; + ident, "this attribute `{}` is in the wrong place", name; help = -"All attribute fields must be placed above children elements. +"all attribute fields must be placed above children elements div { attr: \"...\", <---- attribute is above children @@ -157,9 +157,9 @@ impl Parse for Element { let ident = content.parse::().unwrap(); let name = ident.to_string(); proc_macro_error::emit_error!( - ident, "This attribute `{}` is in the wrong place.", name; + ident, "this attribute `{}` is in the wrong place", name; help = -"All attribute fields must be placed above children elements. +"all attribute fields must be placed above children elements div { attr: \"...\", <---- attribute is above children From b8573bc4dd1816c728ae70ca1eb9437fa4b0ecf8 Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Sat, 19 Mar 2022 19:37:42 +0100 Subject: [PATCH 08/18] refactor: remove the rsx folder it doesn't seem to be published anywhere or used at all --- Cargo.toml | 1 - packages/rsx/Cargo.toml | 12 -- packages/rsx/src/component.rs | 234 ---------------------------- packages/rsx/src/element.rs | 282 ---------------------------------- packages/rsx/src/lib.rs | 97 ------------ packages/rsx/src/node.rs | 86 ----------- packages/rsx/src/pretty.rs | 1 - 7 files changed, 713 deletions(-) delete mode 100644 packages/rsx/Cargo.toml delete mode 100644 packages/rsx/src/component.rs delete mode 100644 packages/rsx/src/element.rs delete mode 100644 packages/rsx/src/lib.rs delete mode 100644 packages/rsx/src/node.rs delete mode 100644 packages/rsx/src/pretty.rs diff --git a/Cargo.toml b/Cargo.toml index de2218ee6..e5ec20306 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,6 @@ liveview = ["dioxus-liveview"] members = [ "packages/core", "packages/core-macro", - "packages/rsx", "packages/html", "packages/hooks", "packages/web", diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml deleted file mode 100644 index 0d84ba250..000000000 --- a/packages/rsx/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "dioxus-rsx" -version = "0.1.0" -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -proc-macro-error = "1" -proc-macro2 = { version = "1.0.6" } -quote = "1.0" -syn = { version = "1.0.11", features = ["full", "extra-traits"] } diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs deleted file mode 100644 index 064e2dad6..000000000 --- a/packages/rsx/src/component.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! Parse components into the VComponent VNode -//! ========================================== -//! -//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the vcomponent format. -//! We can be reasonably sure that whatever enters this parsing path is in the right format. -//! This feature must support -//! - [x] Namespaced components -//! - [x] Fields -//! - [x] Componentbuilder synax -//! - [x] Optional commas -//! - [ ] Children -//! - [ ] Keys -//! - [ ] Properties spreading with with `..` syntax - -use super::*; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - ext::IdentExt, - parse::{Parse, ParseBuffer, ParseStream}, - token, Expr, Ident, LitStr, Result, Token, -}; - -pub struct Component { - pub name: syn::Path, - pub body: Vec, - pub children: Vec, - pub manual_props: Option, -} - -impl Parse for Component { - fn parse(stream: ParseStream) -> Result { - let name = syn::Path::parse_mod_style(stream)?; - - let content: ParseBuffer; - - // if we see a `{` then we have a block - // else parse as a function-like call - if stream.peek(token::Brace) { - syn::braced!(content in stream); - } else { - syn::parenthesized!(content in stream); - } - - let mut body = Vec::new(); - let mut children = Vec::new(); - let mut manual_props = None; - - while !content.is_empty() { - // if we splat into a component then we're merging properties - if content.peek(Token![..]) { - content.parse::()?; - manual_props = Some(content.parse::()?); - } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - body.push(content.parse::()?); - } else { - children.push(content.parse::()?); - } - - if content.peek(Token![,]) { - let _ = content.parse::(); - } - } - - Ok(Self { - name, - body, - children, - manual_props, - }) - } -} - -impl ToTokens for Component { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let name = &self.name; - - let mut has_key = None; - - let builder = match &self.manual_props { - Some(manual_props) => { - let mut toks = quote! { - let mut __manual_props = #manual_props; - }; - for field in &self.body { - if field.name == "key" { - has_key = Some(field); - } else { - let name = &field.name; - let val = &field.content; - toks.append_all(quote! { - __manual_props.#name = #val; - }); - } - } - toks.append_all(quote! { - __manual_props - }); - quote! {{ - #toks - }} - } - None => { - let mut toks = quote! { fc_to_builder(#name) }; - for field in &self.body { - match field.name.to_string().as_str() { - "key" => { - // - has_key = Some(field); - } - _ => toks.append_all(quote! {#field}), - } - } - - if !self.children.is_empty() { - let childs = &self.children; - toks.append_all(quote! { - .children(__cx.create_children([ #( #childs ),* ])) - }); - } - - toks.append_all(quote! { - .build() - }); - toks - } - }; - - let key_token = match has_key { - Some(field) => { - let inners = &field.content; - quote! { Some(format_args_f!(#inners)) } - } - None => quote! { None }, - }; - - let fn_name = self.name.segments.last().unwrap().ident.to_string(); - - tokens.append_all(quote! { - __cx.component( - #name, - #builder, - #key_token, - #fn_name - ) - }) - } -} - -// the struct's fields info -pub struct ComponentField { - name: Ident, - content: ContentField, -} - -enum ContentField { - ManExpr(Expr), - Formatted(LitStr), - OnHandlerRaw(Expr), -} - -impl ToTokens for ContentField { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match self { - ContentField::ManExpr(e) => e.to_tokens(tokens), - ContentField::Formatted(s) => tokens.append_all(quote! { - __cx.raw_text(format_args_f!(#s)).0 - }), - ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { - __cx.event_handler(#e) - }), - } - } -} - -impl Parse for ComponentField { - fn parse(input: ParseStream) -> Result { - let name = Ident::parse_any(input)?; - input.parse::()?; - - if name.to_string().starts_with("on") { - let content = ContentField::OnHandlerRaw(input.parse()?); - return Ok(Self { name, content }); - } - - if name == "key" { - let content = ContentField::ManExpr(input.parse()?); - return Ok(Self { name, content }); - } - - if input.peek(LitStr) && input.peek2(Token![,]) { - let t: LitStr = input.fork().parse()?; - - if is_literal_foramtted(&t) { - let content = ContentField::Formatted(input.parse()?); - return Ok(Self { name, content }); - } - } - - if input.peek(LitStr) && input.peek2(LitStr) { - let item = input.parse::().unwrap(); - proc_macro_error::emit_error!(item, "this attribute is missing a trailing comma") - } - - let content = ContentField::ManExpr(input.parse()?); - Ok(Self { name, content }) - } -} - -impl ToTokens for ComponentField { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let ComponentField { name, content, .. } = self; - tokens.append_all(quote! { - .#name(#content) - }) - } -} - -fn is_literal_foramtted(lit: &LitStr) -> bool { - let s = lit.value(); - let mut chars = s.chars(); - - while let Some(next) = chars.next() { - if next == '{' { - let nen = chars.next(); - if nen != Some('{') { - return true; - } - } - } - - false -} diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs deleted file mode 100644 index 92aa949ce..000000000 --- a/packages/rsx/src/element.rs +++ /dev/null @@ -1,282 +0,0 @@ -use super::*; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseBuffer, ParseStream}, - Expr, Ident, LitStr, Result, Token, -}; - -// ======================================= -// Parse the VNode::Element type -// ======================================= -pub struct Element { - pub name: Ident, - pub key: Option, - pub attributes: Vec, - pub children: Vec, - pub _is_static: bool, -} - -impl Parse for Element { - fn parse(stream: ParseStream) -> Result { - let el_name = Ident::parse(stream)?; - - // parse the guts - let content: ParseBuffer; - syn::braced!(content in stream); - - let mut attributes: Vec = vec![]; - let mut children: Vec = vec![]; - let mut key = None; - let mut _el_ref = None; - - // parse fields with commas - // break when we don't get this pattern anymore - // start parsing bodynodes - // "def": 456, - // abc: 123, - loop { - // Parse the raw literal fields - if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - let name = content.parse::()?; - let ident = name.clone(); - - content.parse::()?; - - if content.peek(LitStr) && content.peek2(Token![,]) { - let value = content.parse::()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrText { name, value }, - }); - } else { - let value = content.parse::()?; - - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrExpression { name, value }, - }); - } - - if content.is_empty() { - break; - } - - // todo: add a message saying you need to include commas between fields - if content.parse::().is_err() { - proc_macro_error::emit_error!( - ident, - "this attribute is missing a trailing comma" - ) - } - continue; - } - - if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - let name = content.parse::()?; - let ident = name.clone(); - - let name_str = name.to_string(); - content.parse::()?; - - if name_str.starts_with("on") { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, - }, - }); - } else { - match name_str.as_str() { - "key" => { - key = Some(content.parse()?); - } - "classes" => todo!("custom class list not supported yet"), - // "namespace" => todo!("custom namespace not supported yet"), - "node_ref" => { - _el_ref = Some(content.parse::()?); - } - _ => { - if content.peek(LitStr) { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name, - value: content.parse()?, - }, - }); - } else { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::AttrExpression { - name, - value: content.parse()?, - }, - }); - } - } - } - } - - if content.is_empty() { - break; - } - - // todo: add a message saying you need to include commas between fields - if content.parse::().is_err() { - proc_macro_error::emit_error!( - ident, - "this attribute is missing a trailing comma" - ) - } - continue; - } - - break; - } - - while !content.is_empty() { - if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { - let ident = content.parse::().unwrap(); - let name = ident.value(); - proc_macro_error::emit_error!( - ident, "this attribute `{}` is in the wrong place", name; - help = -"all attribute fields must be placed above children elements - - div { - attr: \"...\", <---- attribute is above children - div { } <---- children are below attributes - }"; - ) - } - - if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) { - let ident = content.parse::().unwrap(); - let name = ident.to_string(); - proc_macro_error::emit_error!( - ident, "this attribute `{}` is in the wrong place", name; - help = -"all attribute fields must be placed above children elements - - div { - attr: \"...\", <---- attribute is above children - div { } <---- children are below attributes - }"; - ) - } - - children.push(content.parse::()?); - // consume comma if it exists - // we don't actually care if there *are* commas after elements/text - if content.peek(Token![,]) { - let _ = content.parse::(); - } - } - - Ok(Self { - key, - name: el_name, - attributes, - children, - _is_static: false, - }) - } -} - -impl ToTokens for Element { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let name = &self.name; - let children = &self.children; - - let key = match &self.key { - Some(ty) => quote! { Some(format_args_f!(#ty)) }, - None => quote! { None }, - }; - - let listeners = self - .attributes - .iter() - .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); - - let attr = self - .attributes - .iter() - .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); - - tokens.append_all(quote! { - __cx.element( - dioxus_elements::#name, - __cx.bump().alloc([ #(#listeners),* ]), - __cx.bump().alloc([ #(#attr),* ]), - __cx.bump().alloc([ #(#children),* ]), - #key, - ) - }); - } -} - -pub enum ElementAttr { - /// attribute: "valuee {}" - AttrText { name: Ident, value: LitStr }, - - /// attribute: true, - AttrExpression { name: Ident, value: Expr }, - - /// "attribute": "value {}" - CustomAttrText { name: LitStr, value: LitStr }, - - /// "attribute": true, - CustomAttrExpression { name: LitStr, value: Expr }, - - // /// onclick: move |_| {} - // EventClosure { name: Ident, closure: ExprClosure }, - /// onclick: {} - EventTokens { name: Ident, tokens: Expr }, -} - -pub struct ElementAttrNamed { - pub el_name: Ident, - pub attr: ElementAttr, -} - -impl ToTokens for ElementAttrNamed { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let ElementAttrNamed { el_name, attr } = self; - - tokens.append_all(match attr { - ElementAttr::AttrText { name, value } => { - quote! { - dioxus_elements::#el_name.#name(__cx, format_args_f!(#value)) - } - } - ElementAttr::AttrExpression { name, value } => { - quote! { - dioxus_elements::#el_name.#name(__cx, #value) - } - } - ElementAttr::CustomAttrText { name, value } => { - quote! { - __cx.attr( #name, format_args_f!(#value), None, false ) - } - } - ElementAttr::CustomAttrExpression { name, value } => { - quote! { - __cx.attr( #name, format_args_f!(#value), None, false ) - } - } - // ElementAttr::EventClosure { name, closure } => { - // quote! { - // dioxus_elements::on::#name(__cx, #closure) - // } - // } - ElementAttr::EventTokens { name, tokens } => { - quote! { - dioxus_elements::on::#name(__cx, #tokens) - } - } - }); - } -} diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs deleted file mode 100644 index 858f07c5c..000000000 --- a/packages/rsx/src/lib.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Parse the root tokens in the rsx!{} macro -//! ========================================= -//! -//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing. -//! This feature must support: -//! - [x] Optionally rendering if the `in XYZ` pattern is present -//! - [x] Fragments as top-level element (through ambiguous) -//! - [x] Components as top-level element (through ambiguous) -//! - [x] Tags as top-level elements (through ambiguous) -//! - [x] Good errors if parsing fails -//! -//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. - -mod component; -mod element; -mod node; - -pub mod pretty; - -// Re-export the namespaces into each other -pub use component::*; -pub use element::*; -pub use node::*; - -// imports -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - Ident, Result, Token, -}; - -pub struct CallBody { - pub custom_context: Option, - pub roots: Vec, -} - -impl Parse for CallBody { - fn parse(input: ParseStream) -> Result { - let custom_context = if input.peek(Ident) && input.peek2(Token![,]) { - let name = input.parse::()?; - input.parse::()?; - - Some(name) - } else { - None - }; - - let mut roots = Vec::new(); - - while !input.is_empty() { - let node = input.parse::()?; - - if input.peek(Token![,]) { - let _ = input.parse::(); - } - - roots.push(node); - } - - Ok(Self { - custom_context, - roots, - }) - } -} - -/// Serialize the same way, regardless of flavor -impl ToTokens for CallBody { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let inner = if self.roots.len() == 1 { - let inner = &self.roots[0]; - quote! { #inner } - } else { - let childs = &self.roots; - quote! { __cx.fragment_root([ #(#childs),* ]) } - }; - - match &self.custom_context { - // The `in cx` pattern allows directly rendering - Some(ident) => out_tokens.append_all(quote! { - #ident.render(LazyNodes::new(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - })) - }), - - // Otherwise we just build the LazyNode wrapper - None => out_tokens.append_all(quote! { - LazyNodes::new(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - }) - }), - }; - } -} diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs deleted file mode 100644 index eeee421b0..000000000 --- a/packages/rsx/src/node.rs +++ /dev/null @@ -1,86 +0,0 @@ -use super::*; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - token, Attribute, Expr, LitStr, Result, Token, -}; - -/* -Parse --> div {} --> Component {} --> component() --> "text {with_args}" --> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr -*/ -pub enum BodyNode { - Element(Element), - Component(Component), - Text(LitStr), - RawExpr(Expr), - Meta(Attribute), -} - -impl Parse for BodyNode { - fn parse(stream: ParseStream) -> Result { - if stream.peek(LitStr) { - return Ok(BodyNode::Text(stream.parse()?)); - } - - // div {} -> el - // Div {} -> comp - if stream.peek(syn::Ident) && stream.peek2(token::Brace) { - if stream - .fork() - .parse::()? - .to_string() - .chars() - .next() - .unwrap() - .is_ascii_uppercase() - { - return Ok(BodyNode::Component(stream.parse()?)); - } else { - return Ok(BodyNode::Element(stream.parse::()?)); - } - } - - // component() -> comp - // ::component {} -> comp - // ::component () -> comp - if (stream.peek(syn::Ident) && stream.peek2(token::Paren)) - || (stream.peek(Token![::])) - || (stream.peek(Token![:]) && stream.peek2(Token![:])) - { - return Ok(BodyNode::Component(stream.parse::()?)); - } - - // crate::component{} -> comp - // crate::component() -> comp - if let Ok(pat) = stream.fork().parse::() { - if pat.segments.len() > 1 { - return Ok(BodyNode::Component(stream.parse::()?)); - } - } - - Ok(BodyNode::RawExpr(stream.parse::()?)) - } -} - -impl ToTokens for BodyNode { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match &self { - BodyNode::Element(el) => el.to_tokens(tokens), - BodyNode::Component(comp) => comp.to_tokens(tokens), - BodyNode::Text(txt) => tokens.append_all(quote! { - __cx.text(format_args_f!(#txt)) - }), - BodyNode::RawExpr(exp) => tokens.append_all(quote! { - __cx.fragment_from_iter(#exp) - }), - BodyNode::Meta(_) => {} - } - } -} diff --git a/packages/rsx/src/pretty.rs b/packages/rsx/src/pretty.rs deleted file mode 100644 index 26ad54006..000000000 --- a/packages/rsx/src/pretty.rs +++ /dev/null @@ -1 +0,0 @@ -//! pretty printer for rsx! From 1f065acb4c4e4fbae152d2fffc603ab6434dc4ee Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Sat, 19 Mar 2022 20:37:25 +0100 Subject: [PATCH 09/18] refactor: simplify and organize the errors --- packages/core-macro/src/rsx/component.rs | 3 +- packages/core-macro/src/rsx/element.rs | 37 +++--------------------- packages/core-macro/src/rsx/errors.rs | 15 ++++++++++ packages/core-macro/src/rsx/mod.rs | 3 ++ 4 files changed, 23 insertions(+), 35 deletions(-) create mode 100644 packages/core-macro/src/rsx/errors.rs diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 064e2dad6..ce906e76f 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -199,8 +199,7 @@ impl Parse for ComponentField { } if input.peek(LitStr) && input.peek2(LitStr) { - let item = input.parse::().unwrap(); - proc_macro_error::emit_error!(item, "this attribute is missing a trailing comma") + attr_after_element!(input.span()); } let content = ContentField::ManExpr(input.parse()?); diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 92aa949ce..96c7a2efc 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -63,12 +63,8 @@ impl Parse for Element { break; } - // todo: add a message saying you need to include commas between fields if content.parse::().is_err() { - proc_macro_error::emit_error!( - ident, - "this attribute is missing a trailing comma" - ) + missing_trailing_comma!(ident); } continue; } @@ -126,10 +122,7 @@ impl Parse for Element { // todo: add a message saying you need to include commas between fields if content.parse::().is_err() { - proc_macro_error::emit_error!( - ident, - "this attribute is missing a trailing comma" - ) + missing_trailing_comma!(ident); } continue; } @@ -139,33 +132,11 @@ impl Parse for Element { while !content.is_empty() { if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { - let ident = content.parse::().unwrap(); - let name = ident.value(); - proc_macro_error::emit_error!( - ident, "this attribute `{}` is in the wrong place", name; - help = -"all attribute fields must be placed above children elements - - div { - attr: \"...\", <---- attribute is above children - div { } <---- children are below attributes - }"; - ) + attr_after_element!(content.span()); } if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) { - let ident = content.parse::().unwrap(); - let name = ident.to_string(); - proc_macro_error::emit_error!( - ident, "this attribute `{}` is in the wrong place", name; - help = -"all attribute fields must be placed above children elements - - div { - attr: \"...\", <---- attribute is above children - div { } <---- children are below attributes - }"; - ) + attr_after_element!(content.span()); } children.push(content.parse::()?); diff --git a/packages/core-macro/src/rsx/errors.rs b/packages/core-macro/src/rsx/errors.rs new file mode 100644 index 000000000..27e2c7f98 --- /dev/null +++ b/packages/core-macro/src/rsx/errors.rs @@ -0,0 +1,15 @@ +macro_rules! missing_trailing_comma { + ($span:expr) => { + proc_macro_error::emit_error!($span, "missing trailing comma") + }; +} + +macro_rules! attr_after_element { + ($span:expr) => { + proc_macro_error::emit_error!( + $span, + "expected element"; + help = "move the attribute above all the children and text elements" + ) + }; +} diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs index 858f07c5c..6b9dfc3dc 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/core-macro/src/rsx/mod.rs @@ -11,6 +11,9 @@ //! //! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. +#[macro_use] +mod errors; + mod component; mod element; mod node; From 86159d0b552b451b1831ad3a1d5c6f0635e8b41e Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Sat, 19 Mar 2022 20:48:54 +0100 Subject: [PATCH 10/18] fix: wrong type of error --- packages/core-macro/src/rsx/component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index ce906e76f..dfdda74b8 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -199,7 +199,7 @@ impl Parse for ComponentField { } if input.peek(LitStr) && input.peek2(LitStr) { - attr_after_element!(input.span()); + missing_trailing_comma!(input.span()); } let content = ContentField::ManExpr(input.parse()?); From 334936b27609278e8887971eb11a23f2082bdd31 Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Sat, 19 Mar 2022 21:04:48 +0100 Subject: [PATCH 11/18] fix: typos --- packages/core-macro/src/inlineprops.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-macro/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs index ac71d5601..211f8be95 100644 --- a/packages/core-macro/src/inlineprops.rs +++ b/packages/core-macro/src/inlineprops.rs @@ -34,7 +34,7 @@ impl Parse for InlinePropsBody { let first_arg: FnArg = content.parse()?; let cx_token = { match first_arg { - FnArg::Receiver(_) => panic!("first argument must not be a reciver argument"), + FnArg::Receiver(_) => panic!("first argument must not be a receiver argument"), FnArg::Typed(f) => f.pat, } }; From ae2e8a803895d561701462e3257c7747b280be10 Mon Sep 17 00:00:00 2001 From: Joshua Kifer Date: Sat, 19 Mar 2022 17:05:48 -0600 Subject: [PATCH 12/18] Add some radio buttons to the form example --- examples/form.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/form.rs b/examples/form.rs index 3637826da..3b8d470b3 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -20,6 +20,8 @@ fn app(cx: Scope) -> Element { input { r#type: "text", name: "username" } input { r#type: "text", name: "full-name" } input { r#type: "password", name: "password" } + input { r#type: "radio", name: "color", value: "red" } + input { r#type: "radio", name: "color", value: "blue" } button { r#type: "submit", value: "Submit", "Submit the form" } } } From 10824727564ce969722e89d78b8db8859a5c5e62 Mon Sep 17 00:00:00 2001 From: Imbolc Date: Sun, 20 Mar 2022 08:42:45 +0300 Subject: [PATCH 13/18] Update setup.md A typo --- docs/guide/src/setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 1bbcec213..5e4c9e347 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -56,7 +56,7 @@ When using Debian/bullseye `libappindicator3-dev` is no longer available but rep ``` # on Debian/bullseye use: -sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatanta-appindicator3-dev +sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev ``` If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/en/docs/get-started/setup-linux). From 46b7f3b62308907f4acca705d3a76dba1633356a Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Sun, 20 Mar 2022 20:59:30 +0100 Subject: [PATCH 14/18] feat: basic generic support --- packages/core-macro/src/inlineprops.rs | 49 +++++++++++++++++++++----- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/core-macro/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs index 211f8be95..a0f12706a 100644 --- a/packages/core-macro/src/inlineprops.rs +++ b/packages/core-macro/src/inlineprops.rs @@ -3,7 +3,7 @@ use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, - token, Block, FnArg, Generics, Ident, Pat, Result, ReturnType, Token, Visibility, + *, }; pub struct InlinePropsBody { @@ -86,26 +86,57 @@ impl ToTokens for InlinePropsBody { FnArg::Typed(t) => Some(&t.pat), }); - let modifiers = if generics.params.is_empty() { - quote! { #[derive(Props, PartialEq)] } + let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() { + Some(lt) } else { - quote! { #[derive(Props)] } + None }; - let lifetime = if generics.params.is_empty() { - quote! {} + let modifiers = if first_lifetime.is_some() { + quote! { #[derive(Props)] } } else { - quote! { 'a, } + quote! { #[derive(Props, PartialEq)] } + }; + + let (scope_lifetime, fn_generics, struct_generics) = if let Some(lt) = first_lifetime { + let struct_generics: Punctuated<_, token::Comma> = generics + .params + .iter() + .map(|it| match it { + GenericParam::Type(tp) => { + let mut tp = tp.clone(); + tp.bounds.push(parse_quote!( 'a )); + + GenericParam::Type(tp) + } + _ => it.clone(), + }) + .collect(); + + ( + quote! { #lt, }, + generics.clone(), + quote! { <#struct_generics> }, + ) + } else { + let lifetime: LifetimeDef = parse_quote! { 'a }; + + let mut fn_generics = generics.clone(); + fn_generics + .params + .insert(0, GenericParam::Lifetime(lifetime.clone())); + + (quote! { #lifetime, }, fn_generics, quote! { #generics }) }; out_tokens.append_all(quote! { #modifiers #[allow(non_camel_case_types)] - #vis struct #struct_name #generics { + #vis struct #struct_name #struct_generics { #(#fields),* } - #vis fn #ident #generics (#cx_token: Scope<#lifetime #struct_name #generics>) #output { + #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output { let #struct_name { #(#field_names),* } = &cx.props; #block } From ff459f7297e7344fcb8eba6d769665eb3291cc10 Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Sun, 20 Mar 2022 20:59:50 +0100 Subject: [PATCH 15/18] docs: an example for inlineprops --- examples/README.md | 17 +++++++++-------- examples/inlineprops.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 examples/inlineprops.rs diff --git a/examples/README.md b/examples/README.md index e108c56cd..1f96c66bb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -41,6 +41,7 @@ These examples are not necessarily meant to be run, but rather serve as a refere | [Anti-patterns](./antipatterns.rs) | A collection of discouraged patterns | ✅ | | [Complete rsx reference](./rsx_usage.rs) | A complete reference for all rsx! usage | ✅ | | [Event Listeners](./listener.rs) | Attach closures to events on elements | ✅ | +| [Inline Props](./inlineprops.rs) | Using the `#[inling_props]` macro | ✅ | ## Show me some examples! @@ -51,7 +52,7 @@ In our collection of examples, guides, and tutorials, we have: - The reference (a collection of examples with heavy documentation) - The general examples - The platform-specific examples (web, ssr, desktop, mobile, server) - + Here's what a few common tasks look like in Dioxus: Nested components with children and internal state: @@ -80,7 +81,7 @@ Controlled inputs: ```rust fn App(cx: Scope) -> Element { let value = use_state(&cx, String::new); - cx.render(rsx!( + cx.render(rsx!( input { "type": "text", value: "{value}", @@ -96,16 +97,16 @@ fn App(cx: Scope) -> Element { let list = (0..10).map(|i| { rsx!(li { key: "{i}", "Value: {i}" }) }); - + let title = match list.len() { 0 => rsx!("Not enough"), _ => rsx!("Plenty!"), }; if should_show { - cx.render(rsx!( + cx.render(rsx!( title, - ul { list } + ul { list } )) } else { None @@ -165,18 +166,18 @@ fn App(cx: Scope) -> Element { Route::Home => rsx!( Home {} ), Route::Post(id) => rsx!( Post { id: id }) } - })) + })) } ``` -Suspense +Suspense ```rust fn App(cx: Scope) -> Element { let doggo = use_suspense(cx, || async { reqwest::get("https://dog.ceo/api/breeds/image/random").await.unwrap().json::().await.unwrap() }, |response| cx.render(rsx!( img { src: "{response.message}" })) ); - + cx.render(rsx!{ div { "One doggo coming right up:", diff --git a/examples/inlineprops.rs b/examples/inlineprops.rs new file mode 100644 index 000000000..e63d5977d --- /dev/null +++ b/examples/inlineprops.rs @@ -0,0 +1,41 @@ +//! Run with `cargo-expand` to see what each one expands to +#![allow(non_snake_case)] + +use dioxus::prelude::*; + +#[inline_props] +fn Thing1(cx: Scope, _a: T) -> Element { + cx.render(rsx! { "" }) +} + +#[inline_props] +fn Thing2(cx: Scope, _a: u32) -> Element<'a> { + cx.render(rsx! { "" }) +} + +#[inline_props] +fn Thing3<'a, T>(cx: Scope<'a>, _a: &'a T) -> Element<'a> { + cx.render(rsx! { "" }) +} + +#[inline_props] +fn Thing4<'a>(cx: Scope<'a>, _a: &'a u32) -> Element<'a> { + cx.render(rsx! { "" }) +} + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let state = use_state(&cx, || 1); + + cx.render(rsx! { + div { + Thing1 { _a: 1 }, + Thing2 { _a: 1 }, + Thing3 { _a: state }, + Thing4 { _a: state }, + } + }) +} From a762c25b7e432043203a5c162148da24051b993d Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Sun, 20 Mar 2022 21:04:48 +0100 Subject: [PATCH 16/18] docs: typo in readme --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 1f96c66bb..69d15cf86 100644 --- a/examples/README.md +++ b/examples/README.md @@ -41,7 +41,7 @@ These examples are not necessarily meant to be run, but rather serve as a refere | [Anti-patterns](./antipatterns.rs) | A collection of discouraged patterns | ✅ | | [Complete rsx reference](./rsx_usage.rs) | A complete reference for all rsx! usage | ✅ | | [Event Listeners](./listener.rs) | Attach closures to events on elements | ✅ | -| [Inline Props](./inlineprops.rs) | Using the `#[inling_props]` macro | ✅ | +| [Inline Props](./inlineprops.rs) | Using the `#[inline_props]` macro | ✅ | ## Show me some examples! From ad027d7aa4f51649041bbfb84cb1186b0423d877 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 20 Mar 2022 20:11:12 -0400 Subject: [PATCH 17/18] fix: radio buttons in forms --- packages/interpreter/src/interpreter.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 86a39c2a1..471cba99a 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -259,6 +259,10 @@ export class Interpreter { if (element.getAttribute("type") === "checkbox") { // @ts-ignore contents.values[name] = element.checked ? "true" : "false"; + } else if (element.getAttribute("type") === "radio") { + if (element.checked) { + contents.values[name] = element.value; + } } else { // @ts-ignore contents.values[name] = @@ -362,9 +366,11 @@ export function serialize_event(event) { case "submit": { let target = event.target; let value = target.value ?? target.textContent; + if (target.type === "checkbox") { value = target.checked ? "true" : "false"; } + return { value: value, values: {}, From dc028df0dd4e69bb829ebded5a686d9b678da29d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 20 Mar 2022 21:50:43 -0400 Subject: [PATCH 18/18] clippy: make it happier --- packages/router/src/components/link.rs | 2 +- packages/tui/tests/margin.rs | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 5f0a2de98..a2d03fba3 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -106,7 +106,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { Some(service) => service.cfg.active_class.clone(), None => None, }; - active_from_router.unwrap_or("active".into()) + active_from_router.unwrap_or_else(|| "active".into()) } }; diff --git a/packages/tui/tests/margin.rs b/packages/tui/tests/margin.rs index 0ad4720c4..53e8978e5 100644 --- a/packages/tui/tests/margin.rs +++ b/packages/tui/tests/margin.rs @@ -23,7 +23,6 @@ fn margin_and_flex_row() { size: stretch::geometry::Size { width: stretch::style::Dimension::Points(100f32), height: stretch::style::Dimension::Points(100f32), - ..Default::default() }, ..Default::default() }, @@ -76,7 +75,6 @@ fn margin_and_flex_row2() { size: stretch::geometry::Size { width: stretch::style::Dimension::Points(100f32), height: stretch::style::Dimension::Points(100f32), - ..Default::default() }, ..Default::default() }, @@ -92,11 +90,4 @@ fn margin_and_flex_row2() { assert_eq!(stretch.layout(node).unwrap().size.height, 100f32); assert_eq!(stretch.layout(node).unwrap().location.x, 0f32); assert_eq!(stretch.layout(node).unwrap().location.y, 0f32); - - dbg!(stretch.layout(node0)); - - // assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32); - // assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32); - // assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32); - // assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32); }