diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 700c5392e..b23648b50 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -11,7 +11,7 @@ fn App(cx: Scope) -> Element { cx.render(rsx! { input { r#type: "file", - accept: ".txt", + accept: ".txt, .rs", multiple: true, onchange: |evt| { to_owned![files_uploaded]; diff --git a/examples/generic_component.rs b/examples/generic_component.rs index 9757b1032..3d6171a9b 100644 --- a/examples/generic_component.rs +++ b/examples/generic_component.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use dioxus::prelude::*; fn main() { @@ -5,9 +7,20 @@ fn main() { } fn app(cx: Scope) -> Element { - cx.render(rsx! { generic_child::{} }) + cx.render(rsx! { generic_child { + data: 0i32 + } }) } -fn generic_child(cx: Scope) -> Element { - cx.render(rsx! { div {} }) +#[derive(PartialEq, Props)] +struct GenericChildProps { + data: T, +} + +fn generic_child(cx: Scope>) -> Element { + let data = &cx.props.data; + + cx.render(rsx! { div { + "{data}" + } }) } diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 6d2cdc830..5bb7d42fc 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -276,13 +276,12 @@ impl Writer<'_> { let start = location.start(); let line_start = start.line - 1; - let this_line = self.src[line_start]; - - let beginning = if this_line.len() > start.column { - this_line[..start.column].trim() - } else { - "" - }; + let beginning = self + .src + .get(line_start) + .filter(|this_line| this_line.len() > start.column) + .map(|this_line| this_line[..start.column].trim()) + .unwrap_or_default(); beginning.is_empty() } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 2269815aa..da5526b32 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -384,51 +384,73 @@ impl VirtualDom { data, }; - // Loop through each dynamic attribute in this template before moving up to the template's parent. - while let Some(el_ref) = parent_path { - // safety: we maintain references of all vnodes in the element slab - let template = unsafe { el_ref.template.unwrap().as_ref() }; - let node_template = template.template.get(); - let target_path = el_ref.path; + // If the event bubbles, we traverse through the tree until we find the target element. + if bubbles { + // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent. + while let Some(el_ref) = parent_path { + // safety: we maintain references of all vnodes in the element slab + let template = unsafe { el_ref.template.unwrap().as_ref() }; + let node_template = template.template.get(); + let target_path = el_ref.path; - for (idx, attr) in template.dynamic_attrs.iter().enumerate() { - let this_path = node_template.attr_paths[idx]; + for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + let this_path = node_template.attr_paths[idx]; - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - if attr.name.trim_start_matches("on") == name - && target_path.is_decendant(&this_path) - { - listeners.push(&attr.value); + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + if attr.name.trim_start_matches("on") == name + && target_path.is_decendant(&this_path) + { + listeners.push(&attr.value); - // Break if the event doesn't bubble anyways - if !bubbles { - break; + // Break if this is the exact target element. + // This means we won't call two listeners with the same name on the same element. This should be + // documented, or be rejected from the rsx! macro outright + if target_path == this_path { + break; + } } + } - // Break if this is the exact target element. - // This means we won't call two listeners with the same name on the same element. This should be - // documented, or be rejected from the rsx! macro outright - if target_path == this_path { - break; + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order + // We check the bubble state between each call to see if the event has been stopped from bubbling + for listener in listeners.drain(..).rev() { + if let AttributeValue::Listener(listener) = listener { + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + + if !uievent.propagates.get() { + return; + } + } + } + + parent_path = template.parent.and_then(|id| self.elements.get(id.0)); + } + } else { + // Otherwise, we just call the listener on the target element + if let Some(el_ref) = parent_path { + // safety: we maintain references of all vnodes in the element slab + let template = unsafe { el_ref.template.unwrap().as_ref() }; + let node_template = template.template.get(); + let target_path = el_ref.path; + + for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + let this_path = node_template.attr_paths[idx]; + + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + // Only call the listener if this is the exact target element. + if attr.name.trim_start_matches("on") == name && target_path == this_path { + if let AttributeValue::Listener(listener) = &attr.value { + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + + break; + } } } } - - // Now that we've accumulated all the parent attributes for the target element, call them in reverse order - // We check the bubble state between each call to see if the event has been stopped from bubbling - for listener in listeners.drain(..).rev() { - if let AttributeValue::Listener(listener) = listener { - if let Some(cb) = listener.borrow_mut().as_deref_mut() { - cb(uievent.clone()); - } - - if !uievent.propagates.get() { - return; - } - } - } - - parent_path = template.parent.and_then(|id| self.elements.get(id.0)); } } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 81639b778..9b93ec9e3 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -167,10 +167,10 @@ class Interpreter { } break; case "checked": - node.checked = value === "true"; + node.checked = value === "true" || value === true; break; case "selected": - node.selected = value === "true"; + node.selected = value === "true" || value === true; break; case "dangerous_inner_html": node.innerHTML = value; @@ -344,11 +344,104 @@ class Interpreter { this.RemoveEventListener(edit.id, edit.name); break; case "NewEventListener": - const bubbles = event_bubbles(edit.name); + let bubbles = event_bubbles(edit.name); - this.NewEventListener(edit.name, edit.id, bubbles, (event) => - handler(event, edit.name, bubbles) - ); + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event) => { + let target = event.target; + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + let shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); + + if (event.type === "click") { + // todo call prevent default if it's the right type of event + let a_element = target.closest("a"); + if (a_element != null) { + event.preventDefault(); + if ( + shouldPreventDefault !== `onclick` && + a_element.getAttribute(`dioxus-prevent-default`) !== `onclick` + ) { + const href = a_element.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.ipc.postMessage( + serializeIpcMessage("browser_open", { href }) + ); + } + } + } + + // also prevent buttons from submitting + if (target.tagName === "BUTTON" && event.type == "submit") { + event.preventDefault(); + } + } + // walk the tree to find the real element + while (realId == null) { + // we've reached the root we don't want to send an event + if (target.parentElement === null) { + return; + } + + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + + shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); + + let contents = serialize_event(event); + + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + + if (event.type === "submit") { + event.preventDefault(); + } + + if ( + target.tagName === "FORM" && + (event.type === "submit" || event.type === "input") + ) { + for (let x = 0; x < target.elements.length; x++) { + let element = target.elements[x]; + let name = element.getAttribute("name"); + if (name != null) { + 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] = + element.value ?? element.textContent; + } + } + } + } + + if (realId === null) { + return; + } + window.ipc.postMessage( + serializeIpcMessage("user_event", { + name: edit.name, + element: parseInt(realId), + data: contents, + bubbles, + }) + ); + } + }; + this.NewEventListener(edit.name, edit.id, bubbles, handler); break; } } diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 61fbe0887..6a7ad6aee 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -18,7 +18,7 @@ futures-util = { version = "0.3.25", default-features = false, features = [ "sink", ] } futures-channel = { version = "0.3.25", features = ["sink"] } -tokio = { version = "1.22.0", features = ["time"] } +tokio = { version = "1.22.0", features = ["time", "macros"] } tokio-stream = { version = "0.1.11", features = ["net"] } tokio-util = { version = "0.7.4", features = ["rt"] } serde = { version = "1.0.151", features = ["derive"] } diff --git a/packages/native-core/Cargo.toml b/packages/native-core/Cargo.toml index 9ea6d4c9f..c9be2f27d 100644 --- a/packages/native-core/Cargo.toml +++ b/packages/native-core/Cargo.toml @@ -29,7 +29,6 @@ lightningcss = "1.0.0-alpha.39" rayon = "1.6.1" shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false } -shipyard_hierarchy = "0.6.0" [dev-dependencies] rand = "0.8.5" diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index 9fceb6b48..7f15aa751 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -98,7 +98,8 @@ pub fn render( let event_tx_clone = event_tx.clone(); if !cfg.headless { std::thread::spawn(move || { - let tick_rate = Duration::from_millis(1000); + // Timeout after 10ms when waiting for events + let tick_rate = Duration::from_millis(10); loop { if crossterm::event::poll(tick_rate).unwrap() { let evt = crossterm::event::read().unwrap(); diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index cf665d2e1..7bfc48993 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -45,13 +45,13 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { router_root.register_total_route(route_context.total_route, cx.scope_id()); }); - log::debug!("Checking Route: {:?}", cx.props.to); + log::trace!("Checking Route: {:?}", cx.props.to); if router_root.should_render(cx.scope_id()) { - log::debug!("Route should render: {:?}", cx.scope_id()); + log::trace!("Route should render: {:?}", cx.scope_id()); cx.render(rsx!(&cx.props.children)) } else { - log::debug!("Route should *not* render: {:?}", cx.scope_id()); + log::trace!("Route should *not* render: {:?}", cx.scope_id()); cx.render(rsx!(())) } } diff --git a/packages/rsx-rosetta/Cargo.toml b/packages/rsx-rosetta/Cargo.toml index d160d2bf9..e6bc45225 100644 --- a/packages/rsx-rosetta/Cargo.toml +++ b/packages/rsx-rosetta/Cargo.toml @@ -25,3 +25,6 @@ convert_case = "0.5.0" # default = ["html"] # eventually more output options + +[dev-dependencies] +pretty_assertions = "1.2.1" \ No newline at end of file diff --git a/packages/rsx-rosetta/tests/simple.rs b/packages/rsx-rosetta/tests/simple.rs new file mode 100644 index 000000000..b8fd095e1 --- /dev/null +++ b/packages/rsx-rosetta/tests/simple.rs @@ -0,0 +1,68 @@ +use html_parser::Dom; + +#[test] +fn simple_elements() { + let html = r#" +
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+ hello world! +
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(&dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + let expected = r#" + div { + div { class: "asd", "hello world!" } + div { id: "asd", "hello world!" } + div { id: "asd", "hello world!" } + div { r#for: "asd", "hello world!" } + div { r#async: "asd", "hello world!" } + div { large_thing: "asd", "hello world!" } + ai_is_awesome { "hello world!" } + }"#; + pretty_assertions::assert_eq!(&out, &expected); +} + +#[test] +fn deeply_nested() { + let html = r#" +
+
+
+
+
+
+
+
+
+
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(&dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + let expected = r#" + div { + div { class: "asd", + div { class: "asd", + div { class: "asd", div { class: "asd" } } + } + } + }"#; + pretty_assertions::assert_eq!(&out, &expected); +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 14705b85a..34aa7033b 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -226,7 +226,7 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop websys_dom.mount(); loop { - log::debug!("waiting for work"); + log::trace!("waiting for work"); // if virtualdom has nothing, wait for it to have something before requesting idle time // if there is work then this future resolves immediately.