Explorar el Código

Fix #2043: use formvalue instead of String for forms (#2103)

Fix #2043: use formvalue instead of String for forms
Jonathan Kelley hace 1 año
padre
commit
2dc6cecf2e

+ 22 - 1
examples/form.rs

@@ -60,8 +60,9 @@ fn app() -> Element {
 
 
                     label { r#for: "full-name", "Full Name" }
                     label { r#for: "full-name", "Full Name" }
                     input { r#type: "text", name: "full-name" }
                     input { r#type: "text", name: "full-name" }
+                    input { r#type: "text", name: "full-name" }
 
 
-                    label { r#for: "email", "Email" }
+                    label { r#for: "email", "Email (matching <name>@example.com)" }
                     input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" }
                     input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" }
 
 
                     label { r#for: "password", "Password" }
                     label { r#for: "password", "Password" }
@@ -102,6 +103,26 @@ fn app() -> Element {
                         id: "start"
                         id: "start"
                     }
                     }
 
 
+                    // CHekcboxes
+                    label { r#for: "cbox", "Color" }
+                    div {
+                        label { r#for: "cbox-red", "red" }
+                        input { r#type: "checkbox", checked: true, name: "cbox", value: "red", id: "cbox-red" }
+                    }
+                    div {
+                        label { r#for: "cbox-blue", "blue" }
+                        input { r#type: "checkbox", name: "cbox", value: "blue", id: "cbox-blue" }
+                    }
+                    div {
+                        label { r#for: "cbox-green", "green" }
+                        input { r#type: "checkbox", name: "cbox", value: "green", id: "cbox-green" }
+                    }
+                    div {
+                        label { r#for: "cbox-yellow", "yellow" }
+                        input { r#type: "checkbox", name: "cbox", value: "yellow", id: "cbox-yellow" }
+                    }
+
+
                     // Buttons will submit your form by default.
                     // Buttons will submit your form by default.
                     button { r#type: "submit", value: "Submit", "Submit the form" }
                     button { r#type: "submit", value: "Submit", "Submit the form" }
                 }
                 }

+ 1 - 0
packages/core/src/scopes.rs

@@ -13,6 +13,7 @@ use std::{cell::Ref, rc::Rc};
 pub struct ScopeId(pub usize);
 pub struct ScopeId(pub usize);
 
 
 impl std::fmt::Debug for ScopeId {
 impl std::fmt::Debug for ScopeId {
+    #[allow(unused_mut)]
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let mut builder = f.debug_tuple("ScopeId");
         let mut builder = f.debug_tuple("ScopeId");
         let mut builder = builder.field(&self.0);
         let mut builder = builder.field(&self.0);

+ 9 - 9
packages/desktop/headless_tests/events.rs

@@ -1,4 +1,4 @@
-use std::{collections::HashMap, ops::Deref};
+use std::collections::HashMap;
 
 
 use dioxus::html::geometry::euclid::Vector3D;
 use dioxus::html::geometry::euclid::Vector3D;
 use dioxus::prelude::*;
 use dioxus::prelude::*;
@@ -453,10 +453,10 @@ fn test_form_input() -> Element {
 
 
         // And then the value the form gives us should also match
         // And then the value the form gives us should also match
         values.with_mut(|x| {
         values.with_mut(|x| {
-            assert_eq!(x.get("username").unwrap().deref(), "hello");
-            assert_eq!(x.get("full-name").unwrap().deref(), "lorem");
-            assert_eq!(x.get("password").unwrap().deref(), "ipsum");
-            assert_eq!(x.get("color").unwrap().deref(), "red");
+            assert_eq!(x.get("username").unwrap(), "hello");
+            assert_eq!(x.get("full-name").unwrap(), "lorem");
+            assert_eq!(x.get("password").unwrap(), "ipsum");
+            assert_eq!(x.get("color").unwrap(), "red");
         });
         });
         RECEIVED_EVENTS.with_mut(|x| *x += 1);
         RECEIVED_EVENTS.with_mut(|x| *x += 1);
     };
     };
@@ -500,10 +500,10 @@ fn test_form_submit() -> Element {
     let set_values = move |ev: FormEvent| {
     let set_values = move |ev: FormEvent| {
         values.set(ev.values());
         values.set(ev.values());
         values.with_mut(|x| {
         values.with_mut(|x| {
-            assert_eq!(x.get("username").unwrap().deref(), "goodbye");
-            assert_eq!(x.get("full-name").unwrap().deref(), "lorem");
-            assert_eq!(x.get("password").unwrap().deref(), "ipsum");
-            assert_eq!(x.get("color").unwrap().deref(), "red");
+            assert_eq!(x.get("username").unwrap(), "goodbye");
+            assert_eq!(x.get("full-name").unwrap(), "lorem");
+            assert_eq!(x.get("password").unwrap(), "ipsum");
+            assert_eq!(x.get("color").unwrap(), "red");
         });
         });
         RECEIVED_EVENTS.with_mut(|x| *x += 1);
         RECEIVED_EVENTS.with_mut(|x| *x += 1);
     };
     };

+ 42 - 6
packages/html/src/events/form.rs

@@ -1,11 +1,47 @@
 use crate::file_data::FileEngine;
 use crate::file_data::FileEngine;
 use crate::file_data::HasFileData;
 use crate::file_data::HasFileData;
-use std::{collections::HashMap, fmt::Debug};
+use std::{collections::HashMap, fmt::Debug, ops::Deref};
 
 
 use dioxus_core::Event;
 use dioxus_core::Event;
 
 
 pub type FormEvent = Event<FormData>;
 pub type FormEvent = Event<FormData>;
 
 
+/// A form value that may either be a list of values or a single value
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Debug, Default, Clone, PartialEq)]
+pub struct FormValue(pub Vec<String>);
+
+impl Deref for FormValue {
+    type Target = [String];
+
+    fn deref(&self) -> &Self::Target {
+        self.as_slice()
+    }
+}
+
+impl FormValue {
+    /// Convenient way to represent Value as slice
+    pub fn as_slice(&self) -> &[String] {
+        &self.0
+    }
+
+    /// Return the first value, panicking if there are none
+    pub fn as_value(&self) -> String {
+        self.0.first().unwrap().clone()
+    }
+
+    /// Convert into Vec<String>
+    pub fn to_vec(self) -> Vec<String> {
+        self.0.clone()
+    }
+}
+
+impl PartialEq<str> for FormValue {
+    fn eq(&self, other: &str) -> bool {
+        self.0.len() == 1 && self.0.first().map(|s| s.as_str()) == Some(other)
+    }
+}
+
 /* DOMEvent:  Send + SyncTarget relatedTarget */
 /* DOMEvent:  Send + SyncTarget relatedTarget */
 pub struct FormData {
 pub struct FormData {
     inner: Box<dyn HasFormData>,
     inner: Box<dyn HasFormData>,
@@ -65,7 +101,7 @@ impl FormData {
     /// Collect all the named form values from the containing form.
     /// Collect all the named form values from the containing form.
     ///
     ///
     /// Every input must be named!
     /// Every input must be named!
-    pub fn values(&self) -> HashMap<String, String> {
+    pub fn values(&self) -> HashMap<String, FormValue> {
         self.inner.values()
         self.inner.values()
     }
     }
 
 
@@ -95,7 +131,7 @@ pub trait HasFormData: HasFileData + std::any::Any {
         true
         true
     }
     }
 
 
-    fn values(&self) -> HashMap<String, String> {
+    fn values(&self) -> HashMap<String, FormValue> {
         Default::default()
         Default::default()
     }
     }
 
 
@@ -135,7 +171,7 @@ pub struct SerializedFormData {
     value: String,
     value: String,
 
 
     #[serde(default)]
     #[serde(default)]
-    values: HashMap<String, String>,
+    values: HashMap<String, FormValue>,
 
 
     #[serde(default)]
     #[serde(default)]
     valid: bool,
     valid: bool,
@@ -149,7 +185,7 @@ impl SerializedFormData {
     /// Create a new serialized form data object
     /// Create a new serialized form data object
     pub fn new(
     pub fn new(
         value: String,
         value: String,
-        values: HashMap<String, String>,
+        values: HashMap<String, FormValue>,
         files: Option<crate::file_data::SerializedFileEngine>,
         files: Option<crate::file_data::SerializedFileEngine>,
     ) -> Self {
     ) -> Self {
         Self {
         Self {
@@ -200,7 +236,7 @@ impl HasFormData for SerializedFormData {
         self.value.clone()
         self.value.clone()
     }
     }
 
 
-    fn values(&self) -> HashMap<String, String> {
+    fn values(&self) -> HashMap<String, FormValue> {
         self.values.clone()
         self.values.clone()
     }
     }
 
 

+ 5 - 1
packages/interpreter/build.rs

@@ -2,7 +2,11 @@ use std::process::Command;
 
 
 fn main() {
 fn main() {
     // If any TS changes, re-run the build script
     // If any TS changes, re-run the build script
-    println!("cargo:rerun-if-changed=src/ts/*.ts");
+    println!("cargo:rerun-if-changed=src/ts/form.ts");
+    println!("cargo:rerun-if-changed=src/ts/core.ts");
+    println!("cargo:rerun-if-changed=src/ts/serialize.ts");
+    println!("cargo:rerun-if-changed=src/ts/set_attribute.ts");
+    println!("cargo:rerun-if-changed=src/ts/common.ts");
 
 
     // Compute the hash of the ts files
     // Compute the hash of the ts files
     let hash = hash_ts_files();
     let hash = hash_ts_files();

+ 1 - 1
packages/interpreter/src/js/common.js

@@ -1 +1 @@
-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===!0},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!0;default:return!1}};function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key]+=","+value;else contents[key]=value}),{valid:form.checkValidity(),values:contents}}export{setAttributeInner,retrieveFormValues};
+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===!0},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!0;default:return!1}};function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}export{setAttributeInner,retrieveFormValues};

+ 1 - 1
packages/interpreter/src/js/native.js

@@ -1 +1 @@
-function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key]+=","+value;else contents[key]=value}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files:{files:{a:[1,2,3]}}}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;liveview;constructor(editsPath){super();this.editsPath=editsPath}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message)}event.preventDefault()}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){const target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target);let body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event,target),this.liveview){if(target instanceof HTMLInputElement&&(event.type==="change"||event.type==="input")){if(target.getAttribute("type")==="file")this.readFiles(target,contents,bubbles,realId,name)}}else{const message=this.serializeIpcMessage("user_event",body);this.ipc.postMessage(message)}}preventDefaults(event,target){let preventDefaultRequests=null;if(target instanceof Element)preventDefaultRequests=target.getAttribute("dioxus-prevent-default");if(preventDefaultRequests&&preventDefaultRequests.includes(`on${event.type}`))event.preventDefault();if(event.type==="submit")event.preventDefault();if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target,preventDefaultRequests)}handleClickNavigate(event,target,preventDefaultRequests){if(!this.intercept_link_redirects)return;if(target.tagName==="BUTTON"&&event.type=="submit")event.preventDefault();let a_element=target.closest("a");if(a_element==null)return;event.preventDefault();let elementShouldPreventDefault=preventDefaultRequests&&preventDefaultRequests.includes("onclick"),aElementShouldPreventDefault=a_element.getAttribute("dioxus-prevent-default"),linkShouldPreventDefault=aElementShouldPreventDefault&&aElementShouldPreventDefault.includes("onclick");if(!elementShouldPreventDefault&&!linkShouldPreventDefault){const href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}waitForRequest(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.waitForRequest(headless)})}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){const file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};const message=this.serializeIpcMessage("user_event",{name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}export{NativeInterpreter};
+function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files:{files:{a:[1,2,3]}}}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;liveview;constructor(editsPath){super();this.editsPath=editsPath}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message)}event.preventDefault()}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){const target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target);let body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event,target),this.liveview){if(target instanceof HTMLInputElement&&(event.type==="change"||event.type==="input")){if(target.getAttribute("type")==="file")this.readFiles(target,contents,bubbles,realId,name)}}else{const message=this.serializeIpcMessage("user_event",body);this.ipc.postMessage(message)}}preventDefaults(event,target){let preventDefaultRequests=null;if(target instanceof Element)preventDefaultRequests=target.getAttribute("dioxus-prevent-default");if(preventDefaultRequests&&preventDefaultRequests.includes(`on${event.type}`))event.preventDefault();if(event.type==="submit")event.preventDefault();if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target,preventDefaultRequests)}handleClickNavigate(event,target,preventDefaultRequests){if(!this.intercept_link_redirects)return;if(target.tagName==="BUTTON"&&event.type=="submit")event.preventDefault();let a_element=target.closest("a");if(a_element==null)return;event.preventDefault();let elementShouldPreventDefault=preventDefaultRequests&&preventDefaultRequests.includes("onclick"),aElementShouldPreventDefault=a_element.getAttribute("dioxus-prevent-default"),linkShouldPreventDefault=aElementShouldPreventDefault&&aElementShouldPreventDefault.includes("onclick");if(!elementShouldPreventDefault&&!linkShouldPreventDefault){const href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}waitForRequest(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.waitForRequest(headless)})}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){const file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};const message=this.serializeIpcMessage("user_event",{name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}export{NativeInterpreter};

+ 4 - 4
packages/interpreter/src/ts/form.ts

@@ -2,7 +2,7 @@
 
 
 type FormValues = {
 type FormValues = {
   valid?: boolean;
   valid?: boolean;
-  values: { [key: string]: FormDataEntryValue };
+  values: { [key: string]: FormDataEntryValue[] };
 }
 }
 
 
 export function retriveValues(event: Event, target: HTMLElement): FormValues {
 export function retriveValues(event: Event, target: HTMLElement): FormValues {
@@ -33,12 +33,12 @@ export function retriveValues(event: Event, target: HTMLElement): FormValues {
 // We encode select multiple as a comma separated list which breaks... when there's commas in the values
 // We encode select multiple as a comma separated list which breaks... when there's commas in the values
 export function retrieveFormValues(form: HTMLFormElement): FormValues {
 export function retrieveFormValues(form: HTMLFormElement): FormValues {
   const formData = new FormData(form);
   const formData = new FormData(form);
-  const contents: { [key: string]: FormDataEntryValue } = {};
+  const contents: { [key: string]: FormDataEntryValue[] } = {};
   formData.forEach((value, key) => {
   formData.forEach((value, key) => {
     if (contents[key]) {
     if (contents[key]) {
-      contents[key] += "," + value;
+      contents[key].push(value);
     } else {
     } else {
-      contents[key] = value;
+      contents[key] = [value];
     }
     }
   });
   });
   return {
   return {

+ 1 - 1
packages/interpreter/src/ts/serialize.ts

@@ -5,7 +5,7 @@ import { retriveSelectValue, retriveValues } from "./form";
 export type AppTouchEvent = TouchEvent;
 export type AppTouchEvent = TouchEvent;
 
 
 export type SerializedEvent = {
 export type SerializedEvent = {
-  values?: { [key: string]: FormDataEntryValue };
+  values?: { [key: string]: FormDataEntryValue[] };
   value?: string;
   value?: string;
   [key: string]: any;
   [key: string]: any;
 };
 };

+ 8 - 3
packages/rsx/src/hot_reload/hot_reloading_file_map.rs

@@ -9,7 +9,7 @@ pub use proc_macro2::TokenStream;
 pub use std::collections::HashMap;
 pub use std::collections::HashMap;
 pub use std::sync::Mutex;
 pub use std::sync::Mutex;
 pub use std::time::SystemTime;
 pub use std::time::SystemTime;
-use std::{collections::HashSet, path::PathBuf};
+use std::{collections::HashSet, ffi::OsStr, path::PathBuf};
 pub use std::{fs, io, path::Path};
 pub use std::{fs, io, path::Path};
 pub use std::{fs::File, io::Read};
 pub use std::{fs::File, io::Read};
 pub use syn::__private::ToTokens;
 pub use syn::__private::ToTokens;
@@ -55,8 +55,14 @@ pub struct CachedSynFile {
 
 
 impl<Ctx: HotReloadingContext> FileMap<Ctx> {
 impl<Ctx: HotReloadingContext> FileMap<Ctx> {
     /// Create a new FileMap from a crate directory
     /// Create a new FileMap from a crate directory
+    ///
+    /// TODO: this should be created with a gitignore filter
     pub fn create(path: PathBuf) -> io::Result<FileMapBuildResult<Ctx>> {
     pub fn create(path: PathBuf) -> io::Result<FileMapBuildResult<Ctx>> {
-        Self::create_with_filter(path, |_| false)
+        Self::create_with_filter(path, |p| {
+            // skip some stuff we know is large by default
+            p.file_name() == Some(OsStr::new("target"))
+                || p.file_name() == Some(OsStr::new("node_modules"))
+        })
     }
     }
 
 
     /// Create a new FileMap from a crate directory
     /// Create a new FileMap from a crate directory
@@ -352,7 +358,6 @@ fn find_rs_files(root: PathBuf, filter: &mut impl FnMut(&Path) -> bool) -> FileM
                     };
                     };
 
 
                     // track assets while we're here
                     // track assets while we're here
-
                     files.insert(root, cached_file);
                     files.insert(root, cached_file);
                 }
                 }
                 Err(err) => {
                 Err(err) => {

+ 6 - 10
packages/web/src/event.rs

@@ -4,7 +4,7 @@ use dioxus_html::{
     point_interaction::{
     point_interaction::{
         InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
         InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
     },
     },
-    DragData, FileEngine, FormData, HasDragData, HasFileData, HasFormData, HasImageData,
+    DragData, FileEngine, FormData, FormValue, HasDragData, HasFileData, HasFormData, HasImageData,
     HasMouseData, HtmlEventConverter, ImageData, MountedData, PlatformEventData, ScrollData,
     HasMouseData, HtmlEventConverter, ImageData, MountedData, PlatformEventData, ScrollData,
 };
 };
 use js_sys::Array;
 use js_sys::Array;
@@ -384,15 +384,11 @@ impl HasFormData for WebFormData {
         .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
         .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
     }
     }
 
 
-    fn values(&self) -> HashMap<String, String> {
+    fn values(&self) -> HashMap<String, FormValue> {
         let mut values = HashMap::new();
         let mut values = HashMap::new();
 
 
-        fn insert_value(map: &mut HashMap<String, String>, key: String, new_value: String) {
-            if let Some(value) = map.get(&key) {
-                map.insert(key, format!("{},{}", value, new_value));
-            } else {
-                map.insert(key, new_value);
-            }
+        fn insert_value(map: &mut HashMap<String, FormValue>, key: String, new_value: String) {
+            map.entry(key.clone()).or_default().0.push(new_value);
         }
         }
 
 
         // try to fill in form values
         // try to fill in form values
@@ -414,8 +410,8 @@ impl HasFormData for WebFormData {
             }
             }
         } else if let Some(select) = self.element.dyn_ref::<web_sys::HtmlSelectElement>() {
         } else if let Some(select) = self.element.dyn_ref::<web_sys::HtmlSelectElement>() {
             // try to fill in select element values
             // try to fill in select element values
-            let options = get_select_data(select).join(",");
-            values.insert("options".to_string(), options);
+            let options = get_select_data(select);
+            values.insert("options".to_string(), FormValue(options));
         }
         }
 
 
         values
         values