From f66d17ca84f80f199e5ce1735a11f087af7fe4ea Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 22 Jan 2022 14:53:59 -0500 Subject: [PATCH 1/7] wip: pass form data between web and desktop --- examples/form.rs | 23 +++++++++++++++++++++++ packages/html/src/events.rs | 3 +++ packages/jsinterpreter/interpreter.js | 18 ++++++++++++++++++ packages/jsinterpreter/interpreter.ts | 24 ++++++++++++++++++++---- packages/web/Cargo.toml | 1 + packages/web/src/dom.rs | 7 +++---- 6 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 examples/form.rs diff --git a/examples/form.rs b/examples/form.rs new file mode 100644 index 0000000000..577658aeb8 --- /dev/null +++ b/examples/form.rs @@ -0,0 +1,23 @@ +//! Example: README.md showcase +//! +//! The example from the README.md. + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Form" } + form { + oninput: move |ev| println!("{:?}", ev), + input { r#type: "text", name: "username" } + input { r#type: "text", name: "full-name" } + input { r#type: "password", name: "password" } + } + } + }) +} diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index b9bcecd4f2..8067e63140 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -3,6 +3,8 @@ use dioxus_core::exports::bumpalo; use dioxus_core::*; pub mod on { + use std::collections::HashMap; + use super::*; macro_rules! event_directory { ( $( @@ -484,6 +486,7 @@ pub mod on { #[derive(Debug)] pub struct FormData { pub value: String, + pub values: HashMap, /* DOMEvent: Send + SyncTarget relatedTarget */ } diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index ea50ec3d39..09cfa194b9 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -231,6 +231,23 @@ export class Interpreter { } } } + if (target.tagName == "FORM") { + let formTarget = target; + for (let x = 0; x < formTarget.elements.length; x++) { + let element = formTarget.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 { + // @ts-ignore + contents.values[name] = element.value ?? element.textContent; + } + } + } + } if (realId == null) { return; } @@ -316,6 +333,7 @@ function serialize_event(event) { } return { value: value, + values: {} }; } case "click": diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index fdd8c671d3..744f910818 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -244,7 +244,6 @@ export class Interpreter { break; case "NewEventListener": - // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event: Event) => { @@ -286,6 +285,23 @@ export class Interpreter { } } + if (target.tagName == "FORM") { + let formTarget = target as HTMLFormElement; + for (let x = 0; x < formTarget.elements.length; x++) { + let element = formTarget.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 { + // @ts-ignore + contents.values[name] = element.value ?? element.textContent; + } + } + } + } + if (realId == null) { return; } @@ -385,6 +401,8 @@ function serialize_event(event: Event) { case "invalid": case "reset": case "submit": { + + let target = event.target as HTMLFormElement; let value = target.value ?? target.textContent; @@ -394,6 +412,7 @@ function serialize_event(event: Event) { return { value: value, + values: {} }; } @@ -643,8 +662,6 @@ const bool_attrs = { truespeed: true, }; - - type PushRoot = { type: "PushRoot", root: number }; type AppendChildren = { type: "AppendChildren", many: number }; type ReplaceWith = { type: "ReplaceWith", root: number, m: number }; @@ -661,7 +678,6 @@ type SetText = { type: "SetText", root: number, text: string }; type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined }; type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string }; - type DomEdit = PushRoot | AppendChildren | diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 52a78e6317..6acdb53667 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -41,6 +41,7 @@ features = [ "HtmlInputElement", "HtmlSelectElement", "HtmlTextAreaElement", + "HtmlFormElement", "EventTarget", "HtmlCollection", "Node", diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ffbef9f4bf..b60f2485dc 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -178,7 +178,8 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc anyhow::Result { priority: dioxus_core::EventPriority::Medium, }); } - Some(Err(e)) => { - return Err(e.into()); - } + Some(Err(e)) => return Err(e.into()), None => { // walk the tree upwards until we actually find an event target if let Some(parent) = target.parent_element() { From 8f9d15db4aa2edcec65a478ce68de27b2264a5f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Feb 2022 02:00:53 -0500 Subject: [PATCH 2/7] feat: enable form elements in web --- packages/interpreter/gen/interpreter.js | 2 ++ packages/web/src/bindings.rs | 2 ++ packages/web/src/dom.rs | 31 ++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/interpreter/gen/interpreter.js b/packages/interpreter/gen/interpreter.js index c582bdf668..62cf3dd131 100644 --- a/packages/interpreter/gen/interpreter.js +++ b/packages/interpreter/gen/interpreter.js @@ -198,6 +198,8 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": + // 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) { diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index 2b773ecd5b..fe25fcec09 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -207,6 +207,8 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": + // 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) { diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 1121549385..67acf7890e 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -215,7 +215,36 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc() { + let elements = form.elements(); + for x in 0..elements.length() { + let element = elements.item(x).unwrap(); + if let Some(name) = element.get_attribute("name") { + let value: String = (&element) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => 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()) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + values.insert(name, value); + } + } + } + Arc::new(FormData { value, values }) } "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" From 2c4e7beae80a7f36a26f2684063afa76e0bd86b6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Feb 2022 11:32:00 -0500 Subject: [PATCH 3/7] wip: update interpreter --- packages/interpreter/src/interpreter.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/interpreter/src/interpreter.ts b/packages/interpreter/src/interpreter.ts index ec48f9c26d..41036905b4 100644 --- a/packages/interpreter/src/interpreter.ts +++ b/packages/interpreter/src/interpreter.ts @@ -263,7 +263,12 @@ export class Interpreter { } // walk the tree to find the real element - while (realId == null && target.parentElement != null) { + 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`); } @@ -326,7 +331,7 @@ export class Interpreter { -function serialize_event(event: Event) { +export function serialize_event(event: Event) { switch (event.type) { case "copy": case "cut": From add21d5f9d440b0f923cafdb4393e0b587a84b43 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 3 Feb 2022 12:52:05 -0500 Subject: [PATCH 4/7] feat: update bindings and interpreter with new tsc code --- packages/interpreter/gen/interpreter.js | 8 ++++++-- packages/web/src/bindings.rs | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/interpreter/gen/interpreter.js b/packages/interpreter/gen/interpreter.js index 62cf3dd131..429b5c95a0 100644 --- a/packages/interpreter/gen/interpreter.js +++ b/packages/interpreter/gen/interpreter.js @@ -217,7 +217,11 @@ export class Interpreter { } } // walk the tree to find the real element - while (realId == null && target.parentElement != null) { + 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`); } @@ -270,7 +274,7 @@ export class Interpreter { } } } -function serialize_event(event) { +export function serialize_event(event) { switch (event.type) { case "copy": case "cut": diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index fe25fcec09..356f5b8eb1 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -226,7 +226,11 @@ export class Interpreter { } } // walk the tree to find the real element - while (realId == null && target.parentElement != null) { + 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`); } @@ -279,7 +283,7 @@ export class Interpreter { } } } -function serialize_event(event) { +export function serialize_event(event) { switch (event.type) { case "copy": case "cut": From d758dc6065a4563e7c1e94eb19466b5b4987d62e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 02:13:35 -0500 Subject: [PATCH 5/7] feat: form works in web --- examples/form.rs | 7 +++--- packages/web/src/dom.rs | 49 +++++++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/examples/form.rs b/examples/form.rs index 577658aeb8..4fa87c885b 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -1,6 +1,7 @@ -//! Example: README.md showcase +//! Forms //! -//! The example from the README.md. +//! Dioxus forms deviate slightly from html, automatically returning all named inputs +//! in the "values" field use dioxus::prelude::*; @@ -13,7 +14,7 @@ fn app(cx: Scope) -> Element { div { h1 { "Form" } form { - oninput: move |ev| println!("{:?}", ev), + oninput: move |ev| println!("{:?}", ev.values), input { r#type: "text", name: "username" } input { r#type: "text", name: "full-name" } input { r#type: "password", name: "password" } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 67acf7890e..efa21ebbf4 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -41,7 +41,7 @@ impl WebsysDom { Some(Ok(id)) => { break Ok(UserEvent { name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), + data: virtual_event_from_websys_event(event.clone(), target.clone()), element: Some(ElementId(id)), scope_id: None, priority: dioxus_core::EventPriority::Medium, @@ -57,7 +57,10 @@ impl WebsysDom { } else { break Ok(UserEvent { name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), + data: virtual_event_from_websys_event( + event.clone(), + target.clone(), + ), element: None, scope_id: None, priority: dioxus_core::EventPriority::Low, @@ -144,7 +147,10 @@ unsafe impl Sync for DioxusWebsysEvent {} // todo: some of these events are being casted to the wrong event type. // We need tests that simulate clicks/etc and make sure every event type works. -fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { +fn virtual_event_from_websys_event( + event: web_sys::Event, + target: Element, +) -> Arc { use dioxus_html::on::*; use dioxus_html::KeyCode; @@ -177,9 +183,6 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { - let evt: &web_sys::Event = event.dyn_ref().unwrap(); - - let target: web_sys::EventTarget = evt.target().unwrap(); let value: String = (&target) .dyn_ref() .map(|input: &web_sys::HtmlInputElement| { @@ -217,28 +220,29 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc() { let elements = form.elements(); for x in 0..elements.length() { let element = elements.item(x).unwrap(); if let Some(name) = element.get_attribute("name") { let value: String = (&element) - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => "true".to_string(), - false => "false".to_string(), - } - }, - _ => 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()) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => 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()) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); values.insert(name, value); } @@ -337,6 +341,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc Arc::new(MediaData {}), "toggle" => Arc::new(ToggleData {}), + _ => Arc::new(()), } } From 28e9e4373ea923917b7c29d50a5bee0e943bf21b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 18:11:58 -0500 Subject: [PATCH 6/7] fix: tweak js code --- packages/interpreter/src/interpreter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 4b2f3b7010..300d120014 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -229,12 +229,14 @@ export class Interpreter { shouldPreventDefault = target.getAttribute( `dioxus-prevent-default` ); + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { - // event.preventDefault(); + event.preventDefault(); } if (event.type == "submit") { - // event.preventDefault(); + event.preventDefault(); } if (target.tagName == "FORM") { From a939df677af9ffd3a684b1c9754fd629a70007e7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 21:02:53 -0500 Subject: [PATCH 7/7] example: use onsubmit to showcase entry --- examples/form.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/form.rs b/examples/form.rs index 4fa87c885b..5f0c992f54 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -14,10 +14,12 @@ fn app(cx: Scope) -> Element { div { h1 { "Form" } form { - oninput: move |ev| println!("{:?}", ev.values), + onsubmit: move |ev| println!("Submitted {:?}", ev.values), + oninput: move |ev| println!("Input {:?}", ev.values), input { r#type: "text", name: "username" } input { r#type: "text", name: "full-name" } input { r#type: "password", name: "password" } + button { "Submit the form" } } } })