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

Fix #2043: use formvalue instead of String for forms
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-03-18 18:21:08 -07:00
1 parent 468d2b6b3b
commit 2dc6cecf2e
11 files changed
+100 -37

No files matched your search

+22 -1
View File
@@ -60,8 +60,9 @@ fn app() -> Element {
label { r#for: "full-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" }
label { r#for: "password", "Password" }
@@ -102,6 +103,26 @@ fn app() -> Element {
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.
button { r#type: "submit", value: "Submit", "Submit the form" }
}
+1
View File
@@ -13,6 +13,7 @@ use std::{cell::Ref, rc::Rc};
pub struct ScopeId(pub usize);
impl std::fmt::Debug for ScopeId {
#[allow(unused_mut)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut builder = f.debug_tuple("ScopeId");
let mut builder = builder.field(&self.0);
+9 -9
View File
@@ -1,4 +1,4 @@
use std::{collections::HashMap, ops::Deref};
use std::collections::HashMap;
use dioxus::html::geometry::euclid::Vector3D;
use dioxus::prelude::*;
@@ -453,10 +453,10 @@ fn test_form_input() -> Element {
// And then the value the form gives us should also match
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);
};
@@ -500,10 +500,10 @@ fn test_form_submit() -> Element {
let set_values = move |ev: FormEvent| {
values.set(ev.values());
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);
};
+42 -6
View File
@@ -1,11 +1,47 @@
use crate::file_data::FileEngine;
use crate::file_data::HasFileData;
use std::{collections::HashMap, fmt::Debug};
use std::{collections::HashMap, fmt::Debug, ops::Deref};
use dioxus_core::Event;
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 */
pub struct FormData {
inner: Box<dyn HasFormData>,
@@ -65,7 +101,7 @@ impl FormData {
/// Collect all the named form values from the containing form.
///
/// Every input must be named!
pub fn values(&self) -> HashMap<String, String> {
pub fn values(&self) -> HashMap<String, FormValue> {
self.inner.values()
}
@@ -95,7 +131,7 @@ pub trait HasFormData: HasFileData + std::any::Any {
true
}
fn values(&self) -> HashMap<String, String> {
fn values(&self) -> HashMap<String, FormValue> {
Default::default()
}
@@ -135,7 +171,7 @@ pub struct SerializedFormData {
value: String,
#[serde(default)]
values: HashMap<String, String>,
values: HashMap<String, FormValue>,
#[serde(default)]
valid: bool,
@@ -149,7 +185,7 @@ impl SerializedFormData {
/// Create a new serialized form data object
pub fn new(
value: String,
values: HashMap<String, String>,
values: HashMap<String, FormValue>,
files: Option<crate::file_data::SerializedFileEngine>,
) -> Self {
Self {
@@ -200,7 +236,7 @@ impl HasFormData for SerializedFormData {
self.value.clone()
}
fn values(&self) -> HashMap<String, String> {
fn values(&self) -> HashMap<String, FormValue> {
self.values.clone()
}
+5 -1
View File
@@ -2,7 +2,11 @@ use std::process::Command;
fn main() {
// 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
let hash = hash_ts_files();
+1 -1
View File
@@ -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
View File
@@ -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}inLine truncated
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=editsPathLine truncated
+4 -4
View File
@@ -2,7 +2,7 @@
type FormValues = {
valid?: boolean;
values: { [key: string]: FormDataEntryValue };
values: { [key: string]: FormDataEntryValue[] };
}
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
export function retrieveFormValues(form: HTMLFormElement): FormValues {
const formData = new FormData(form);
const contents: { [key: string]: FormDataEntryValue } = {};
const contents: { [key: string]: FormDataEntryValue[] } = {};
formData.forEach((value, key) => {
if (contents[key]) {
contents[key] += "," + value;
contents[key].push(value);
} else {
contents[key] = value;
contents[key] = [value];
}
});
return {
+1 -1
View File
@@ -5,7 +5,7 @@ import { retriveSelectValue, retriveValues } from "./form";
export type AppTouchEvent = TouchEvent;
export type SerializedEvent = {
values?: { [key: string]: FormDataEntryValue };
values?: { [key: string]: FormDataEntryValue[] };
value?: string;
[key: string]: any;
};
@@ -9,7 +9,7 @@ pub use proc_macro2::TokenStream;
pub use std::collections::HashMap;
pub use std::sync::Mutex;
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::File, io::Read};
pub use syn::__private::ToTokens;
@@ -55,8 +55,14 @@ pub struct CachedSynFile {
impl<Ctx: HotReloadingContext> FileMap<Ctx> {
/// 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>> {
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
@@ -352,7 +358,6 @@ fn find_rs_files(root: PathBuf, filter: &mut impl FnMut(&Path) -> bool) -> FileM
};
// track assets while we're here
files.insert(root, cached_file);
}
Err(err) => {
+6 -10
View File
@@ -4,7 +4,7 @@ use dioxus_html::{
point_interaction::{
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,
};
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")
}
fn values(&self) -> HashMap<String, String> {
fn values(&self) -> HashMap<String, FormValue> {
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
@@ -414,8 +410,8 @@ impl HasFormData for WebFormData {
}
} else if let Some(select) = self.element.dyn_ref::<web_sys::HtmlSelectElement>() {
// 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