diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 1c5561cc7..d3efc839b 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -24,7 +24,7 @@ serde-value = "0.7.0" tokio = { workspace = true, features = ["fs", "io-util"], optional = true } rfd = { version = "0.12", optional = true } async-channel = "1.8.0" -serde_json = { version = "1" } +serde_json = { version = "1", optional = true } [dependencies.web-sys] optional = true diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index bc052bbe2..ee69104b9 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -1,4 +1,4 @@ -use std::{any::Any, collections::HashMap, fmt::Debug}; +use std::{any::Any, collections::HashMap, fmt::Debug, sync::Arc}; use dioxus_core::Event; use serde::{Deserialize, Serialize}; @@ -14,20 +14,13 @@ pub enum FormValue { } /* DOMEvent: Send + SyncTarget relatedTarget */ -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone)] pub struct FormData { inner: Box, - pub values: HashMap, } impl From for FormData { fn from(e: E) -> Self { - // todo: fix this? - Self { - inner: Box::new(e), - values: Default::default(), - } + Self { inner: Box::new(e) } } } @@ -51,7 +44,6 @@ impl FormData { pub fn new(event: impl HasFormData + 'static) -> Self { Self { inner: Box::new(event), - values: Default::default(), } } @@ -61,7 +53,7 @@ impl FormData { } /// Get the values of the form event - pub fn values(&self) -> HashMap> { + pub fn values(&self) -> HashMap { self.inner.values() } @@ -82,7 +74,7 @@ pub trait HasFormData: std::any::Any { Default::default() } - fn values(&self) -> HashMap> { + fn values(&self) -> HashMap { Default::default() } @@ -109,7 +101,7 @@ impl FormData { T: serde::de::DeserializeOwned, { let parsed_json = - convert_hashmap_to_json(&self.values).expect("Failed to parse values to JSON"); + convert_hashmap_to_json(&self.values()).expect("Failed to parse values to JSON"); serde_json::from_str(&parsed_json) } @@ -120,8 +112,8 @@ impl FormData { #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] pub struct SerializedFormData { value: String, - values: HashMap>, - files: Option>, + values: HashMap, + files: Option, } #[cfg(feature = "serialize")] @@ -129,8 +121,8 @@ impl SerializedFormData { /// Create a new serialized form data object pub fn new( value: String, - values: HashMap>, - files: Option>, + values: HashMap, + files: Option, ) -> Self { Self { value, @@ -153,9 +145,9 @@ impl SerializedFormData { resolved_files.insert(file, bytes.unwrap_or_default()); } - Some(std::sync::Arc::new(SerializedFileEngine { + Some(SerializedFileEngine { files: resolved_files, - })) + }) } None => None, }, @@ -177,14 +169,14 @@ impl HasFormData for SerializedFormData { self.value.clone() } - fn values(&self) -> HashMap> { + fn values(&self) -> HashMap { self.values.clone() } fn files(&self) -> Option> { self.files .as_ref() - .map(|files| std::sync::Arc::clone(files) as std::sync::Arc) + .map(|files| Arc::new(files.clone()) as _) } fn as_any(&self) -> &dyn std::any::Any { @@ -240,36 +232,6 @@ impl FileEngine for SerializedFileEngine { } } -#[cfg(feature = "serialize")] -fn deserialize_file_engine<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: serde::Deserializer<'de>, -{ - let Ok(file_engine) = SerializedFileEngine::deserialize(deserializer) else { - return Ok(None); - }; - - let file_engine = std::sync::Arc::new(file_engine); - Ok(Some(file_engine)) -} - -impl PartialEq for FormData { - fn eq(&self, other: &Self) -> bool { - self.value == other.value && self.values == other.values - } -} - -impl Debug for FormData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FormEvent") - .field("value", &self.value) - .field("values", &self.values) - .finish() - } -} - #[async_trait::async_trait(?Send)] pub trait FileEngine { // get a list of file names diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index 2b094c768..012c0c1eb 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -18,8 +18,8 @@ use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; use dioxus_html::input_data::{ MouseButton as DioxusMouseButton, MouseButtonSet as DioxusMouseButtons, }; +use dioxus_html::FormValue; use dioxus_html::{event_bubbles, prelude::*}; -use dioxus_html::{FocusData, FormValue, KeyboardData, MouseData, WheelData}; use std::any::Any; use std::collections::HashMap; use std::{ @@ -77,7 +77,7 @@ impl HasFormData for FormData { self.value.clone() } - fn values(&self) -> HashMap> { + fn values(&self) -> HashMap { self.values.clone() } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 83b4173e6..9d08f09f4 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -9,15 +9,11 @@ use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{event_bubbles, CompositionData, FormData, FormValue, MountedData}; +use dioxus_html::PlatformEventData; +use dioxus_html::{event_bubbles, MountedData}; use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel}; -use dioxus_html::{event_bubbles, MountedData, PlatformEventData}; -use dioxus_interpreter_js::get_node; -use dioxus_interpreter_js::{minimal_bindings, save_template, Channel}; use futures_channel::mpsc; use rustc_hash::FxHashMap; -use std::{any::Any, collections::HashMap, rc::Rc}; -use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast, JsValue}; use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{Document, Element, Event}; @@ -264,185 +260,6 @@ impl WebsysDom { #[cfg(feature = "mounted")] for id in to_mount { - let node = get_node(id.0 as u32); - if let Some(element) = node.dyn_ref::() { - let data: MountedData = element.into(); - let data = Rc::new(data); - let _ = self.event_channel.unbounded_send(UiEvent { - name: "mounted".to_string(), - bubbles: false, - element: id, - data, - }); - } - } - } -} - -// 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. -pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc { - use dioxus_html::events::*; - - match event.type_().as_str() { - "copy" | "cut" | "paste" => Rc::new(ClipboardData {}), - "compositionend" | "compositionstart" | "compositionupdate" => { - make_composition_event(&event) - } - "keydown" | "keypress" | "keyup" => Rc::new(KeyboardData::from(event)), - "focus" | "blur" | "focusout" | "focusin" => Rc::new(FocusData {}), - - "change" | "input" | "invalid" | "reset" | "submit" => read_input_to_data(target), - - "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" - | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Rc::new(MouseData::from(event)) - } - "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" - | "drop" => { - let mouse = MouseData::from(event); - Rc::new(DragData { mouse }) - } - - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Rc::new(PointerData::from(event)) - } - "select" => Rc::new(SelectionData {}), - "touchcancel" | "touchend" | "touchmove" | "touchstart" => Rc::new(TouchData::from(event)), - - "scroll" => Rc::new(ScrollData {}), - "wheel" => Rc::new(WheelData::from(event)), - "animationstart" | "animationend" | "animationiteration" => { - Rc::new(AnimationData::from(event)) - } - "transitionend" => Rc::new(TransitionData::from(event)), - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => Rc::new(MediaData {}), - "error" => Rc::new(ImageData { load_error: true }), - "load" => Rc::new(ImageData { load_error: false }), - "toggle" => Rc::new(ToggleData {}), - - _ => Rc::new(()), - } -} - -fn make_composition_event(event: &Event) -> Rc { - let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); - Rc::new(CompositionData { - data: evt.data().unwrap_or_default(), - }) -} - -pub(crate) fn load_document() -> Document { - web_sys::window() - .expect("should have access to the Window") - .document() - .expect("should have access to the Document") -} - -fn read_input_to_data(target: Element) -> Rc { - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - - let value: String = target - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - // todo: special case more input types - 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()) - }) - // select elements are NOT input events - because - why woudn't they be?? - .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"); - - let mut values = HashMap::new(); - - // try to fill in form values - if let Some(form) = target.dyn_ref::() { - let form_data = get_form_data(form); - for value in form_data.entries().into_iter().flatten() { - if let Ok(array) = value.dyn_into::() { - if let Some(name) = array.get(0).as_string() { - if let Ok(item_values) = array.get(1).dyn_into::() { - let item_values: Vec = - item_values.iter().filter_map(|v| v.as_string()).collect(); - - values.insert(name, FormValue::VecText(item_values)); - } else if let Ok(item_value) = array.get(1).dyn_into::() { - values.insert(name, FormValue::Text(item_value.as_string().unwrap())); - } - } - } - } - } - - #[cfg(not(feature = "file_engine"))] - let files = None; - #[cfg(feature = "file_engine")] - let files = target - .dyn_ref() - .and_then(|input: &web_sys::HtmlInputElement| { - input.files().and_then(|files| { - #[allow(clippy::arc_with_non_send_sync)] - crate::file_engine::WebFileEngine::new(files) - .map(|f| std::sync::Arc::new(f) as std::sync::Arc) - }) - }); - - Rc::new(FormData { - value, - values, - files, - }) -} - -// web-sys does not expose the keys api for form data, so we need to manually bind to it -#[wasm_bindgen(inline_js = r#" - export function get_form_data(form) { - let values = new Map(); - - const formData = new FormData(form); - - for (let name of formData.keys()) { - const fieldType = form.elements[name].type; - - switch (fieldType) { - case "select-multiple": - values.set(name, formData.getAll(name)); - break; - - // add cases for fieldTypes that can hold multiple values here - default: - values.set(name, formData.get(name)); - break; - } self.send_mount_event(id); } } diff --git a/packages/web/src/event.rs b/packages/web/src/event.rs index d1eed67a6..994bf304a 100644 --- a/packages/web/src/event.rs +++ b/packages/web/src/event.rs @@ -1,11 +1,11 @@ use std::{any::Any, collections::HashMap}; use dioxus_html::{ - FileEngine, FormData, HasFormData, HasImageData, HtmlEventConverter, ImageData, MountedData, - PlatformEventData, ScrollData, + prelude::FormValue, FileEngine, FormData, HasFormData, HasImageData, HtmlEventConverter, + ImageData, MountedData, PlatformEventData, ScrollData, }; use js_sys::Array; -use wasm_bindgen::{prelude::wasm_bindgen, JsCast}; +use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; use web_sys::{Document, Element, Event}; pub(crate) struct WebEventConverter; @@ -378,7 +378,7 @@ 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> { + fn values(&self) -> HashMap { let mut values = std::collections::HashMap::new(); // try to fill in form values @@ -388,10 +388,12 @@ impl HasFormData for WebFormData { if let Ok(array) = value.dyn_into::() { if let Some(name) = array.get(0).as_string() { if let Ok(item_values) = array.get(1).dyn_into::() { - let item_values = + let item_values: Vec = item_values.iter().filter_map(|v| v.as_string()).collect(); - values.insert(name, item_values); + values.insert(name, FormValue::VecText(item_values)); + } else if let Ok(item_value) = array.get(1).dyn_into::() { + values.insert(name, FormValue::Text(item_value.as_string().unwrap())); } } } @@ -432,7 +434,17 @@ impl HasFormData for WebFormData { const formData = new FormData(form); for (let name of formData.keys()) { - values.set(name, formData.getAll(name)); + const fieldType = target.elements[name].type; + + switch (fieldType) { + case "select-multiple": + contents.values[name] = formData.getAll(name); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + contents.values[name] = formData.get(name); + break; } return values;