diff --git a/examples/form.rs b/examples/form.rs index 3442d4d73..b49072292 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -12,95 +12,104 @@ fn main() { fn app() -> Element { let mut values = use_signal(|| HashMap::new()); + let mut submitted_values = use_signal(|| HashMap::new()); + rsx! { - div { - h1 { "Form" } + div { style: "display: flex", + div { style: "width: 50%", + h1 { "Form" } - // The form element is used to create an HTML form for user input - // You can attach regular attributes to it - form { - id: "cool-form", - style: "display: flex; flex-direction: column;", - - // You can attach a handler to the entire form - oninput: move |ev| { - println!("Input event: {:#?}", ev); - values.set(ev.values()); - }, - - // On desktop/liveview, the form will not navigate the page - the expectation is that you handle - // The form event. - // Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview. - // We suggest always attaching a submit handler to the form. - onsubmit: move |ev| { - println!("Submit event: {:#?}", ev); - }, - - // Regular text inputs with handlers - label { r#for: "username", "Username" } - input { - r#type: "text", - name: "username", - oninput: move |ev| { - println!("setting username"); - values.set(ev.values()); - } + if !submitted_values.read().is_empty() { + h2 { "Submitted! ✅" } } - // And then the various inputs that might exist - // Note for a value to be returned in .values(), it must be named! + // The form element is used to create an HTML form for user input + // You can attach regular attributes to it + form { + id: "cool-form", + style: "display: flex; flex-direction: column;", - label { r#for: "full-name", "Full Name" } - input { r#type: "text", name: "full-name" } - - label { r#for: "email", "Email" } - input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" } - - label { r#for: "password", "Password" } - input { r#type: "password", name: "password" } - - label { r#for: "color", "Color" } - input { r#type: "radio", checked: true, name: "color", value: "red" } - input { r#type: "radio", name: "color", value: "blue" } - input { r#type: "radio", name: "color", value: "green" } - - // Select multiple comes in as a comma separated list of selected values - // You should split them on the comma to get the values manually - label { r#for: "country", "Country" } - select { - name: "country", - multiple: true, + // You can attach a handler to the entire form oninput: move |ev| { println!("Input event: {:#?}", ev); - println!("Values: {:#?}", ev.value().split(',').collect::>()); + values.set(ev.values()); }, - option { value: "usa", "USA" } - option { value: "canada", "Canada" } - option { value: "mexico", "Mexico" } + + // On desktop/liveview, the form will not navigate the page - the expectation is that you handle + // The form event. + // Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview. + // We suggest always attaching a submit handler to the form. + onsubmit: move |ev| { + println!("Submit event: {:#?}", ev); + submitted_values.set(ev.values()); + }, + + // Regular text inputs with handlers + label { r#for: "username", "Username" } + input { + r#type: "text", + name: "username", + oninput: move |ev| { + println!("setting username"); + values.set(ev.values()); + } + } + + // And then the various inputs that might exist + // Note for a value to be returned in .values(), it must be named! + + label { r#for: "full-name", "Full Name" } + input { r#type: "text", name: "full-name" } + + label { r#for: "email", "Email" } + input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" } + + label { r#for: "password", "Password" } + input { r#type: "password", name: "password" } + + label { r#for: "color", "Color" } + input { r#type: "radio", checked: true, name: "color", value: "red" } + input { r#type: "radio", name: "color", value: "blue" } + input { r#type: "radio", name: "color", value: "green" } + + // Select multiple comes in as a comma separated list of selected values + // You should split them on the comma to get the values manually + label { r#for: "country", "Country" } + select { + name: "country", + multiple: true, + oninput: move |ev| { + println!("Input event: {:#?}", ev); + println!("Values: {:#?}", ev.value().split(',').collect::>()); + }, + option { value: "usa", "USA" } + option { value: "canada", "Canada" } + option { value: "mexico", "Mexico" } + } + + // Safari can be quirky with color inputs on mac. + // We recommend always providing a text input for color as a fallback. + label { r#for: "color", "Color" } + input { r#type: "color", value: "#000002", name: "head", id: "head" } + + // Dates! + input { + min: "2018-01-01", + value: "2018-07-22", + r#type: "date", + name: "trip-start", + max: "2025-12-31", + id: "start" + } + + // Buttons will submit your form by default. + button { r#type: "submit", value: "Submit", "Submit the form" } } - - // Safari can be quirky with color inputs on mac. - // We recommend always providing a text input for color as a fallback. - label { r#for: "color", "Color" } - input { r#type: "color", value: "#000002", name: "head", id: "head" } - - // Dates! - input { - min: "2018-01-01", - value: "2018-07-22", - r#type: "date", - name: "trip-start", - max: "2025-12-31", - id: "start" - } - - // Buttons will submit your form by default. - button { r#type: "submit", value: "Submit", "Submit the form" } } - } - div { - h1 { "Oninput Values" } - pre { "{values:#?}" } + div { style: "width: 50%", + h1 { "Oninput Values" } + pre { "{values:#?}" } + } } } } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 033d44fbd..ae3da5ba4 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,5 +1,6 @@ use crate::{assets::*, edits::EditQueue}; -use dioxus_interpreter_js::unified_bindings::native_js; +use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; +use dioxus_interpreter_js::NATIVE_JS; use std::path::{Path, PathBuf}; use wry::{ http::{status::StatusCode, Request, Response}, @@ -12,46 +13,16 @@ const EDITS_PATH: &str = "http://dioxus.index.html/edits"; #[cfg(not(any(target_os = "android", target_os = "windows")))] const EDITS_PATH: &str = "dioxus://index.html/edits"; -const PREVENT_FILE_UPLOAD: &str = include_str!("../js/prevent_file_upload.js"); +// const PREVENT_FILE_UPLOAD: &str = include_str!("../js/prevent_file_upload.js"); fn handle_edits_code() -> String { - let polling_request = format!( + format!( r#"// Poll for requests - window.interpreter = new JSChannel(); - window.interpreter.wait_for_request = (headless) => {{ - fetch(new Request("{EDITS_PATH}")) - .then(response => {{ - response.arrayBuffer() - .then(bytes => {{ - // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly - if (headless) {{ - window.interpreter.run_from_bytes(bytes); - }} - else {{ - requestAnimationFrame(() => {{ - window.interpreter.run_from_bytes(bytes); - }}); - }} - window.interpreter.wait_for_request(headless); - }}); - }}) - }}"# - ); - - let mut interpreter = native_js() - .replace("/*POST_HANDLE_EDITS*/", PREVENT_FILE_UPLOAD) - .replace("export", "") - + &polling_request; - - while let Some(import_start) = interpreter.find("import") { - let import_end = interpreter[import_start..] - .find(|c| c == ';' || c == '\n') - .map(|i| i + import_start) - .unwrap_or_else(|| interpreter.len()); - interpreter.replace_range(import_start..import_end, ""); - } - - format!("{interpreter}\nconst intercept_link_redirects = true;") + {SLEDGEHAMMER_JS} + {NATIVE_JS} + window.interpreter = new NativeInterpreter("{EDITS_PATH}"); + "# + ) } static DEFAULT_INDEX: &str = include_str!("./index.html"); diff --git a/packages/interpreter/src/js/core.js b/packages/interpreter/src/js/core.js index 8487578c1..48cc3315d 100644 --- a/packages/interpreter/src/js/core.js +++ b/packages/interpreter/src/js/core.js @@ -88,9 +88,7 @@ class BaseInterpreter { stack; templates; m; - constructor(root, handler) { - this.handler = handler; - this.initialize(root); + constructor() { } initialize(root, handler = null) { this.global = {}; diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index cba33cfe2..00ba413f1 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -7906913262979346544 \ No newline at end of file +5236313184333634830 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 012e5f1ce..376eac052 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1,210 +1,3 @@ -// src/ts/set_attribute.ts -function setAttributeInner(node, field, value, ns) { - if (ns === "style") { - node.style.setProperty(field, value); - return; - } - if (!!ns) { - node.setAttributeNS(ns, field, value); - return; - } - switch (field) { - case "value": - if (node.value !== value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - if (!truthy(value) && isBoolAttr(field)) { - node.removeAttribute(field); - } else { - node.setAttribute(field, value); - } - } -} -var truthy = function(val) { - return val === "true" || val === true; -}; -var isBoolAttr = function(field) { - switch (field) { - case "allowfullscreen": - case "allowpaymentrequest": - case "async": - case "autofocus": - case "autoplay": - case "checked": - case "controls": - case "default": - case "defer": - case "disabled": - case "formnovalidate": - case "hidden": - case "ismap": - case "itemscope": - case "loop": - case "multiple": - case "muted": - case "nomodule": - case "novalidate": - case "open": - case "playsinline": - case "readonly": - case "required": - case "reversed": - case "selected": - case "truespeed": - case "webkitdirectory": - return true; - default: - return false; - } -}; - -// src/ts/core.ts -class BaseInterpreter { - global; - local; - root; - handler; - nodes; - stack; - templates; - m; - constructor(root, handler) { - this.handler = handler; - this.initialize(root); - } - initialize(root, handler = null) { - this.global = {}; - this.local = {}; - this.root = root; - this.nodes = [root]; - this.stack = [root]; - this.templates = {}; - if (handler) { - this.handler = handler; - } - } - createListener(event_name, element, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = { active: 1, callback: this.handler }; - this.root.addEventListener(event_name, this.handler); - } else { - this.global[event_name].active++; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - } - removeListener(element, event_name, bubbles) { - if (bubbles) { - this.removeBubblingListener(event_name); - } else { - this.removeNonBubblingListener(element, event_name); - } - } - removeBubblingListener(event_name) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - removeNonBubblingListener(element, event_name) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (Object.keys(this.local[id]).length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - } - removeAllNonBubblingListeners(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } - getNode(id) { - return this.nodes[id]; - } - appendChildren(id, many) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - for (let k = 0;k < many; k++) { - root.appendChild(els[k]); - } - } - loadChild(ptr, len) { - let node = this.stack[this.stack.length - 1]; - let ptr_end = ptr + len; - for (;ptr < ptr_end; ptr++) { - let end = this.m.getUint8(ptr); - for (node = node.firstChild;end > 0; end--) { - node = node.nextSibling; - } - } - return node; - } - saveTemplate(nodes, tmpl_id) { - this.templates[tmpl_id] = nodes; - } - hydrateRoot(ids) { - const hydrateNodes = document.querySelectorAll("[data-node-hydration]"); - for (let i = 0;i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i]; - const hydration = hydrateNode.getAttribute("data-node-hydration"); - const split = hydration.split(","); - const id = ids[parseInt(split[0])]; - this.nodes[id] = hydrateNode; - if (split.length > 1) { - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute("data-dioxus-id", id.toString()); - for (let j = 1;j < split.length; j++) { - const listener = split[j]; - const split2 = listener.split(":"); - const event_name = split2[0]; - const bubbles = split2[1] === "1"; - this.createListener(event_name, hydrateNode, bubbles); - } - } - } - const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT); - let currentNode = treeWalker.nextNode(); - while (currentNode) { - const id = currentNode.textContent; - const split = id.split("node-id"); - if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - currentNode = treeWalker.nextNode(); - } - } - setAttributeInner(node, field, value, ns) { - setAttributeInner(node, field, value, ns); - } -} - // src/ts/form.ts function retriveValues(event, target) { let contents = { @@ -438,13 +231,21 @@ var targetId = function(target) { } return parseInt(realId); }; +var JSChannel_; +if (RawInterpreter) { + JSChannel_ = RawInterpreter; +} -class PlatformInterpreter extends BaseInterpreter { +class NativeInterpreter extends JSChannel_ { intercept_link_redirects; ipc; + editsPath; liveview; - constructor(root) { - super(root, (event) => this.handleEvent(event, event.type, true)); + constructor(editsPath) { + super(); + this.editsPath = editsPath; + } + initialize(root) { this.intercept_link_redirects = true; this.liveview = false; window.addEventListener("dragover", function(e) { @@ -458,13 +259,12 @@ class PlatformInterpreter extends BaseInterpreter { } }, false); this.ipc = window.ipc; + const handler = (event) => this.handleEvent(event, event.type, true); + super.initialize(root, handler); } serializeIpcMessage(method, params = {}) { return JSON.stringify({ method, params }); } - setAttributeInner(node, field, value, ns) { - setAttributeInner(node, field, value, ns); - } scrollTo(id, behavior) { const node = this.nodes[id]; if (node instanceof HTMLElement) { @@ -492,7 +292,7 @@ class PlatformInterpreter extends BaseInterpreter { } } } - LoadChild(array) { + loadChild(array) { let node = this.stack[this.stack.length - 1]; for (let i = 0;i < array.length; i++) { let end = array[i]; @@ -502,7 +302,7 @@ class PlatformInterpreter extends BaseInterpreter { } return node; } - AppendChildren(id, many) { + appendChildren(id, many) { const root = this.nodes[id]; const els = this.stack.splice(this.stack.length - many); for (let k = 0;k < many; k++) { @@ -510,6 +310,7 @@ class PlatformInterpreter extends BaseInterpreter { } } handleEvent(event, name, bubbles) { + console.log("handling event: ", event); const target = event.target; const realId = targetId(target); const contents = serializeEvent(event, target); @@ -529,6 +330,7 @@ class PlatformInterpreter extends BaseInterpreter { } else { const message = this.serializeIpcMessage("user_event", body); this.ipc.postMessage(message); + console.log("sent message to host: ", message); } } async readFiles(target, contents, bubbles, realId, name) { @@ -584,7 +386,17 @@ class PlatformInterpreter extends BaseInterpreter { } } } + wait_for_request(headless) { + fetch(new Request(this.editsPath)).then((response) => response.arrayBuffer()).then((bytes) => { + if (headless) { + this.run_from_bytes(bytes); + } else { + requestAnimationFrame(() => this.run_from_bytes(bytes)); + } + this.wait_for_request(headless); + }); + } } export { - PlatformInterpreter + NativeInterpreter }; diff --git a/packages/interpreter/src/ts/core.ts b/packages/interpreter/src/ts/core.ts index f5e1cd94a..6f78e9ce3 100644 --- a/packages/interpreter/src/ts/core.ts +++ b/packages/interpreter/src/ts/core.ts @@ -28,10 +28,7 @@ export class BaseInterpreter { // sledgehammer is generating this... m: any; - constructor(root: HTMLElement, handler: EventListener) { - this.handler = handler; - this.initialize(root); - } + constructor() { } initialize(root: HTMLElement, handler: EventListener | null = null) { this.global = {}; diff --git a/packages/interpreter/src/ts/native.ts b/packages/interpreter/src/ts/native.ts index ddce3239a..13bb152e2 100644 --- a/packages/interpreter/src/ts/native.ts +++ b/packages/interpreter/src/ts/native.ts @@ -5,18 +5,31 @@ import { BaseInterpreter, NodeId } from "./core"; import { SerializedEvent, serializeEvent } from "./serialize"; -import { setAttributeInner } from "./set_attribute"; -export class PlatformInterpreter extends BaseInterpreter { +// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope +// we want to extend it, and it technically extends base intepreter. To make typescript happy, +// we're going to bind the JSChannel_ object to the JSChannel object, and then extend it +var JSChannel_: typeof BaseInterpreter; + +// @ts-ignore - this is coming from the host +if (RawInterpreter) { JSChannel_ = RawInterpreter; } + +export class NativeInterpreter extends JSChannel_ { intercept_link_redirects: boolean; ipc: any; + editsPath: string; // eventually we want to remove liveview and build it into the server-side-events of fullstack // however, for now we need to support it since SSE in fullstack doesn't exist yet liveview: boolean; - constructor(root: HTMLElement) { - super(root, (event) => this.handleEvent(event, event.type, true)); + constructor(editsPath: string) { + super(); + this.editsPath = editsPath; + } + + initialize(root: HTMLElement): void { + this.intercept_link_redirects = true; this.liveview = false; @@ -39,16 +52,16 @@ export class PlatformInterpreter extends BaseInterpreter { // @ts-ignore - wry gives us this this.ipc = window.ipc; + + // make sure we pass the handler to the base interpreter + const handler: EventListener = (event) => this.handleEvent(event, event.type, true); + super.initialize(root, handler); } serializeIpcMessage(method: string, params = {}) { return JSON.stringify({ method, params }); } - setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { - setAttributeInner(node, field, value, ns); - } - scrollTo(id: NodeId, behavior: ScrollBehavior) { const node = this.nodes[id]; if (node instanceof HTMLElement) { @@ -80,7 +93,9 @@ export class PlatformInterpreter extends BaseInterpreter { } } - LoadChild(array: number[]) { + // ignore the fact the base interpreter uses ptr + len but we use array + // @ts-ignore + loadChild(array: number[]) { // iterate through each number and get that child let node = this.stack[this.stack.length - 1]; @@ -94,7 +109,7 @@ export class PlatformInterpreter extends BaseInterpreter { return node; } - AppendChildren(id: NodeId, many: number) { + appendChildren(id: NodeId, many: number) { const root = this.nodes[id]; const els = this.stack.splice(this.stack.length - many); @@ -104,6 +119,8 @@ export class PlatformInterpreter extends BaseInterpreter { } handleEvent(event: Event, name: string, bubbles: boolean) { + console.log("handling event: ", event); + const target = event.target!; const realId = targetId(target)!; const contents = serializeEvent(event, target); @@ -137,6 +154,8 @@ export class PlatformInterpreter extends BaseInterpreter { const message = this.serializeIpcMessage("user_event", body); this.ipc.postMessage(message); + console.log("sent message to host: ", message); + // // Run the event handler on the virtualdom // // capture/prevent default of the event if the virtualdom wants to // const res = handleVirtualdomEventSync(JSON.stringify(body)); @@ -243,6 +262,22 @@ export class PlatformInterpreter extends BaseInterpreter { } } } + + wait_for_request(headless: boolean) { + fetch(new Request(this.editsPath)) + .then(response => response.arrayBuffer()) + .then(bytes => { + // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly + if (headless) { + // @ts-ignore + this.run_from_bytes(bytes); + } else { + // @ts-ignore + requestAnimationFrame(() => this.run_from_bytes(bytes)); + } + this.wait_for_request(headless); + }); + } } type EventSyncResult = {