From 73351590b85f6885fb434549eb093efe12bbdfa3 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Mon, 21 Aug 2023 17:22:55 +0200 Subject: [PATCH 1/6] xilem_html: Experiment with composition types and strong typed DOM elements --- crates/xilem_html/src/dom/attribute.rs | 72 ++++ .../xilem_html/src/dom/elements/generated.rs | 403 ++++++++++++++++++ crates/xilem_html/src/dom/elements/mod.rs | 70 +++ crates/xilem_html/src/dom/event.rs | 114 +++++ .../xilem_html/src/dom/interfaces/element.rs | 55 +++ .../src/dom/interfaces/event_target.rs | 46 ++ .../src/dom/interfaces/generated.rs | 115 +++++ crates/xilem_html/src/dom/interfaces/mod.rs | 9 + crates/xilem_html/src/dom/interfaces/node.rs | 19 + crates/xilem_html/src/dom/mod.rs | 4 + .../xilem_html/src/element/attribute_value.rs | 2 +- crates/xilem_html/src/element/mod.rs | 8 +- crates/xilem_html/src/lib.rs | 1 + crates/xilem_html/src/vecmap.rs | 36 ++ .../web_examples/todomvc/src/main.rs | 28 +- 15 files changed, 961 insertions(+), 21 deletions(-) create mode 100644 crates/xilem_html/src/dom/attribute.rs create mode 100644 crates/xilem_html/src/dom/elements/generated.rs create mode 100644 crates/xilem_html/src/dom/elements/mod.rs create mode 100644 crates/xilem_html/src/dom/event.rs create mode 100644 crates/xilem_html/src/dom/interfaces/element.rs create mode 100644 crates/xilem_html/src/dom/interfaces/event_target.rs create mode 100644 crates/xilem_html/src/dom/interfaces/generated.rs create mode 100644 crates/xilem_html/src/dom/interfaces/mod.rs create mode 100644 crates/xilem_html/src/dom/interfaces/node.rs create mode 100644 crates/xilem_html/src/dom/mod.rs diff --git a/crates/xilem_html/src/dom/attribute.rs b/crates/xilem_html/src/dom/attribute.rs new file mode 100644 index 000000000..6ad0ead7e --- /dev/null +++ b/crates/xilem_html/src/dom/attribute.rs @@ -0,0 +1,72 @@ +use std::borrow::Cow; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + view::DomElement, AttributeValue, ChangeFlags, Cx, IntoAttributeValue, View, ViewMarker, +}; + +use super::elements::ElementState; + +pub struct Attr { + pub(crate) element: E, + pub(crate) name: Cow<'static, str>, + pub(crate) value: Option, +} + +impl Attr { + pub fn attr>, V: IntoAttributeValue>( + self, + name: K, + value: V, + ) -> Attr { + Attr { + element: self, + name: name.into(), + value: value.into_attribute_value(), + } + } +} + +impl ViewMarker for Attr {} + +impl View for Attr +where + E: View>, + E::Element: DomElement, +{ + type State = E::State; + type Element = E::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, state, element) = self.element.build(cx); + if let Some(value) = &self.value { + let _ = element + .as_element_ref() + .set_attribute(&self.name, &value.serialize()); + } + (id, state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + state.add_new_attribute(&self.name, &self.value); + self.element.rebuild(cx, &prev.element, id, state, element) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + self.element.message(id_path, state, message, app_state) + } +} diff --git a/crates/xilem_html/src/dom/elements/generated.rs b/crates/xilem_html/src/dom/elements/generated.rs new file mode 100644 index 000000000..483ab848d --- /dev/null +++ b/crates/xilem_html/src/dom/elements/generated.rs @@ -0,0 +1,403 @@ +use std::marker::PhantomData; + +use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use xilem_core::{Id, MessageResult, VecSplice}; + +use crate::{dom::interfaces::Node, ChangeFlags, Cx, View, ViewMarker, ViewSequence}; + +use super::ElementState; + +macro_rules! generate_dom_interface_impl { + ($ty_name:ident, $name:ident, $t:ident, $a:ident, $vs:ident, $dom_interface:ident) => { + generate_dom_interface_impl!($ty_name, $name, $t, $a, $vs, $dom_interface, {}); + }; + ($ty_name:ident, $name:ident, $t:ident, $a:ident, $vs:ident, $dom_interface:ident, $body: tt) => { + impl<$t, $a, $vs> crate::dom::interfaces::$dom_interface<$t, $a> for $ty_name<$t, $a, $vs> + where + $vs: crate::view::ViewSequence<$t, $a>, + $body + }; +} + +macro_rules! impl_html_dom_interface { + ($ty_name: ident, $name: ident, $t: ident, $a:ident, $vs:ident, Node) => { + impl<$t, $a, $vs> crate::dom::interfaces::EventTarget for $ty_name<$t, $a, $vs> {} + impl<$t, $a, $vs> crate::dom::interfaces::Node for $ty_name<$t, $a, $vs> { + fn node_name(&self) -> &str { + stringify!($name) + } + } + }; + ($ty_name: ident, $name: ident, $t: ident, $a:ident, $vs:ident, Element) => { + impl_html_dom_interface!($ty_name, $name, $t, $a, $vs, Node); + generate_dom_interface_impl!($ty_name, $name, $t, $a, $vs, Element); + }; + ($ty_name: ident, $name: ident, $t: ident, $a:ident, $vs:ident, HtmlElement) => { + impl_html_dom_interface!($ty_name, $name, $t, $a, $vs, Element); + generate_dom_interface_impl!($ty_name, $name, $t, $a, $vs, HtmlElement); + }; + ($ty_name: ident, $name: ident, $t: ident, $a:ident, $vs:ident, HtmlAudioElement) => { + impl_html_dom_interface!($ty_name, $name, $t, $a, $vs, HtmlMediaElement); + generate_dom_interface_impl!($ty_name, $name, $t, $a, $vs, HtmlAudioElement); + }; + ($ty_name: ident, $name: ident, $t: ident, $a:ident, $vs:ident, HtmlVideoElement) => { + impl_html_dom_interface!($ty_name, $name, $t, $a, $vs, HtmlMediaElement); + generate_dom_interface_impl!($ty_name, $name, $t, $a, $vs, HtmlVideoElement); + }; + // TODO resolve parent interface correctly + // All remaining interfaces inherit directly from HtmlElement + ($ty_name: ident, $name: ident, $t: ident, $a:ident, $vs:ident, $dom_interface: ident) => { + impl_html_dom_interface!($ty_name, $name, $t, $a, $vs, HtmlElement); + generate_dom_interface_impl!($ty_name, $name, $t, $a, $vs, $dom_interface); + }; +} + +// TODO maybe it's possible to reduce even more in the impl function bodies and put into impl_functions +// (should improve compile times and probably wasm binary size) +macro_rules! define_html_element { + (($ty_name:ident, $name:ident, $dom_interface:ident)) => { + define_html_element!(($ty_name, $name, $dom_interface, T, A, VS)); + }; + (($ty_name:ident, $name:ident, $dom_interface:ident, $t:ident, $a: ident, $vs: ident)) => { + pub struct $ty_name<$t, $a, $vs>($vs, PhantomData ($t, $a)>); + + impl<$t, $a, $vs> ViewMarker for $ty_name<$t, $a, $vs> {} + + impl<$t, $a, $vs: ViewSequence<$t, $a>> View<$t, $a> for $ty_name<$t, $a, $vs> { + type State = ElementState<$vs::State>; + + type Element = web_sys::$dom_interface; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let el = cx.create_html_element(self.node_name()); + + let mut child_elements = vec![]; + let (id, child_states) = cx.with_new_id(|cx| self.0.build(cx, &mut child_elements)); + for child in &child_elements { + el.append_child(child.0.as_node_ref()).unwrap_throw(); + } + + // Set the id used internally to the `data-debugid` attribute. + // This allows the user to see if an element has been re-created or only altered. + #[cfg(debug_assertions)] + el.set_attribute("data-debugid", &id.to_raw().to_string()) + .unwrap_throw(); + + let el = el.dyn_into().unwrap_throw(); + let state = ElementState { + children_states: child_states, + child_elements, + scratch: vec![], + listeners: vec![], + new_attributes: Default::default(), + attributes: Default::default(), + id, + }; + (id, state, el) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + + state.apply_attribute_changes(element); + + // update children + let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); + changed |= cx.with_id(*id, |cx| { + self.0 + .rebuild(cx, &prev.0, &mut state.children_states, &mut splice) + }); + if changed.contains(ChangeFlags::STRUCTURE) { + // This is crude and will result in more DOM traffic than needed. + // The right thing to do is diff the new state of the children id + // vector against the old, and derive DOM mutations from that. + while let Some(child) = element.first_child() { + element.remove_child(&child).unwrap_throw(); + } + for child in &state.child_elements { + element.append_child(child.0.as_node_ref()).unwrap_throw(); + } + changed.remove(ChangeFlags::STRUCTURE); + } + changed + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut $t, + ) -> MessageResult<$a> { + self.0 + .message(id_path, &mut state.children_states, message, app_state) + } + } + + /// Builder function for a + #[doc = concat!("`", stringify!($name), "`")] + /// element view. + pub fn $name<$t, $a, $vs: ViewSequence<$t, $a>>(children: $vs) -> $ty_name<$t, $a, $vs> { + $ty_name(children, PhantomData) + } + + impl<$t, $a, $vs> $ty_name<$t, $a, $vs> { + pub fn attr< + AttrName: Into>, + AttrValue: crate::IntoAttributeValue, + >( + self, + name: AttrName, + value: AttrValue, + ) -> crate::dom::attribute::Attr { + crate::dom::attribute::Attr { + element: self, + name: name.into(), + value: value.into_attribute_value(), + } + } + } + + impl_html_dom_interface!($ty_name, $name, $t, $a, $vs, $dom_interface); + }; +} + +macro_rules! define_html_elements { + ($($element_def:tt,)*) => { + $(define_html_element!($element_def);)* + }; +} + +define_html_elements!( + // the order is copied from + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element + // DOM interfaces copied from https://html.spec.whatwg.org/multipage/grouping-content.html and friends + + // content sectioning + (Address, address, HtmlElement), + (Article, article, HtmlElement), + (Aside, aside, HtmlElement), + (Footer, footer, HtmlElement), + (Header, header, HtmlElement), + (H1, h1, HtmlHeadingElement), + (H2, h2, HtmlHeadingElement), + (H3, h3, HtmlHeadingElement), + (H4, h4, HtmlHeadingElement), + (H5, h5, HtmlHeadingElement), + (H6, h6, HtmlHeadingElement), + (Hgroup, hgroup, HtmlElement), + (Main, main, HtmlElement), + (Nav, nav, HtmlElement), + (Section, section, HtmlElement), + // text content + (Blockquote, blockquote, HtmlQuoteElement), + (Dd, dd, HtmlElement), + (Div, div, HtmlDivElement), + (Dl, dl, HtmlDListElement), + (Dt, dt, HtmlElement), + (Figcaption, figcaption, HtmlElement), + (Figure, figure, HtmlElement), + (Hr, hr, HtmlHrElement), + (Li, li, HtmlLiElement), + (Menu, menu, HtmlMenuElement), + (Ol, ol, HtmlOListElement), + (P, p, HtmlParagraphElement), + (Pre, pre, HtmlPreElement), + (Ul, ul, HtmlUListElement), + // inline text + (A, a, HtmlAnchorElement, T, A_, VS), + (Abbr, abbr, HtmlElement), + (B, b, HtmlElement), + (Bdi, bdi, HtmlElement), + (Bdo, bdo, HtmlElement), + (Br, br, HtmlBrElement), + (Cite, cite, HtmlElement), + (Code, code, HtmlElement), + (Data, data, HtmlDataElement), + (Dfn, dfn, HtmlElement), + (Em, em, HtmlElement), + (I, i, HtmlElement), + (Kbd, kbd, HtmlElement), + (Mark, mark, HtmlElement), + (Q, q, HtmlQuoteElement), + (Rp, rp, HtmlElement), + (Rt, rt, HtmlElement), + (Ruby, ruby, HtmlElement), + (S, s, HtmlElement), + (Samp, samp, HtmlElement), + (Small, small, HtmlElement), + (Span, span, HtmlSpanElement), + (Strong, strong, HtmlElement), + (Sub, sub, HtmlElement), + (Sup, sup, HtmlElement), + (Time, time, HtmlTimeElement), + (U, u, HtmlElement), + (Var, var, HtmlElement), + (Wbr, wbr, HtmlElement), + // image and multimedia + (Area, area, HtmlAreaElement), + (Audio, audio, HtmlAudioElement), + (Img, img, HtmlImageElement), + (Map, map, HtmlMapElement), + (Track, track, HtmlTrackElement), + (Video, video, HtmlVideoElement), + // embedded content + (Embed, embed, HtmlEmbedElement), + (Iframe, iframe, HtmlIFrameElement), + (Object, object, HtmlObjectElement), + (Picture, picture, HtmlPictureElement), + (Portal, portal, HtmlElement), + (Source, source, HtmlSourceElement), + // SVG and MathML (TODO, svg and mathml elements) + (Svg, svg, HtmlElement), + (Math, math, HtmlElement), + // scripting + (Canvas, canvas, HtmlCanvasElement), + (Noscript, noscript, HtmlElement), + (Script, script, HtmlScriptElement), + // demarcating edits + (Del, del, HtmlModElement), + (Ins, ins, HtmlModElement), + // tables + (Caption, caption, HtmlTableCaptionElement), + (Col, col, HtmlTableColElement), + (Colgroup, colgroup, HtmlTableColElement), + (Table, table, HtmlTableSectionElement), + (Tbody, tbody, HtmlTableSectionElement), + (Td, td, HtmlTableCellElement), + (Tfoot, tfoot, HtmlTableSectionElement), + (Th, th, HtmlTableCellElement), + (Thead, thead, HtmlTableSectionElement), + (Tr, tr, HtmlTableRowElement), + // forms + (Button, button, HtmlButtonElement), + (Datalist, datalist, HtmlDataListElement), + (Fieldset, fieldset, HtmlFieldSetElement), + (Form, form, HtmlFormElement), + (Input, input, HtmlInputElement), + (Label, label, HtmlLabelElement), + (Legend, legend, HtmlLegendElement), + (Meter, meter, HtmlMeterElement), + (Optgroup, optgroup, HtmlOptGroupElement), + (OptionElement, option, HtmlOptionElement), // Avoid cluttering the namespace with `Option` + (Output, output, HtmlOutputElement), + (Progress, progress, HtmlProgressElement), + (Select, select, HtmlSelectElement), + (Textarea, textarea, HtmlTextAreaElement), + // interactive elements, + (Details, details, HtmlDetailsElement), + (Dialog, dialog, HtmlDialogElement), + (Summary, summary, HtmlElement), + // web components, + (Slot, slot, HtmlSlotElement), + (Template, template, HtmlTemplateElement), +); + +// For quicker iteration/experimentation, The code below should be 1:1 the code used above in the macro +// ---------------------------------------------------------------------------------------------------- + +// use crate::dom::interfaces::{Element, EventTarget, HtmlDivElement, HtmlElement, Node}, + +// pub struct Div(VS, PhantomData (T, A)>); + +// impl ViewMarker for Div {} + +// impl> View for Div { +// type State = ElementState; + +// type Element = web_sys::HtmlDivElement; + +// fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { +// let el = cx.create_html_element(self.node_name()); + +// let mut child_elements = vec![]; +// let (id, child_states) = cx.with_new_id(|cx| self.0.build(cx, &mut child_elements)); +// for child in &child_elements { +// el.append_child(child.0.as_node_ref()).unwrap_throw(); +// } + +// // Set the id used internally to the `data-debugid` attribute. +// // This allows the user to see if an element has been re-created or only altered. +// #[cfg(debug_assertions)] +// el.set_attribute("data-debugid", &id.to_raw().to_string()) +// .unwrap_throw(); + +// let el = el.dyn_into().unwrap_throw(); +// let state = ElementState { +// children_states: child_states, +// child_elements, +// scratch: vec![], +// listeners: vec![], +// new_attributes: Default::default(), +// attributes: Default::default(), +// id, +// }; +// (id, state, el) +// } + +// fn rebuild( +// &self, +// cx: &mut Cx, +// prev: &Self, +// id: &mut Id, +// state: &mut Self::State, +// element: &mut Self::Element, +// ) -> ChangeFlags { +// let mut changed = ChangeFlags::empty(); + +// state.apply_attribute_changes(element); + +// // update children +// let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); +// changed |= cx.with_id(*id, |cx| { +// self.0 +// .rebuild(cx, &prev.0, &mut state.children_states, &mut splice) +// }); +// if changed.contains(ChangeFlags::STRUCTURE) { +// // This is crude and will result in more DOM traffic than needed. +// // The right thing to do is diff the new state of the children id +// // vector against the old, and derive DOM mutations from that. +// while let Some(child) = element.first_child() { +// element.remove_child(&child).unwrap_throw(); +// } +// for child in &state.child_elements { +// element.append_child(child.0.as_node_ref()).unwrap_throw(); +// } +// changed.remove(ChangeFlags::STRUCTURE); +// } +// changed +// } + +// fn message( +// &self, +// id_path: &[Id], +// state: &mut Self::State, +// message: Box, +// app_state: &mut T, +// ) -> MessageResult { +// self.0 +// .message(id_path, &mut state.children_states, message, app_state) +// } +// } + +// pub fn div>(children: VS) -> Div { +// Div(children, PhantomData) +// } + +// impl EventTarget for Div {} +// impl Node for Div { +// fn node_name(&self) -> &str { +// "div" +// } +// } +// impl Element for Div where VS: ViewSequence {} + +// impl HtmlElement for Div where VS: ViewSequence {} +// impl HtmlDivElement for Div where VS: ViewSequence {} diff --git a/crates/xilem_html/src/dom/elements/mod.rs b/crates/xilem_html/src/dom/elements/mod.rs new file mode 100644 index 000000000..fbd674e84 --- /dev/null +++ b/crates/xilem_html/src/dom/elements/mod.rs @@ -0,0 +1,70 @@ +mod generated; +pub use generated::*; + +use crate::{ + diff::{diff_kv_iterables, Diff}, + element::{remove_attribute, set_attribute}, + vecmap::VecMap, + AttributeValue, ChangeFlags, Pod, +}; +use xilem_core::Id; + +type CowStr = std::borrow::Cow<'static, str>; + +// TODO: could be split to struct without generic parameter (to avoid monomorphized bloat (methods below)) +/// The state associated with a HTML element `View`. +/// +/// Stores handles to the child elements and any child state, as well as attributes and event listeners +pub struct ElementState { + pub(crate) children_states: ViewSeqState, + pub(crate) listeners: Vec<(Id, gloo::events::EventListener)>, // used to keep the listeners alive + pub(crate) id: Id, + pub(crate) new_attributes: VecMap, + pub(crate) attributes: VecMap, + pub(crate) child_elements: Vec, + pub(crate) scratch: Vec, +} + +impl ElementState { + pub(crate) fn add_new_listener(&mut self, id: Id, value: gloo::events::EventListener) { + self.listeners.push((id, value)); + } + + pub(crate) fn get_listener(&mut self, id: Id) -> Option<&mut gloo::events::EventListener> { + self.listeners + .iter_mut() + .find(|(listener_id, _)| *listener_id == id) + .map(|(_, listener)| listener) + } + + // TODO Not sure how multiple attribute definitions with the same name should be handled (e.g. `e.attr("class", "a").attr("class", "b")`) + // Currently the outer most (in the example above "b") defines the attribute (when it isn't `None`, in that case the inner attr defines the value) + pub(crate) fn add_new_attribute(&mut self, name: &CowStr, value: &Option) { + if let Some(value) = value { + // could be slightly optimized via something like this: `new_attrs.entry(name).or_insert_with(|| value)` + if !self.new_attributes.contains_key(name) { + self.new_attributes.insert(name.clone(), value.clone()); + } + } + } + + pub(crate) fn apply_attribute_changes(&mut self, element: &web_sys::Element) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + // update attributes + for itm in diff_kv_iterables(&self.attributes, &self.new_attributes) { + match itm { + Diff::Add(name, value) | Diff::Change(name, value) => { + set_attribute(element, name, &value.serialize()); + changed |= ChangeFlags::OTHER_CHANGE; + } + Diff::Remove(name) => { + remove_attribute(element, name); + changed |= ChangeFlags::OTHER_CHANGE; + } + } + } + std::mem::swap(&mut self.attributes, &mut self.new_attributes); + self.new_attributes.clear(); + changed + } +} diff --git a/crates/xilem_html/src/dom/event.rs b/crates/xilem_html/src/dom/event.rs new file mode 100644 index 000000000..65d3bac31 --- /dev/null +++ b/crates/xilem_html/src/dom/event.rs @@ -0,0 +1,114 @@ +use std::{any::Any, borrow::Cow, marker::PhantomData}; + +use gloo::events::EventListenerOptions; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use xilem_core::{Id, MessageResult}; + +use crate::{view::DomNode, ChangeFlags, Cx, OptionalAction, View, ViewMarker}; + +use super::elements::ElementState; + +/// Wraps a [`View`] `V` and attaches an event listener. +/// +/// The event type `E` contains both the [`web_sys::Event`] subclass for this event and the +/// [`web_sys::HtmlElement`] subclass that matches `V::Element`. +pub struct EventListener { + pub(crate) element: V, + pub(crate) event: Cow<'static, str>, + pub(crate) event_handler_options: EventListenerOptions, + pub(crate) handler: F, + pub(crate) phantom_event_ty: PhantomData, +} + +impl EventListener +where + E: JsCast + 'static, +{ + fn create_event_listener( + &self, + target: &web_sys::EventTarget, + cx: &Cx, + ) -> gloo::events::EventListener { + let thunk = cx.message_thunk(); + gloo::events::EventListener::new_with_options( + target, + self.event.clone(), + self.event_handler_options, + move |event: &web_sys::Event| { + let event = (*event).clone().dyn_into::().unwrap_throw(); + thunk.push_message(event); + }, + ) + } +} + +impl ViewMarker for EventListener {} + +impl View for EventListener +where + F: Fn(&mut T, E) -> OA, + V: View>, + E: JsCast + 'static, + OA: OptionalAction, +{ + type State = V::State; + + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, (mut state, element, listener)) = cx.with_new_id(|cx| { + // id is already stored in element state + let (_id, state, element) = self.element.build(cx); + let listener = self.create_event_listener(element.as_node_ref(), cx); + (state, element, listener) + }); + state.add_new_listener(id, listener); + (id, state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + cx.with_id(*id, |cx| { + let mut changed = self.element.rebuild(cx, &prev.element, id, state, element); + // TODO check equality of prev and current element + if prev.event != self.event || changed.contains(ChangeFlags::STRUCTURE) { + let new_listener = self.create_event_listener(element.as_node_ref(), cx); + if let Some(listener) = state.get_listener(*id) { + *listener = new_listener; + } else { + state.add_new_listener(*id, new_listener); + } + changed |= ChangeFlags::OTHER_CHANGE; + } + changed + }) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + match id_path { + [] if message.downcast_ref::().is_some() => { + let event = message.downcast::().unwrap(); + match (self.handler)(app_state, *event).action() { + Some(a) => MessageResult::Action(a), + None => MessageResult::Nop, + } + } + [element_id, rest_path @ ..] if *element_id == state.id => { + self.element.message(rest_path, state, message, app_state) + } + _ => MessageResult::Stale(message), + } + } +} diff --git a/crates/xilem_html/src/dom/interfaces/element.rs b/crates/xilem_html/src/dom/interfaces/element.rs new file mode 100644 index 000000000..d8faa697f --- /dev/null +++ b/crates/xilem_html/src/dom/interfaces/element.rs @@ -0,0 +1,55 @@ +use std::borrow::Cow; + +use wasm_bindgen::JsCast; + +use crate::{ + dom::{attribute::Attr, elements::ElementState, event::EventListener}, + view::DomElement, + IntoAttributeValue, OptionalAction, View, ViewMarker, +}; + +use super::Node; +pub trait Element: Node + View + ViewMarker +where + Self: Sized, +{ + // TODO should the API be "functional" in the sense, that new attributes are wrappers around the type, + // or should they modify the underlying instance (e.g. via the following methods)? + // The disadvantage that "functional" brings in, is that elements are not modifiable (i.e. attributes can't be simply added etc.) + // fn attrs(&self) -> &Attributes; + // fn attrs_mut(&mut self) -> &mut Attributes; + + /// Set an attribute on this element. + /// + /// # Panics + /// + /// If the name contains characters that are not valid in an attribute name, + /// then the `View::build`/`View::rebuild` functions will panic for this view. + fn attr>, V: IntoAttributeValue>( + self, + name: K, + value: V, + ) -> Attr { + Attr { + element: self, + name: name.into(), + value: value.into_attribute_value(), + } + } +} + +impl, ES> Element for Attr +where + E: View>, + E::Element: DomElement, +{ +} + +impl Element for EventListener +where + F: Fn(&mut T, Ev) -> OA, + E: View> + Element, + Ev: JsCast + 'static, + OA: OptionalAction, +{ +} diff --git a/crates/xilem_html/src/dom/interfaces/event_target.rs b/crates/xilem_html/src/dom/interfaces/event_target.rs new file mode 100644 index 000000000..ebad6870b --- /dev/null +++ b/crates/xilem_html/src/dom/interfaces/event_target.rs @@ -0,0 +1,46 @@ +use std::{borrow::Cow, marker::PhantomData}; + +use gloo::events::EventListenerOptions; + +use crate::dom::{attribute::Attr, event::EventListener}; + +pub trait EventTarget { + fn on( + self, + event: impl Into>, + handler: EH, + ) -> EventListener + where + EH: Fn(&mut T, E) -> OA, + Self: Sized, + { + EventListener { + event: event.into(), + element: self, + event_handler_options: Default::default(), + handler, + phantom_event_ty: PhantomData, + } + } + fn on_with_options( + self, + event: impl Into>, + handler: EH, + options: EventListenerOptions, + ) -> EventListener + where + EH: Fn(&mut T, E) -> OA, + Self: Sized, + { + EventListener { + event: event.into(), + element: self, + event_handler_options: options, + handler, + phantom_event_ty: PhantomData, + } + } +} + +impl EventTarget for Attr {} +impl EventTarget for EventListener {} diff --git a/crates/xilem_html/src/dom/interfaces/generated.rs b/crates/xilem_html/src/dom/interfaces/generated.rs new file mode 100644 index 000000000..aa9e6a112 --- /dev/null +++ b/crates/xilem_html/src/dom/interfaces/generated.rs @@ -0,0 +1,115 @@ +use wasm_bindgen::JsCast; + +use crate::{ + dom::{attribute::Attr, elements::ElementState, event::EventListener, interfaces::Element}, + view::DomElement, + OptionalAction, View, +}; + +macro_rules! dom_interface_trait_definitions { + ($($dom_interface:ident : $super_dom_interface: ident $body: tt),*) => { + $( + pub trait $dom_interface: $super_dom_interface $body + + impl, ES> $dom_interface for Attr + where + E: View>, + E::Element: DomElement, + { + } + + impl $dom_interface for EventListener + where + F: Fn(&mut T, Ev) -> OA, + E: View> + $dom_interface, + Ev: JsCast + 'static, + OA: OptionalAction, + { + } + )* + }; +} + +dom_interface_trait_definitions!( + HtmlAnchorElement : HtmlElement {}, + HtmlAreaElement : HtmlElement {}, + HtmlAudioElement : HtmlMediaElement {}, + HtmlBaseElement : HtmlElement {}, + HtmlBodyElement : HtmlElement {}, + HtmlBrElement : HtmlElement {}, + HtmlButtonElement : HtmlElement {}, + HtmlCanvasElement : HtmlElement { + // Basic idea how to get strong typed attributes working + // Rather the DOM interface attributes than HTML/XML attributes though, as they are (mostly) well defined by the spec, + // compared to HTML/XML attributes. + fn width(self, width: u32) -> Attr { + self.attr("width", width) + } + fn height(self, height: u32) -> Attr { + self.attr("height", height) + } + }, + HtmlDataElement : HtmlElement {}, + HtmlDataListElement : HtmlElement {}, + HtmlDetailsElement : HtmlElement {}, + HtmlDialogElement : HtmlElement {}, + HtmlDirectoryElement : HtmlElement {}, + HtmlDivElement : HtmlElement {}, + HtmlDListElement : HtmlElement {}, + HtmlElement : Element {}, + HtmlUnknownElement : HtmlElement {}, + HtmlEmbedElement : HtmlElement {}, + HtmlFieldSetElement : HtmlElement {}, + HtmlFontElement : HtmlElement {}, + HtmlFormElement : HtmlElement {}, + HtmlFrameElement : HtmlElement {}, + HtmlFrameSetElement : HtmlElement {}, + HtmlHeadElement : HtmlElement {}, + HtmlHeadingElement : HtmlElement {}, + HtmlHrElement : HtmlElement {}, + HtmlHtmlElement : HtmlElement {}, + HtmlIFrameElement : HtmlElement {}, + HtmlImageElement : HtmlElement {}, + HtmlInputElement : HtmlElement {}, + HtmlLabelElement : HtmlElement {}, + HtmlLegendElement : HtmlElement {}, + HtmlLiElement : HtmlElement {}, + HtmlLinkElement : HtmlElement {}, + HtmlMapElement : HtmlElement {}, + HtmlMediaElement : HtmlElement {}, + HtmlMenuElement : HtmlElement {}, + HtmlMenuItemElement : HtmlElement {}, + HtmlMetaElement : HtmlElement {}, + HtmlMeterElement : HtmlElement {}, + HtmlModElement : HtmlElement {}, + HtmlObjectElement : HtmlElement {}, + HtmlOListElement : HtmlElement {}, + HtmlOptGroupElement : HtmlElement {}, + HtmlOptionElement : HtmlElement {}, + HtmlOutputElement : HtmlElement {}, + HtmlParagraphElement : HtmlElement {}, + HtmlParamElement : HtmlElement {}, + HtmlPictureElement : HtmlElement {}, + HtmlPreElement : HtmlElement {}, + HtmlProgressElement : HtmlElement {}, + HtmlQuoteElement : HtmlElement {}, + HtmlScriptElement : HtmlElement {}, + HtmlSelectElement : HtmlElement {}, + HtmlSlotElement : HtmlElement {}, + HtmlSourceElement : HtmlElement {}, + HtmlSpanElement : HtmlElement {}, + HtmlStyleElement : HtmlElement {}, + HtmlTableCaptionElement : HtmlElement {}, + HtmlTableCellElement : HtmlElement {}, + HtmlTableColElement : HtmlElement {}, + HtmlTableElement : HtmlElement {}, + HtmlTableRowElement : HtmlElement {}, + HtmlTableSectionElement : HtmlElement {}, + HtmlTemplateElement : HtmlElement {}, + HtmlTimeElement : HtmlElement {}, + HtmlTextAreaElement : HtmlElement {}, + HtmlTitleElement : HtmlElement {}, + HtmlTrackElement : HtmlElement {}, + HtmlUListElement : HtmlElement {}, + HtmlVideoElement : HtmlMediaElement {} +); diff --git a/crates/xilem_html/src/dom/interfaces/mod.rs b/crates/xilem_html/src/dom/interfaces/mod.rs new file mode 100644 index 000000000..bff14222c --- /dev/null +++ b/crates/xilem_html/src/dom/interfaces/mod.rs @@ -0,0 +1,9 @@ +pub mod element; +pub mod event_target; +pub mod generated; +pub mod node; + +pub use element::*; +pub use event_target::*; +pub use generated::*; +pub use node::*; diff --git a/crates/xilem_html/src/dom/interfaces/node.rs b/crates/xilem_html/src/dom/interfaces/node.rs new file mode 100644 index 000000000..bf68af1d8 --- /dev/null +++ b/crates/xilem_html/src/dom/interfaces/node.rs @@ -0,0 +1,19 @@ +use crate::dom::{attribute::Attr, event::EventListener}; + +use super::EventTarget; + +pub trait Node: EventTarget { + fn node_name(&self) -> &str; +} + +impl Node for Attr { + fn node_name(&self) -> &str { + self.element.node_name() + } +} + +impl Node for EventListener { + fn node_name(&self) -> &str { + self.element.node_name() + } +} diff --git a/crates/xilem_html/src/dom/mod.rs b/crates/xilem_html/src/dom/mod.rs new file mode 100644 index 000000000..6dc2075a8 --- /dev/null +++ b/crates/xilem_html/src/dom/mod.rs @@ -0,0 +1,4 @@ +pub mod interfaces; +pub mod attribute; +pub mod event; +pub mod elements; \ No newline at end of file diff --git a/crates/xilem_html/src/element/attribute_value.rs b/crates/xilem_html/src/element/attribute_value.rs index 050a3a683..dd0e5c9a1 100644 --- a/crates/xilem_html/src/element/attribute_value.rs +++ b/crates/xilem_html/src/element/attribute_value.rs @@ -1,6 +1,6 @@ type CowStr = std::borrow::Cow<'static, str>; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Clone, Debug)] pub enum AttributeValue { True, // for the boolean true, this serializes to an empty string (e.g. for ) I32(i32), diff --git a/crates/xilem_html/src/element/mod.rs b/crates/xilem_html/src/element/mod.rs index d3dca3d6f..4a4311c19 100644 --- a/crates/xilem_html/src/element/mod.rs +++ b/crates/xilem_html/src/element/mod.rs @@ -237,7 +237,7 @@ where } #[cfg(feature = "typed")] -fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { +pub(crate) fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { // we have to special-case `value` because setting the value using `set_attribute` // doesn't work after the value has been changed. if name == "value" { @@ -252,12 +252,12 @@ fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { } #[cfg(not(feature = "typed"))] -fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { +pub(crate) fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { element.set_attribute(name, value).unwrap_throw(); } #[cfg(feature = "typed")] -fn remove_attribute(element: &web_sys::Element, name: &str) { +pub(crate) fn remove_attribute(element: &web_sys::Element, name: &str) { // we have to special-case `value` because setting the value using `set_attribute` // doesn't work after the value has been changed. if name == "checked" { @@ -269,6 +269,6 @@ fn remove_attribute(element: &web_sys::Element, name: &str) { } #[cfg(not(feature = "typed"))] -fn remove_attribute(element: &web_sys::Element, name: &str) { +pub(crate) fn remove_attribute(element: &web_sys::Element, name: &str) { element.remove_attribute(name).unwrap_throw(); } diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index 9e6f50783..f671fbe7a 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -11,6 +11,7 @@ mod app; mod class; mod context; mod diff; +pub mod dom; mod element; mod event; mod one_of; diff --git a/crates/xilem_html/src/vecmap.rs b/crates/xilem_html/src/vecmap.rs index d64fc52b7..d67788c88 100644 --- a/crates/xilem_html/src/vecmap.rs +++ b/crates/xilem_html/src/vecmap.rs @@ -38,6 +38,30 @@ impl VecMap { .find_map(|(k, v)| if key.eq(k.borrow()) { Some(v) } else { None }) } + /// Returns `true` if the map contains a value for the specified key. + /// + /// The key may be any borrowed form of the map's key type, but the ordering + /// on the borrowed form *must* match the ordering on the key type. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```ignore + /// # use crate::vecmap::VecMap; + /// let mut map = VecMap::default(); + /// map.insert(1, "a"); + /// assert!(map.contains_key(&1)); + /// assert!(!map.contains_key(&2)); + /// ``` + pub fn contains_key(&self, key: &Q) -> bool + where + K: Borrow + Ord, + Q: Ord, + { + self.get(key).is_some() + } + /// Returns a mutable reference to the value corresponding to the key. /// /// The key may be any borrowed form of the map's key type, but the ordering @@ -178,6 +202,10 @@ impl VecMap { } } + pub fn clear(&mut self) { + self.0.clear() + } + /// Returns `true` if the map contains no elements. /// /// # Examples @@ -254,6 +282,14 @@ mod tests { assert_eq!(map.get(&2), None); } + #[test] + fn contains_key() { + let mut map = VecMap::default(); + map.insert(1, "a"); + assert!(map.contains_key(&1)); + assert!(!map.contains_key(&2)); + } + #[test] fn get_mut() { let mut map = VecMap::default(); diff --git a/crates/xilem_html/web_examples/todomvc/src/main.rs b/crates/xilem_html/web_examples/todomvc/src/main.rs index 1e877daf2..62c14a902 100644 --- a/crates/xilem_html/web_examples/todomvc/src/main.rs +++ b/crates/xilem_html/web_examples/todomvc/src/main.rs @@ -3,8 +3,9 @@ mod state; use state::{AppState, Filter, Todo}; use xilem_html::{ - elements as el, events::on_click, get_element_by_id, Action, Adapt, App, MessageResult, View, - ViewExt, ViewMarker, + dom::{elements as el, interfaces::Element}, + events::on_click, + get_element_by_id, Action, Adapt, App, MessageResult, View, ViewExt, ViewMarker, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce @@ -18,7 +19,7 @@ enum TodoAction { impl Action for TodoAction {} -fn todo_item(todo: &mut Todo, editing: bool) -> impl View + ViewMarker { +fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { let mut class = String::new(); if todo.completed { class.push_str(" completed"); @@ -68,7 +69,7 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View + Vi .attr("class", class) } -fn footer_view(state: &mut AppState, should_display: bool) -> impl View + ViewMarker { +fn footer_view(state: &mut AppState, should_display: bool) -> impl Element { let item_str = if state.todos.len() == 1 { "item" } else { @@ -86,7 +87,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl View impl View impl View + ViewMarker { @@ -159,16 +157,14 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl View .attr("class", "toggle-all") .attr("type", "checkbox") .attr("checked", state.are_all_complete()); - let mut section = el::section(( + + el::section(( toggle_all.on_click(|state: &mut AppState, _| state.toggle_all_complete()), el::label(()).attr("for", "toggle-all"), el::ul(todos).attr("class", "todo-list"), )) - .attr("class", "main"); - if !should_display { - section.set_attr("style", "display:none;"); - } - section + .attr("class", "main") + .attr("style", (!should_display).then_some("display:none;")) } fn app_logic(state: &mut AppState) -> impl View { From 7aa400883ec6260f990c54310b9a74c8b675035b Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Tue, 22 Aug 2023 21:09:59 +0200 Subject: [PATCH 2/6] Fix exponential compilation issue, and cleanup a little bit --- crates/xilem_html/src/context.rs | 53 ++++++- crates/xilem_html/src/dom/attribute.rs | 34 ++--- .../xilem_html/src/dom/elements/generated.rs | 129 +----------------- crates/xilem_html/src/dom/elements/mod.rs | 55 +------- crates/xilem_html/src/dom/event.rs | 46 ++++--- .../xilem_html/src/dom/interfaces/element.rs | 14 +- .../src/dom/interfaces/generated.rs | 16 +-- crates/xilem_html/src/dom/mod.rs | 4 +- crates/xilem_html/src/view.rs | 2 +- .../web_examples/todomvc/src/main.rs | 14 +- 10 files changed, 114 insertions(+), 253 deletions(-) diff --git a/crates/xilem_html/src/context.rs b/crates/xilem_html/src/context.rs index 57e9d42e3..d0ab8818b 100644 --- a/crates/xilem_html/src/context.rs +++ b/crates/xilem_html/src/context.rs @@ -6,12 +6,22 @@ use web_sys::Document; use xilem_core::{Id, IdPath}; -use crate::{app::AppRunner, Message, HTML_NS, SVG_NS}; +use crate::{ + app::AppRunner, + diff::{diff_kv_iterables, Diff}, + element::{remove_attribute, set_attribute}, + vecmap::VecMap, + AttributeValue, Message, HTML_NS, SVG_NS, +}; + +type CowStr = std::borrow::Cow<'static, str>; // Note: xilem has derive Clone here. Not sure. pub struct Cx { id_path: IdPath, document: Document, + // TODO There's likely a cleaner more robust way to propagate the attributes to an element + pub(crate) current_element_attributes: VecMap, app_ref: Option>, } @@ -34,6 +44,7 @@ impl Cx { id_path: Vec::new(), document: crate::document(), app_ref: None, + current_element_attributes: Default::default(), } } @@ -89,6 +100,46 @@ impl Cx { self.create_element(SVG_NS, name).unchecked_into() } + // TODO Not sure how multiple attribute definitions with the same name should be handled (e.g. `e.attr("class", "a").attr("class", "b")`) + // Currently the outer most (in the example above "b") defines the attribute (when it isn't `None`, in that case the inner attr defines the value) + pub(crate) fn add_new_attribute_to_current_element( + &mut self, + name: &CowStr, + value: &Option, + ) { + if let Some(value) = value { + // could be slightly optimized via something like this: `new_attrs.entry(name).or_insert_with(|| value)` + if !self.current_element_attributes.contains_key(name) { + self.current_element_attributes + .insert(name.clone(), value.clone()); + } + } + } + + pub(crate) fn apply_attribute_changes( + &mut self, + element: &web_sys::Element, + attributes: &mut VecMap, + ) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + // update attributes + for itm in diff_kv_iterables(&*attributes, &self.current_element_attributes) { + match itm { + Diff::Add(name, value) | Diff::Change(name, value) => { + set_attribute(element, name, &value.serialize()); + changed |= ChangeFlags::OTHER_CHANGE; + } + Diff::Remove(name) => { + remove_attribute(element, name); + changed |= ChangeFlags::OTHER_CHANGE; + } + } + } + std::mem::swap(attributes, &mut self.current_element_attributes); + self.current_element_attributes.clear(); + changed + } + pub fn message_thunk(&self) -> MessageThunk { MessageThunk { id_path: self.id_path.clone(), diff --git a/crates/xilem_html/src/dom/attribute.rs b/crates/xilem_html/src/dom/attribute.rs index 6ad0ead7e..b92524324 100644 --- a/crates/xilem_html/src/dom/attribute.rs +++ b/crates/xilem_html/src/dom/attribute.rs @@ -1,12 +1,11 @@ use std::borrow::Cow; +use wasm_bindgen::JsCast; use xilem_core::{Id, MessageResult}; -use crate::{ - view::DomElement, AttributeValue, ChangeFlags, Cx, IntoAttributeValue, View, ViewMarker, -}; +use crate::{view::DomNode, AttributeValue, ChangeFlags, Cx, View, ViewMarker}; -use super::elements::ElementState; +use super::interfaces::Element; pub struct Attr { pub(crate) element: E, @@ -14,27 +13,9 @@ pub struct Attr { pub(crate) value: Option, } -impl Attr { - pub fn attr>, V: IntoAttributeValue>( - self, - name: K, - value: V, - ) -> Attr { - Attr { - element: self, - name: name.into(), - value: value.into_attribute_value(), - } - } -} - impl ViewMarker for Attr {} -impl View for Attr -where - E: View>, - E::Element: DomElement, -{ +impl> View for Attr { type State = E::State; type Element = E::Element; @@ -42,7 +23,10 @@ where let (id, state, element) = self.element.build(cx); if let Some(value) = &self.value { let _ = element - .as_element_ref() + .as_node_ref() + .dyn_ref::() + // TODO remove the unwrap, make this safer... + .unwrap() .set_attribute(&self.name, &value.serialize()); } (id, state, element) @@ -56,7 +40,7 @@ where state: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { - state.add_new_attribute(&self.name, &self.value); + cx.add_new_attribute_to_current_element(&self.name, &self.value); self.element.rebuild(cx, &prev.element, id, state, element) } diff --git a/crates/xilem_html/src/dom/elements/generated.rs b/crates/xilem_html/src/dom/elements/generated.rs index 483ab848d..7071ca871 100644 --- a/crates/xilem_html/src/dom/elements/generated.rs +++ b/crates/xilem_html/src/dom/elements/generated.rs @@ -72,7 +72,8 @@ macro_rules! define_html_element { let el = cx.create_html_element(self.node_name()); let mut child_elements = vec![]; - let (id, child_states) = cx.with_new_id(|cx| self.0.build(cx, &mut child_elements)); + let (id, children_states) = + cx.with_new_id(|cx| self.0.build(cx, &mut child_elements)); for child in &child_elements { el.append_child(child.0.as_node_ref()).unwrap_throw(); } @@ -85,13 +86,10 @@ macro_rules! define_html_element { let el = el.dyn_into().unwrap_throw(); let state = ElementState { - children_states: child_states, + children_states, child_elements, scratch: vec![], - listeners: vec![], - new_attributes: Default::default(), attributes: Default::default(), - id, }; (id, state, el) } @@ -106,7 +104,7 @@ macro_rules! define_html_element { ) -> ChangeFlags { let mut changed = ChangeFlags::empty(); - state.apply_attribute_changes(element); + cx.apply_attribute_changes(element, &mut state.attributes); // update children let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); @@ -148,23 +146,6 @@ macro_rules! define_html_element { $ty_name(children, PhantomData) } - impl<$t, $a, $vs> $ty_name<$t, $a, $vs> { - pub fn attr< - AttrName: Into>, - AttrValue: crate::IntoAttributeValue, - >( - self, - name: AttrName, - value: AttrValue, - ) -> crate::dom::attribute::Attr { - crate::dom::attribute::Attr { - element: self, - name: name.into(), - value: value.into_attribute_value(), - } - } - } - impl_html_dom_interface!($ty_name, $name, $t, $a, $vs, $dom_interface); }; } @@ -299,105 +280,3 @@ define_html_elements!( (Slot, slot, HtmlSlotElement), (Template, template, HtmlTemplateElement), ); - -// For quicker iteration/experimentation, The code below should be 1:1 the code used above in the macro -// ---------------------------------------------------------------------------------------------------- - -// use crate::dom::interfaces::{Element, EventTarget, HtmlDivElement, HtmlElement, Node}, - -// pub struct Div(VS, PhantomData (T, A)>); - -// impl ViewMarker for Div {} - -// impl> View for Div { -// type State = ElementState; - -// type Element = web_sys::HtmlDivElement; - -// fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { -// let el = cx.create_html_element(self.node_name()); - -// let mut child_elements = vec![]; -// let (id, child_states) = cx.with_new_id(|cx| self.0.build(cx, &mut child_elements)); -// for child in &child_elements { -// el.append_child(child.0.as_node_ref()).unwrap_throw(); -// } - -// // Set the id used internally to the `data-debugid` attribute. -// // This allows the user to see if an element has been re-created or only altered. -// #[cfg(debug_assertions)] -// el.set_attribute("data-debugid", &id.to_raw().to_string()) -// .unwrap_throw(); - -// let el = el.dyn_into().unwrap_throw(); -// let state = ElementState { -// children_states: child_states, -// child_elements, -// scratch: vec![], -// listeners: vec![], -// new_attributes: Default::default(), -// attributes: Default::default(), -// id, -// }; -// (id, state, el) -// } - -// fn rebuild( -// &self, -// cx: &mut Cx, -// prev: &Self, -// id: &mut Id, -// state: &mut Self::State, -// element: &mut Self::Element, -// ) -> ChangeFlags { -// let mut changed = ChangeFlags::empty(); - -// state.apply_attribute_changes(element); - -// // update children -// let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); -// changed |= cx.with_id(*id, |cx| { -// self.0 -// .rebuild(cx, &prev.0, &mut state.children_states, &mut splice) -// }); -// if changed.contains(ChangeFlags::STRUCTURE) { -// // This is crude and will result in more DOM traffic than needed. -// // The right thing to do is diff the new state of the children id -// // vector against the old, and derive DOM mutations from that. -// while let Some(child) = element.first_child() { -// element.remove_child(&child).unwrap_throw(); -// } -// for child in &state.child_elements { -// element.append_child(child.0.as_node_ref()).unwrap_throw(); -// } -// changed.remove(ChangeFlags::STRUCTURE); -// } -// changed -// } - -// fn message( -// &self, -// id_path: &[Id], -// state: &mut Self::State, -// message: Box, -// app_state: &mut T, -// ) -> MessageResult { -// self.0 -// .message(id_path, &mut state.children_states, message, app_state) -// } -// } - -// pub fn div>(children: VS) -> Div { -// Div(children, PhantomData) -// } - -// impl EventTarget for Div {} -// impl Node for Div { -// fn node_name(&self) -> &str { -// "div" -// } -// } -// impl Element for Div where VS: ViewSequence {} - -// impl HtmlElement for Div where VS: ViewSequence {} -// impl HtmlDivElement for Div where VS: ViewSequence {} diff --git a/crates/xilem_html/src/dom/elements/mod.rs b/crates/xilem_html/src/dom/elements/mod.rs index fbd674e84..3ae402230 100644 --- a/crates/xilem_html/src/dom/elements/mod.rs +++ b/crates/xilem_html/src/dom/elements/mod.rs @@ -1,13 +1,7 @@ mod generated; pub use generated::*; -use crate::{ - diff::{diff_kv_iterables, Diff}, - element::{remove_attribute, set_attribute}, - vecmap::VecMap, - AttributeValue, ChangeFlags, Pod, -}; -use xilem_core::Id; +use crate::{vecmap::VecMap, AttributeValue, Pod}; type CowStr = std::borrow::Cow<'static, str>; @@ -17,54 +11,7 @@ type CowStr = std::borrow::Cow<'static, str>; /// Stores handles to the child elements and any child state, as well as attributes and event listeners pub struct ElementState { pub(crate) children_states: ViewSeqState, - pub(crate) listeners: Vec<(Id, gloo::events::EventListener)>, // used to keep the listeners alive - pub(crate) id: Id, - pub(crate) new_attributes: VecMap, pub(crate) attributes: VecMap, pub(crate) child_elements: Vec, pub(crate) scratch: Vec, } - -impl ElementState { - pub(crate) fn add_new_listener(&mut self, id: Id, value: gloo::events::EventListener) { - self.listeners.push((id, value)); - } - - pub(crate) fn get_listener(&mut self, id: Id) -> Option<&mut gloo::events::EventListener> { - self.listeners - .iter_mut() - .find(|(listener_id, _)| *listener_id == id) - .map(|(_, listener)| listener) - } - - // TODO Not sure how multiple attribute definitions with the same name should be handled (e.g. `e.attr("class", "a").attr("class", "b")`) - // Currently the outer most (in the example above "b") defines the attribute (when it isn't `None`, in that case the inner attr defines the value) - pub(crate) fn add_new_attribute(&mut self, name: &CowStr, value: &Option) { - if let Some(value) = value { - // could be slightly optimized via something like this: `new_attrs.entry(name).or_insert_with(|| value)` - if !self.new_attributes.contains_key(name) { - self.new_attributes.insert(name.clone(), value.clone()); - } - } - } - - pub(crate) fn apply_attribute_changes(&mut self, element: &web_sys::Element) -> ChangeFlags { - let mut changed = ChangeFlags::empty(); - // update attributes - for itm in diff_kv_iterables(&self.attributes, &self.new_attributes) { - match itm { - Diff::Add(name, value) | Diff::Change(name, value) => { - set_attribute(element, name, &value.serialize()); - changed |= ChangeFlags::OTHER_CHANGE; - } - Diff::Remove(name) => { - remove_attribute(element, name); - changed |= ChangeFlags::OTHER_CHANGE; - } - } - } - std::mem::swap(&mut self.attributes, &mut self.new_attributes); - self.new_attributes.clear(); - changed - } -} diff --git a/crates/xilem_html/src/dom/event.rs b/crates/xilem_html/src/dom/event.rs index 65d3bac31..d993c817f 100644 --- a/crates/xilem_html/src/dom/event.rs +++ b/crates/xilem_html/src/dom/event.rs @@ -6,8 +6,6 @@ use xilem_core::{Id, MessageResult}; use crate::{view::DomNode, ChangeFlags, Cx, OptionalAction, View, ViewMarker}; -use super::elements::ElementState; - /// Wraps a [`View`] `V` and attaches an event listener. /// /// The event type `E` contains both the [`web_sys::Event`] subclass for this event and the @@ -42,27 +40,39 @@ where } } +/// State for the `OnEvent` view. +pub struct EventListenerState { + #[allow(unused)] + listener: gloo::events::EventListener, + child_id: Id, + child_state: S, +} + impl ViewMarker for EventListener {} -impl View for EventListener +impl View for EventListener where F: Fn(&mut T, E) -> OA, - V: View>, + V: View, E: JsCast + 'static, OA: OptionalAction, { - type State = V::State; + type State = EventListenerState; type Element = V::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (id, (mut state, element, listener)) = cx.with_new_id(|cx| { + let (id, (element, state)) = cx.with_new_id(|cx| { // id is already stored in element state - let (_id, state, element) = self.element.build(cx); + let (child_id, child_state, element) = self.element.build(cx); let listener = self.create_event_listener(element.as_node_ref(), cx); - (state, element, listener) + let state = EventListenerState { + child_state, + child_id, + listener, + }; + (element, state) }); - state.add_new_listener(id, listener); (id, state, element) } @@ -75,15 +85,12 @@ where element: &mut Self::Element, ) -> ChangeFlags { cx.with_id(*id, |cx| { - let mut changed = self.element.rebuild(cx, &prev.element, id, state, element); - // TODO check equality of prev and current element + let mut changed = + self.element + .rebuild(cx, &prev.element, id, &mut state.child_state, element); + // TODO check equality of prev and current element somehow if prev.event != self.event || changed.contains(ChangeFlags::STRUCTURE) { - let new_listener = self.create_event_listener(element.as_node_ref(), cx); - if let Some(listener) = state.get_listener(*id) { - *listener = new_listener; - } else { - state.add_new_listener(*id, new_listener); - } + state.listener = self.create_event_listener(element.as_node_ref(), cx); changed |= ChangeFlags::OTHER_CHANGE; } changed @@ -105,8 +112,9 @@ where None => MessageResult::Nop, } } - [element_id, rest_path @ ..] if *element_id == state.id => { - self.element.message(rest_path, state, message, app_state) + [element_id, rest_path @ ..] if *element_id == state.child_id => { + self.element + .message(rest_path, &mut state.child_state, message, app_state) } _ => MessageResult::Stale(message), } diff --git a/crates/xilem_html/src/dom/interfaces/element.rs b/crates/xilem_html/src/dom/interfaces/element.rs index d8faa697f..96f2b18a9 100644 --- a/crates/xilem_html/src/dom/interfaces/element.rs +++ b/crates/xilem_html/src/dom/interfaces/element.rs @@ -3,8 +3,7 @@ use std::borrow::Cow; use wasm_bindgen::JsCast; use crate::{ - dom::{attribute::Attr, elements::ElementState, event::EventListener}, - view::DomElement, + dom::{attribute::Attr, event::EventListener}, IntoAttributeValue, OptionalAction, View, ViewMarker, }; @@ -38,17 +37,12 @@ where } } -impl, ES> Element for Attr -where - E: View>, - E::Element: DomElement, -{ -} +impl> Element for Attr {} -impl Element for EventListener +impl Element for EventListener where F: Fn(&mut T, Ev) -> OA, - E: View> + Element, + E: Element, Ev: JsCast + 'static, OA: OptionalAction, { diff --git a/crates/xilem_html/src/dom/interfaces/generated.rs b/crates/xilem_html/src/dom/interfaces/generated.rs index aa9e6a112..e0d739c82 100644 --- a/crates/xilem_html/src/dom/interfaces/generated.rs +++ b/crates/xilem_html/src/dom/interfaces/generated.rs @@ -1,9 +1,8 @@ use wasm_bindgen::JsCast; use crate::{ - dom::{attribute::Attr, elements::ElementState, event::EventListener, interfaces::Element}, - view::DomElement, - OptionalAction, View, + dom::{attribute::Attr, event::EventListener, interfaces::Element}, + OptionalAction, }; macro_rules! dom_interface_trait_definitions { @@ -11,17 +10,12 @@ macro_rules! dom_interface_trait_definitions { $( pub trait $dom_interface: $super_dom_interface $body - impl, ES> $dom_interface for Attr - where - E: View>, - E::Element: DomElement, - { - } + impl> $dom_interface for Attr { } - impl $dom_interface for EventListener + impl $dom_interface for EventListener where F: Fn(&mut T, Ev) -> OA, - E: View> + $dom_interface, + E: $dom_interface, Ev: JsCast + 'static, OA: OptionalAction, { diff --git a/crates/xilem_html/src/dom/mod.rs b/crates/xilem_html/src/dom/mod.rs index 6dc2075a8..aee67c128 100644 --- a/crates/xilem_html/src/dom/mod.rs +++ b/crates/xilem_html/src/dom/mod.rs @@ -1,4 +1,4 @@ -pub mod interfaces; pub mod attribute; +pub mod elements; pub mod event; -pub mod elements; \ No newline at end of file +pub mod interfaces; diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs index 361b66e11..140d3bb3b 100644 --- a/crates/xilem_html/src/view.rs +++ b/crates/xilem_html/src/view.rs @@ -18,7 +18,7 @@ mod sealed { // for a view element, rather than an associated type with a bound. /// This trait is implemented for types that implement `AsRef`. /// It is an implementation detail. -pub trait DomNode: sealed::Sealed { +pub trait DomNode: sealed::Sealed + 'static { fn into_pod(self) -> Pod; fn as_node_ref(&self) -> &web_sys::Node; } diff --git a/crates/xilem_html/web_examples/todomvc/src/main.rs b/crates/xilem_html/web_examples/todomvc/src/main.rs index 62c14a902..9f9c9c717 100644 --- a/crates/xilem_html/web_examples/todomvc/src/main.rs +++ b/crates/xilem_html/web_examples/todomvc/src/main.rs @@ -3,7 +3,10 @@ mod state; use state::{AppState, Filter, Todo}; use xilem_html::{ - dom::{elements as el, interfaces::Element}, + dom::{ + elements::{self as el}, + interfaces::{Element, EventTarget}, + }, events::on_click, get_element_by_id, Action, Adapt, App, MessageResult, View, ViewExt, ViewMarker, }; @@ -30,13 +33,14 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { let input = el::input(()) .attr("class", "toggle") .attr("type", "checkbox") - .attr("checked", todo.completed); + .attr("checked", todo.completed) + .on("click", |state: &mut Todo, _evt: web_sys::Event| { + state.completed = !state.completed; + }); el::li(( el::div(( - input.on_click(|state: &mut Todo, _| { - state.completed = !state.completed; - }), + input, el::label(todo.title.clone()) .on_dblclick(|state: &mut Todo, _| TodoAction::SetEditing(state.id)), el::button(()) From b628766278c2145a1275fe3ca6892dc494b22f3d Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Thu, 24 Aug 2023 16:45:04 +0200 Subject: [PATCH 3/6] Ported all event listeners for the `Element` DOM interface, and started with a `CustomElement` View --- .../xilem_html/src/dom/elements/generated.rs | 2 +- crates/xilem_html/src/dom/elements/mod.rs | 138 +++++++++- crates/xilem_html/src/dom/event.rs | 31 ++- .../xilem_html/src/dom/interfaces/element.rs | 253 +++++++++++++++++- .../src/dom/interfaces/event_target.rs | 33 ++- crates/xilem_html/src/view_ext.rs | 106 ++++---- .../web_examples/counter/src/main.rs | 18 +- .../web_examples/todomvc/src/main.rs | 56 ++-- 8 files changed, 527 insertions(+), 110 deletions(-) diff --git a/crates/xilem_html/src/dom/elements/generated.rs b/crates/xilem_html/src/dom/elements/generated.rs index 7071ca871..7ef8a0d30 100644 --- a/crates/xilem_html/src/dom/elements/generated.rs +++ b/crates/xilem_html/src/dom/elements/generated.rs @@ -59,7 +59,7 @@ macro_rules! define_html_element { define_html_element!(($ty_name, $name, $dom_interface, T, A, VS)); }; (($ty_name:ident, $name:ident, $dom_interface:ident, $t:ident, $a: ident, $vs: ident)) => { - pub struct $ty_name<$t, $a, $vs>($vs, PhantomData ($t, $a)>); + pub struct $ty_name<$t, $a = (), $vs = ()>($vs, PhantomData ($t, $a)>); impl<$t, $a, $vs> ViewMarker for $ty_name<$t, $a, $vs> {} diff --git a/crates/xilem_html/src/dom/elements/mod.rs b/crates/xilem_html/src/dom/elements/mod.rs index 3ae402230..1a59388e2 100644 --- a/crates/xilem_html/src/dom/elements/mod.rs +++ b/crates/xilem_html/src/dom/elements/mod.rs @@ -1,11 +1,21 @@ mod generated; +use std::marker::PhantomData; + pub use generated::*; -use crate::{vecmap::VecMap, AttributeValue, Pod}; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use xilem_core::{Id, MessageResult, VecSplice}; + +use crate::{ + vecmap::VecMap, + view::{DomElement, DomNode}, + AttributeValue, ChangeFlags, Cx, Pod, View, ViewMarker, ViewSequence, +}; + +use super::interfaces::{Element, EventTarget, Node}; type CowStr = std::borrow::Cow<'static, str>; -// TODO: could be split to struct without generic parameter (to avoid monomorphized bloat (methods below)) /// The state associated with a HTML element `View`. /// /// Stores handles to the child elements and any child state, as well as attributes and event listeners @@ -15,3 +25,127 @@ pub struct ElementState { pub(crate) child_elements: Vec, pub(crate) scratch: Vec, } + +pub struct CustomElement { + name: CowStr, + children: Children, + phantom: PhantomData El>, +} + +impl ViewMarker for CustomElement {} + +impl View for CustomElement +where + Children: ViewSequence, + El: DomElement + JsCast, +{ + type State = ElementState; + + type Element = El; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let el = cx.create_html_element(self.node_name()); + + let mut child_elements = vec![]; + let (id, children_states) = + cx.with_new_id(|cx| self.children.build(cx, &mut child_elements)); + + for child in &child_elements { + el.append_child(child.0.as_node_ref()).unwrap_throw(); + } + + // Set the id used internally to the `data-debugid` attribute. + // This allows the user to see if an element has been re-created or only altered. + #[cfg(debug_assertions)] + el.set_attribute("data-debugid", &id.to_raw().to_string()) + .unwrap_throw(); + + let el = el.dyn_into().unwrap_throw(); + let state = ElementState { + children_states, + child_elements, + scratch: vec![], + attributes: Default::default(), + }; + (id, state, el) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + + // update tag name + if prev.name != self.name { + // recreate element + let parent = element + .as_element_ref() + .parent_element() + .expect_throw("this element was mounted and so should have a parent"); + parent.remove_child(element.as_node_ref()).unwrap_throw(); + let new_element = cx.create_html_element(&self.name); + // TODO could this be combined with child updates? + while element.as_element_ref().child_element_count() > 0 { + new_element + .append_child(&element.as_element_ref().child_nodes().get(0).unwrap_throw()) + .unwrap_throw(); + } + *element = new_element.dyn_into().unwrap_throw(); + changed |= ChangeFlags::STRUCTURE; + } + + + let element = element.as_element_ref(); + + cx.apply_attribute_changes(element, &mut state.attributes); + + // update children + let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); + changed |= cx.with_id(*id, |cx| { + self.children + .rebuild(cx, &prev.children, &mut state.children_states, &mut splice) + }); + if changed.contains(ChangeFlags::STRUCTURE) { + // This is crude and will result in more DOM traffic than needed. + // The right thing to do is diff the new state of the children id + // vector against the old, and derive DOM mutations from that. + while let Some(child) = element.first_child() { + element.remove_child(&child).unwrap_throw(); + } + for child in &state.child_elements { + element.append_child(child.0.as_node_ref()).unwrap_throw(); + } + changed.remove(ChangeFlags::STRUCTURE); + } + changed + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + self.children + .message(id_path, &mut state.children_states, message, app_state) + } +} + +impl EventTarget for CustomElement {} + +impl Node for CustomElement { + fn node_name(&self) -> &str { + &self.name + } +} + +impl> Element + for CustomElement +{ +} diff --git a/crates/xilem_html/src/dom/event.rs b/crates/xilem_html/src/dom/event.rs index d993c817f..8a8275a71 100644 --- a/crates/xilem_html/src/dom/event.rs +++ b/crates/xilem_html/src/dom/event.rs @@ -1,6 +1,6 @@ use std::{any::Any, borrow::Cow, marker::PhantomData}; -use gloo::events::EventListenerOptions; +pub use gloo::events::EventListenerOptions; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use xilem_core::{Id, MessageResult}; @@ -13,7 +13,7 @@ use crate::{view::DomNode, ChangeFlags, Cx, OptionalAction, View, ViewMarker}; pub struct EventListener { pub(crate) element: V, pub(crate) event: Cow<'static, str>, - pub(crate) event_handler_options: EventListenerOptions, + pub(crate) options: EventListenerOptions, pub(crate) handler: F, pub(crate) phantom_event_ty: PhantomData, } @@ -22,6 +22,31 @@ impl EventListener where E: JsCast + 'static, { + pub fn new(element: V, event: impl Into>, handler: F) -> Self { + EventListener { + element, + event: event.into(), + options: Default::default(), + handler, + phantom_event_ty: PhantomData, + } + } + + pub fn new_with_options( + element: V, + event: impl Into>, + handler: F, + options: EventListenerOptions, + ) -> Self { + EventListener { + element, + event: event.into(), + options, + handler, + phantom_event_ty: PhantomData, + } + } + fn create_event_listener( &self, target: &web_sys::EventTarget, @@ -31,7 +56,7 @@ where gloo::events::EventListener::new_with_options( target, self.event.clone(), - self.event_handler_options, + self.options, move |event: &web_sys::Event| { let event = (*event).clone().dyn_into::().unwrap_throw(); thunk.push_message(event); diff --git a/crates/xilem_html/src/dom/interfaces/element.rs b/crates/xilem_html/src/dom/interfaces/element.rs index 96f2b18a9..e3a6d23c0 100644 --- a/crates/xilem_html/src/dom/interfaces/element.rs +++ b/crates/xilem_html/src/dom/interfaces/element.rs @@ -7,6 +7,37 @@ use crate::{ IntoAttributeValue, OptionalAction, View, ViewMarker, }; +// TODO should the options be its own function `on_event_with_options`, +// or should that be done via the builder pattern: `el.on_event().passive(false)`? +macro_rules! event_handler_mixin { + ($(($fn_name:ident, $fn_name_options:ident, $event:expr, $web_sys_event_type:ident),)*) => { + $( + fn $fn_name( + self, + handler: EH, + ) -> EventListener + where + OA: OptionalAction, + EH: Fn(&mut T, web_sys::$web_sys_event_type) -> OA, + { + EventListener::new(self, $event, handler) + } + + fn $fn_name_options( + self, + handler: EH, + options: gloo::events::EventListenerOptions, + ) -> EventListener + where + OA: OptionalAction, + EH: Fn(&mut T, web_sys::$web_sys_event_type) -> OA, + { + EventListener::new_with_options(self, $event, handler, options) + } + )* + }; +} + use super::Node; pub trait Element: Node + View + ViewMarker where @@ -24,17 +55,229 @@ where /// /// If the name contains characters that are not valid in an attribute name, /// then the `View::build`/`View::rebuild` functions will panic for this view. - fn attr>, V: IntoAttributeValue>( - self, - name: K, - value: V, - ) -> Attr { + fn attr(self, name: K, value: V) -> Attr + where + K: Into>, + V: IntoAttributeValue, + { Attr { element: self, name: name.into(), value: value.into_attribute_value(), } } + + fn class(self, class: V) -> Attr + where + V: IntoAttributeValue, + { + self.attr("class", class) + } + + // event list from + // https://html.spec.whatwg.org/multipage/webappapis.html#idl-definitions + // + // I didn't include the events on the window, since we aren't attaching + // any events to the window in xilem_html + event_handler_mixin!( + (on_abort, on_abort_with_options, "abort", Event), + ( + on_auxclick, + on_auxclick_with_options, + "auxclick", + PointerEvent + ), + ( + on_beforeinput, + on_beforeinput_with_options, + "beforeinput", + InputEvent + ), + ( + on_beforematch, + on_beforematch_with_options, + "beforematch", + Event + ), + ( + on_beforetoggle, + on_beforetoggle_with_options, + "beforetoggle", + Event + ), + (on_blur, on_blur_with_options, "blur", FocusEvent), + (on_cancel, on_cancel_with_options, "cancel", Event), + (on_canplay, on_canplay_with_options, "canplay", Event), + ( + on_canplaythrough, + on_canplaythrough_with_options, + "canplaythrough", + Event + ), + (on_change, on_change_with_options, "change", Event), + (on_click, on_click_with_options, "click", MouseEvent), + (on_close, on_close_with_options, "close", Event), + ( + on_contextlost, + on_contextlost_with_options, + "contextlost", + Event + ), + ( + on_contextmenu, + on_contextmenu_with_options, + "contextmenu", + PointerEvent + ), + ( + on_contextrestored, + on_contextrestored_with_options, + "contextrestored", + Event + ), + (on_copy, on_copy_with_options, "copy", Event), + (on_cuechange, on_cuechange_with_options, "cuechange", Event), + (on_cut, on_cut_with_options, "cut", Event), + ( + on_dblclick, + on_dblclick_with_options, + "dblclick", + MouseEvent + ), + (on_drag, on_drag_with_options, "drag", Event), + (on_dragend, on_dragend_with_options, "dragend", Event), + (on_dragenter, on_dragenter_with_options, "dragenter", Event), + (on_dragleave, on_dragleave_with_options, "dragleave", Event), + (on_dragover, on_dragover_with_options, "dragover", Event), + (on_dragstart, on_dragstart_with_options, "dragstart", Event), + (on_drop, on_drop_with_options, "drop", Event), + ( + on_durationchange, + on_durationchange_with_options, + "durationchange", + Event + ), + (on_emptied, on_emptied_with_options, "emptied", Event), + (on_ended, on_ended_with_options, "ended", Event), + (on_error, on_error_with_options, "error", Event), + (on_focus, on_focus_with_options, "focus", FocusEvent), + (on_focusin, on_focusin_with_options, "focusin", FocusEvent), + ( + on_focusout, + on_focusout_with_options, + "focusout", + FocusEvent + ), + (on_formdata, on_formdata_with_options, "formdata", Event), + (on_input, on_input_with_options, "input", InputEvent), + (on_invalid, on_invalid_with_options, "invalid", Event), + ( + on_keydown, + on_keydown_with_options, + "keydown", + KeyboardEvent + ), + (on_keyup, on_keyup_with_options, "keyup", KeyboardEvent), + (on_load, on_load_with_options, "load", Event), + ( + on_loadeddata, + on_loadeddata_with_options, + "loadeddata", + Event + ), + ( + on_loadedmetadata, + on_loadedmetadata_with_options, + "loadedmetadata", + Event + ), + (on_loadstart, on_loadstart_with_options, "loadstart", Event), + ( + on_mousedown, + on_mousedown_with_options, + "mousedown", + MouseEvent + ), + ( + on_mouseenter, + on_mouseenter_with_options, + "mouseenter", + MouseEvent + ), + ( + on_mouseleave, + on_mouseleave_with_options, + "mouseleave", + MouseEvent + ), + ( + on_mousemove, + on_mousemove_with_options, + "mousemove", + MouseEvent + ), + ( + on_mouseout, + on_mouseout_with_options, + "mouseout", + MouseEvent + ), + ( + on_mouseover, + on_mouseover_with_options, + "mouseover", + MouseEvent + ), + (on_mouseup, on_mouseup_with_options, "mouseup", MouseEvent), + (on_paste, on_paste_with_options, "paste", Event), + (on_pause, on_pause_with_options, "pause", Event), + (on_play, on_play_with_options, "play", Event), + (on_playing, on_playing_with_options, "playing", Event), + (on_progress, on_progress_with_options, "progress", Event), + ( + on_ratechange, + on_ratechange_with_options, + "ratechange", + Event + ), + (on_reset, on_reset_with_options, "reset", Event), + (on_resize, on_resize_with_options, "resize", Event), + (on_scroll, on_scroll_with_options, "scroll", Event), + (on_scrollend, on_scrollend_with_options, "scrollend", Event), + ( + on_securitypolicyviolation, + on_securitypolicyviolation_with_options, + "securitypolicyviolation", + Event + ), + (on_seeked, on_seeked_with_options, "seeked", Event), + (on_seeking, on_seeking_with_options, "seeking", Event), + (on_select, on_select_with_options, "select", Event), + ( + on_slotchange, + on_slotchange_with_options, + "slotchange", + Event + ), + (on_stalled, on_stalled_with_options, "stalled", Event), + (on_submit, on_submit_with_options, "submit", Event), + (on_suspend, on_suspend_with_options, "suspend", Event), + ( + on_timeupdate, + on_timeupdate_with_options, + "timeupdate", + Event + ), + (on_toggle, on_toggle_with_options, "toggle", Event), + ( + on_volumechange, + on_volumechange_with_options, + "volumechange", + Event + ), + (on_waiting, on_waiting_with_options, "waiting", Event), + (on_wheel, on_wheel_with_options, "wheel", WheelEvent), + ); } impl> Element for Attr {} diff --git a/crates/xilem_html/src/dom/interfaces/event_target.rs b/crates/xilem_html/src/dom/interfaces/event_target.rs index ebad6870b..de26c997a 100644 --- a/crates/xilem_html/src/dom/interfaces/event_target.rs +++ b/crates/xilem_html/src/dom/interfaces/event_target.rs @@ -1,44 +1,41 @@ -use std::{borrow::Cow, marker::PhantomData}; +use std::borrow::Cow; use gloo::events::EventListenerOptions; +use wasm_bindgen::JsCast; -use crate::dom::{attribute::Attr, event::EventListener}; +use crate::{ + dom::{attribute::Attr, event::EventListener}, + OptionalAction, +}; pub trait EventTarget { - fn on( + fn on( self, event: impl Into>, handler: EH, ) -> EventListener where + E: JsCast + 'static, + OA: OptionalAction, EH: Fn(&mut T, E) -> OA, Self: Sized, { - EventListener { - event: event.into(), - element: self, - event_handler_options: Default::default(), - handler, - phantom_event_ty: PhantomData, - } + EventListener::new(self, event, handler) } - fn on_with_options( + + fn on_with_options( self, event: impl Into>, handler: EH, options: EventListenerOptions, ) -> EventListener where + E: JsCast + 'static, + OA: OptionalAction, EH: Fn(&mut T, E) -> OA, Self: Sized, { - EventListener { - event: event.into(), - element: self, - event_handler_options: options, - handler, - phantom_event_ty: PhantomData, - } + EventListener::new_with_options(self, event, handler, options) } } diff --git a/crates/xilem_html/src/view_ext.rs b/crates/xilem_html/src/view_ext.rs index f62a46a11..6f1d858ec 100644 --- a/crates/xilem_html/src/view_ext.rs +++ b/crates/xilem_html/src/view_ext.rs @@ -10,59 +10,59 @@ use crate::{ /// A trait that makes it possible to attach event listeners and more to views /// in the continuation style. pub trait ViewExt: View + Sized { - /// Add an `onclick` event listener. - fn on_click< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnClick { - events::on_click(self, f) - } + // /// Add an `onclick` event listener. + // fn on_click< + // OA: OptionalAction, + // F: Fn(&mut T, &Event) -> OA, + // >( + // self, + // f: F, + // ) -> events::OnClick { + // events::on_click(self, f) + // } - /// Add an `ondblclick` event listener. - fn on_dblclick< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnDblClick { - events::on_dblclick(self, f) - } + // /// Add an `ondblclick` event listener. + // fn on_dblclick< + // OA: OptionalAction, + // F: Fn(&mut T, &Event) -> OA, + // >( + // self, + // f: F, + // ) -> events::OnDblClick { + // events::on_dblclick(self, f) + // } - /// Add an `oninput` event listener. - fn on_input< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnInput { - events::on_input(self, f) - } + // /// Add an `oninput` event listener. + // fn on_input< + // OA: OptionalAction, + // F: Fn(&mut T, &Event) -> OA, + // >( + // self, + // f: F, + // ) -> events::OnInput { + // events::on_input(self, f) + // } - /// Add an `onkeydown` event listener. - fn on_keydown< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnKeyDown { - events::on_keydown(self, f) - } + // /// Add an `onkeydown` event listener. + // fn on_keydown< + // OA: OptionalAction, + // F: Fn(&mut T, &Event) -> OA, + // >( + // self, + // f: F, + // ) -> events::OnKeyDown { + // events::on_keydown(self, f) + // } - fn on_blur< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnBlur { - events::on_blur(self, f) - } + // fn on_blur< + // OA: OptionalAction, + // F: Fn(&mut T, &Event) -> OA, + // >( + // self, + // f: F, + // ) -> events::OnBlur { + // events::on_blur(self, f) + // } fn adapt(self, f: F) -> Adapt where @@ -78,10 +78,10 @@ pub trait ViewExt: View + Sized { AdaptState::new(f, self) } - /// Apply a CSS class to the child view. - fn class(self, class: impl Into>) -> Class { - crate::class::class(self, class) - } + // /// Apply a CSS class to the child view. + // fn class(self, class: impl Into>) -> Class { + // crate::class::class(self, class) + // } } impl> ViewExt for V {} diff --git a/crates/xilem_html/web_examples/counter/src/main.rs b/crates/xilem_html/web_examples/counter/src/main.rs index baf919049..f2025cf68 100644 --- a/crates/xilem_html/web_examples/counter/src/main.rs +++ b/crates/xilem_html/web_examples/counter/src/main.rs @@ -1,7 +1,10 @@ use xilem_html::{ - document_body, elements as el, - events::{self as evt}, - App, Event, View, ViewExt, + document_body, + dom::{ + elements as el, + interfaces::{Element, HtmlButtonElement}, + }, + App, View, }; #[derive(Default)] @@ -39,13 +42,10 @@ impl AppState { } /// You can create functions that generate views. -fn btn( +fn btn( label: &'static str, - click_fn: F, -) -> evt::OnClick, F, ()> -where - F: Fn(&mut AppState, &Event), -{ + click_fn: impl Fn(&mut AppState, web_sys::MouseEvent), +) -> impl HtmlButtonElement { el::button(label).on_click(click_fn) } diff --git a/crates/xilem_html/web_examples/todomvc/src/main.rs b/crates/xilem_html/web_examples/todomvc/src/main.rs index 9f9c9c717..5b4d59760 100644 --- a/crates/xilem_html/web_examples/todomvc/src/main.rs +++ b/crates/xilem_html/web_examples/todomvc/src/main.rs @@ -2,13 +2,15 @@ mod state; use state::{AppState, Filter, Todo}; +use wasm_bindgen::JsCast; use xilem_html::{ dom::{ elements::{self as el}, - interfaces::{Element, EventTarget}, + event::EventListenerOptions, + interfaces::{Element, HtmlButtonElement}, }, events::on_click, - get_element_by_id, Action, Adapt, App, MessageResult, View, ViewExt, ViewMarker, + get_element_by_id, Action, Adapt, App, MessageResult, View, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce @@ -30,17 +32,16 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { if editing { class.push_str(" editing"); } - let input = el::input(()) + + let checkbox = el::input(()) .attr("class", "toggle") .attr("type", "checkbox") .attr("checked", todo.completed) - .on("click", |state: &mut Todo, _evt: web_sys::Event| { - state.completed = !state.completed; - }); + .on_click(|state: &mut Todo, _| state.completed = !state.completed); el::li(( el::div(( - input, + checkbox, el::label(todo.title.clone()) .on_dblclick(|state: &mut Todo, _| TodoAction::SetEditing(state.id)), el::button(()) @@ -62,12 +63,20 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { None } }) - .on_input(|state: &mut Todo, evt| { - state.title_editing.clear(); - state.title_editing.push_str(&evt.target().value()); - evt.prevent_default(); - }) - .passive(false) + .on_input_with_options( + |state: &mut Todo, evt| { + // TODO is there a less boilerplate but safe way to get to the value of the element? + let Some(target) = evt.target() else { + return; + }; + let Some(element) = target.dyn_ref::() else { + return; + }; + evt.prevent_default(); + state.title_editing = element.value(); + }, + EventListenerOptions::enable_prevent_default(), + ) .on_blur(|_, _| TodoAction::CancelEditing), )) .attr("class", class) @@ -132,7 +141,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element impl View + ViewMarker { +fn main_view(state: &mut AppState, should_display: bool) -> impl Element { let editing_id = state.editing_id; let todos: Vec<_> = state .visible_todos() @@ -190,11 +199,20 @@ fn app_logic(state: &mut AppState) -> impl View { state.create_todo(); } }) - .on_input(|state: &mut AppState, evt| { - state.update_new_todo(&evt.target().value()); - evt.prevent_default(); - }) - .passive(false), + .on_input_with_options( + |state: &mut AppState, evt| { + // TODO is there a less boilerplate but safe way to get to the value of the element? + let Some(target) = evt.target() else { + return; + }; + let Some(element) = target.dyn_ref::() else { + return; + }; + state.update_new_todo(&element.value()); + evt.prevent_default(); + }, + EventListenerOptions::enable_prevent_default(), + ), )) .attr("class", "header"), main, From 8560d0eae58f5aff18aeaa694768e98b202f4c61 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 26 Aug 2023 00:01:34 +0200 Subject: [PATCH 4/6] General cleanup --- .../xilem_html/src/dom/elements/generated.rs | 6 +- crates/xilem_html/src/dom/elements/mod.rs | 57 +++++++++++-------- .../xilem_html/src/dom/interfaces/element.rs | 25 ++++---- .../src/dom/interfaces/event_target.rs | 10 ++-- crates/xilem_html/src/dom/interfaces/node.rs | 6 +- crates/xilem_html/src/event/mod.rs | 1 + .../web_examples/counter/src/main.rs | 4 +- .../web_examples/todomvc/src/main.rs | 19 ++++--- 8 files changed, 71 insertions(+), 57 deletions(-) diff --git a/crates/xilem_html/src/dom/elements/generated.rs b/crates/xilem_html/src/dom/elements/generated.rs index 7ef8a0d30..12636023f 100644 --- a/crates/xilem_html/src/dom/elements/generated.rs +++ b/crates/xilem_html/src/dom/elements/generated.rs @@ -21,8 +21,8 @@ macro_rules! generate_dom_interface_impl { macro_rules! impl_html_dom_interface { ($ty_name: ident, $name: ident, $t: ident, $a:ident, $vs:ident, Node) => { - impl<$t, $a, $vs> crate::dom::interfaces::EventTarget for $ty_name<$t, $a, $vs> {} - impl<$t, $a, $vs> crate::dom::interfaces::Node for $ty_name<$t, $a, $vs> { + impl<$t, $a, $vs> crate::dom::interfaces::EventTarget<$t, $a> for $ty_name<$t, $a, $vs> {} + impl<$t, $a, $vs> crate::dom::interfaces::Node<$t, $a> for $ty_name<$t, $a, $vs> { fn node_name(&self) -> &str { stringify!($name) } @@ -44,7 +44,7 @@ macro_rules! impl_html_dom_interface { impl_html_dom_interface!($ty_name, $name, $t, $a, $vs, HtmlMediaElement); generate_dom_interface_impl!($ty_name, $name, $t, $a, $vs, HtmlVideoElement); }; - // TODO resolve parent interface correctly + // TODO resolve super interface correctly // All remaining interfaces inherit directly from HtmlElement ($ty_name: ident, $name: ident, $t: ident, $a:ident, $vs:ident, $dom_interface: ident) => { impl_html_dom_interface!($ty_name, $name, $t, $a, $vs, HtmlElement); diff --git a/crates/xilem_html/src/dom/elements/mod.rs b/crates/xilem_html/src/dom/elements/mod.rs index 1a59388e2..325649a35 100644 --- a/crates/xilem_html/src/dom/elements/mod.rs +++ b/crates/xilem_html/src/dom/elements/mod.rs @@ -7,12 +7,11 @@ use wasm_bindgen::{JsCast, UnwrapThrowExt}; use xilem_core::{Id, MessageResult, VecSplice}; use crate::{ - vecmap::VecMap, - view::{DomElement, DomNode}, - AttributeValue, ChangeFlags, Cx, Pod, View, ViewMarker, ViewSequence, + vecmap::VecMap, view::DomNode, AttributeValue, ChangeFlags, Cx, Pod, View, ViewMarker, + ViewSequence, }; -use super::interfaces::{Element, EventTarget, Node}; +use super::interfaces::{Element, EventTarget, HtmlElement, Node}; type CowStr = std::borrow::Cow<'static, str>; @@ -26,25 +25,39 @@ pub struct ElementState { pub(crate) scratch: Vec, } -pub struct CustomElement { +pub struct CustomElement { name: CowStr, children: Children, - phantom: PhantomData El>, + #[allow(clippy::type_complexity)] + phantom: PhantomData (T, A)>, } -impl ViewMarker for CustomElement {} +/// Builder function for a custom element view. +pub fn custom_element>( + name: impl Into, + children: Children, +) -> CustomElement { + CustomElement { + name: name.into(), + children, + phantom: PhantomData, + } +} + +impl ViewMarker for CustomElement {} -impl View for CustomElement +impl View for CustomElement where Children: ViewSequence, - El: DomElement + JsCast, { type State = ElementState; - type Element = El; + // This is mostly intended for Autonomous custom elements, + // TODO: Custom builtin components need some special handling (`document.createElement("p", { is: "custom-component" })`) + type Element = web_sys::HtmlElement; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let el = cx.create_html_element(self.node_name()); + let el = cx.create_html_element(&self.name); let mut child_elements = vec![]; let (id, children_states) = @@ -84,24 +97,20 @@ where if prev.name != self.name { // recreate element let parent = element - .as_element_ref() .parent_element() .expect_throw("this element was mounted and so should have a parent"); - parent.remove_child(element.as_node_ref()).unwrap_throw(); - let new_element = cx.create_html_element(&self.name); + parent.remove_child(element).unwrap_throw(); + let new_element = cx.create_html_element(self.node_name()); // TODO could this be combined with child updates? - while element.as_element_ref().child_element_count() > 0 { + while element.child_element_count() > 0 { new_element - .append_child(&element.as_element_ref().child_nodes().get(0).unwrap_throw()) + .append_child(&element.child_nodes().get(0).unwrap_throw()) .unwrap_throw(); } *element = new_element.dyn_into().unwrap_throw(); changed |= ChangeFlags::STRUCTURE; } - - let element = element.as_element_ref(); - cx.apply_attribute_changes(element, &mut state.attributes); // update children @@ -137,15 +146,13 @@ where } } -impl EventTarget for CustomElement {} +impl EventTarget for CustomElement {} -impl Node for CustomElement { +impl Node for CustomElement { fn node_name(&self) -> &str { &self.name } } -impl> Element - for CustomElement -{ -} +impl> Element for CustomElement {} +impl> HtmlElement for CustomElement {} diff --git a/crates/xilem_html/src/dom/interfaces/element.rs b/crates/xilem_html/src/dom/interfaces/element.rs index e3a6d23c0..6d4a31c84 100644 --- a/crates/xilem_html/src/dom/interfaces/element.rs +++ b/crates/xilem_html/src/dom/interfaces/element.rs @@ -4,7 +4,7 @@ use wasm_bindgen::JsCast; use crate::{ dom::{attribute::Attr, event::EventListener}, - IntoAttributeValue, OptionalAction, View, ViewMarker, + AttributeValue, IntoAttributeValue, OptionalAction, View, ViewMarker, }; // TODO should the options be its own function `on_event_with_options`, @@ -39,7 +39,8 @@ macro_rules! event_handler_mixin { } use super::Node; -pub trait Element: Node + View + ViewMarker +// TODO should Node or even EventTarget have the super trait View instead? +pub trait Element: Node + View + ViewMarker where Self: Sized, { @@ -55,11 +56,11 @@ where /// /// If the name contains characters that are not valid in an attribute name, /// then the `View::build`/`View::rebuild` functions will panic for this view. - fn attr(self, name: K, value: V) -> Attr - where - K: Into>, - V: IntoAttributeValue, - { + fn attr( + self, + name: impl Into>, + value: impl IntoAttributeValue, + ) -> Attr { Attr { element: self, name: name.into(), @@ -67,11 +68,11 @@ where } } - fn class(self, class: V) -> Attr - where - V: IntoAttributeValue, - { - self.attr("class", class) + // TODO should some methods extend some properties automatically, + // instead of overwriting the (possibly set) inner value + // or should there be (extra) "modifier" methods like `add_class` and/or `remove_class` + fn class(self, class: impl Into>) -> Attr { + self.attr("class", AttributeValue::String(class.into())) } // event list from diff --git a/crates/xilem_html/src/dom/interfaces/event_target.rs b/crates/xilem_html/src/dom/interfaces/event_target.rs index de26c997a..6c8f50782 100644 --- a/crates/xilem_html/src/dom/interfaces/event_target.rs +++ b/crates/xilem_html/src/dom/interfaces/event_target.rs @@ -8,8 +8,8 @@ use crate::{ OptionalAction, }; -pub trait EventTarget { - fn on( +pub trait EventTarget { + fn on( self, event: impl Into>, handler: EH, @@ -23,7 +23,7 @@ pub trait EventTarget { EventListener::new(self, event, handler) } - fn on_with_options( + fn on_with_options( self, event: impl Into>, handler: EH, @@ -39,5 +39,5 @@ pub trait EventTarget { } } -impl EventTarget for Attr {} -impl EventTarget for EventListener {} +impl> EventTarget for Attr {} +impl, Ev, F> EventTarget for EventListener {} diff --git a/crates/xilem_html/src/dom/interfaces/node.rs b/crates/xilem_html/src/dom/interfaces/node.rs index bf68af1d8..4941309c5 100644 --- a/crates/xilem_html/src/dom/interfaces/node.rs +++ b/crates/xilem_html/src/dom/interfaces/node.rs @@ -2,17 +2,17 @@ use crate::dom::{attribute::Attr, event::EventListener}; use super::EventTarget; -pub trait Node: EventTarget { +pub trait Node: EventTarget { fn node_name(&self) -> &str; } -impl Node for Attr { +impl> Node for Attr { fn node_name(&self) -> &str { self.element.node_name() } } -impl Node for EventListener { +impl, Ev, F> Node for EventListener { fn node_name(&self) -> &str { self.element.node_name() } diff --git a/crates/xilem_html/src/event/mod.rs b/crates/xilem_html/src/event/mod.rs index abe78f273..c50ad21fe 100644 --- a/crates/xilem_html/src/event/mod.rs +++ b/crates/xilem_html/src/event/mod.rs @@ -161,6 +161,7 @@ where /// Get the event target element. /// /// Because this type knows its child view's element type, we can downcast to this type here. + // TODO this is actually not safe, an event with the same name could bubble up to the element and this will throw an exception pub fn target(&self) -> El { let evt: &web_sys::Event = self.raw.as_ref(); evt.target().unwrap_throw().dyn_into().unwrap_throw() diff --git a/crates/xilem_html/web_examples/counter/src/main.rs b/crates/xilem_html/web_examples/counter/src/main.rs index f2025cf68..37963d58f 100644 --- a/crates/xilem_html/web_examples/counter/src/main.rs +++ b/crates/xilem_html/web_examples/counter/src/main.rs @@ -42,10 +42,10 @@ impl AppState { } /// You can create functions that generate views. -fn btn( +fn btn( label: &'static str, click_fn: impl Fn(&mut AppState, web_sys::MouseEvent), -) -> impl HtmlButtonElement { +) -> impl HtmlButtonElement { el::button(label).on_click(click_fn) } diff --git a/crates/xilem_html/web_examples/todomvc/src/main.rs b/crates/xilem_html/web_examples/todomvc/src/main.rs index 5b4d59760..8b40c1e75 100644 --- a/crates/xilem_html/web_examples/todomvc/src/main.rs +++ b/crates/xilem_html/web_examples/todomvc/src/main.rs @@ -7,10 +7,15 @@ use xilem_html::{ dom::{ elements::{self as el}, event::EventListenerOptions, - interfaces::{Element, HtmlButtonElement}, + interfaces::*, }, - events::on_click, - get_element_by_id, Action, Adapt, App, MessageResult, View, + // events::on_click, + get_element_by_id, + Action, + Adapt, + App, + MessageResult, + View, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce @@ -90,7 +95,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element 0).then(|| { - on_click( + Element::on_click( el::button("Clear completed").attr("class", "clear-completed"), |state: &mut AppState, _| { state.todos.retain(|todo| !todo.completed); @@ -107,7 +112,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element impl Element impl Element Date: Tue, 29 Aug 2023 23:19:12 +0200 Subject: [PATCH 5/6] xilem_html: Cleanup everything * Remove old element views and events * Repurpose `counter_untyped` example to `counter_custom_element` * Make typing the sensible default (but DOM interfaces will be able to be toggled via feature flags in the future) * Make the `EventTarget` trait a View, so that every DOM interface, including Node has to be a `View` (whether this makes sense for `EventTarget` is an open question) --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/xilem_html/Cargo.toml | 88 ++++-- crates/xilem_html/src/{dom => }/attribute.rs | 6 +- .../src/{element => }/attribute_value.rs | 0 crates/xilem_html/src/class.rs | 75 ----- crates/xilem_html/src/context.rs | 28 +- crates/xilem_html/src/dom/interfaces/node.rs | 19 -- crates/xilem_html/src/element/elements.rs | 232 --------------- crates/xilem_html/src/element/mod.rs | 274 ------------------ .../src/{dom => }/elements/generated.rs | 13 +- .../xilem_html/src/{dom => }/elements/mod.rs | 6 +- crates/xilem_html/src/{dom => }/event.rs | 3 +- crates/xilem_html/src/event/events.rs | 273 ----------------- crates/xilem_html/src/event/mod.rs | 212 -------------- .../src/{dom => }/interfaces/element.rs | 7 +- .../src/{dom => }/interfaces/event_target.rs | 22 +- .../src/{dom => }/interfaces/generated.rs | 5 +- .../src/{dom => }/interfaces/mod.rs | 0 crates/xilem_html/src/interfaces/node.rs | 27 ++ crates/xilem_html/src/lib.rs | 21 +- crates/xilem_html/src/{dom => }/mod.rs | 0 crates/xilem_html/src/optional_action.rs | 35 +++ crates/xilem_html/src/view.rs | 12 - crates/xilem_html/src/view_ext.rs | 68 +---- .../web_examples/counter/src/main.rs | 7 +- .../Cargo.toml | 4 +- .../index.html | 0 .../src/main.rs | 29 +- .../web_examples/todomvc/src/main.rs | 14 +- 30 files changed, 222 insertions(+), 1262 deletions(-) rename crates/xilem_html/src/{dom => }/attribute.rs (89%) rename crates/xilem_html/src/{element => }/attribute_value.rs (100%) delete mode 100644 crates/xilem_html/src/class.rs delete mode 100644 crates/xilem_html/src/dom/interfaces/node.rs delete mode 100644 crates/xilem_html/src/element/elements.rs delete mode 100644 crates/xilem_html/src/element/mod.rs rename crates/xilem_html/src/{dom => }/elements/generated.rs (96%) rename crates/xilem_html/src/{dom => }/elements/mod.rs (94%) rename crates/xilem_html/src/{dom => }/event.rs (96%) delete mode 100644 crates/xilem_html/src/event/events.rs delete mode 100644 crates/xilem_html/src/event/mod.rs rename crates/xilem_html/src/{dom => }/interfaces/element.rs (97%) rename crates/xilem_html/src/{dom => }/interfaces/event_target.rs (57%) rename crates/xilem_html/src/{dom => }/interfaces/generated.rs (97%) rename crates/xilem_html/src/{dom => }/interfaces/mod.rs (100%) create mode 100644 crates/xilem_html/src/interfaces/node.rs rename crates/xilem_html/src/{dom => }/mod.rs (100%) create mode 100644 crates/xilem_html/src/optional_action.rs rename crates/xilem_html/web_examples/{counter_untyped => counter_custom_element}/Cargo.toml (70%) rename crates/xilem_html/web_examples/{counter_untyped => counter_custom_element}/index.html (100%) rename crates/xilem_html/web_examples/{counter_untyped => counter_custom_element}/src/main.rs (56%) diff --git a/Cargo.lock b/Cargo.lock index 7cbeddd46..f575723ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,7 +576,7 @@ dependencies = [ ] [[package]] -name = "counter_untyped" +name = "counter_custom_element" version = "0.1.0" dependencies = [ "console_error_panic_hook", diff --git a/Cargo.toml b/Cargo.toml index c2ae73f90..4e76be913 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [ "crates/xilem_svg", "crates/xilem_html", "crates/xilem_html/web_examples/counter", - "crates/xilem_html/web_examples/counter_untyped", + "crates/xilem_html/web_examples/counter_custom_element", "crates/xilem_html/web_examples/todomvc", ] diff --git a/crates/xilem_html/Cargo.toml b/crates/xilem_html/Cargo.toml index c0c42baf6..595977f36 100644 --- a/crates/xilem_html/Cargo.toml +++ b/crates/xilem_html/Cargo.toml @@ -10,31 +10,6 @@ edition.workspace = true homepage.workspace = true repository.workspace = true -[features] -default = ["typed"] -typed = [ - "web-sys/FocusEvent", "web-sys/HtmlAnchorElement", "web-sys/HtmlAreaElement", - "web-sys/HtmlAudioElement", "web-sys/HtmlBrElement", "web-sys/HtmlButtonElement", - "web-sys/HtmlCanvasElement", "web-sys/HtmlDataElement", "web-sys/HtmlDataListElement", - "web-sys/HtmlDetailsElement", "web-sys/HtmlDialogElement", "web-sys/HtmlDivElement", - "web-sys/HtmlDListElement", "web-sys/HtmlEmbedElement", "web-sys/HtmlFieldSetElement", - "web-sys/HtmlFormElement", "web-sys/HtmlHeadingElement", "web-sys/HtmlHrElement", - "web-sys/HtmlIFrameElement", "web-sys/HtmlImageElement", "web-sys/HtmlInputElement", - "web-sys/HtmlLabelElement", "web-sys/HtmlLegendElement", "web-sys/HtmlLiElement", - "web-sys/HtmlMapElement", "web-sys/HtmlMenuElement", "web-sys/HtmlMeterElement", - "web-sys/HtmlModElement", "web-sys/HtmlObjectElement", "web-sys/HtmlOListElement", - "web-sys/HtmlOptGroupElement", "web-sys/HtmlOptionElement", "web-sys/HtmlOutputElement", - "web-sys/HtmlParagraphElement", "web-sys/HtmlPictureElement", "web-sys/HtmlPreElement", - "web-sys/HtmlProgressElement", "web-sys/HtmlQuoteElement", "web-sys/HtmlScriptElement", - "web-sys/HtmlSelectElement", "web-sys/HtmlSlotElement", "web-sys/HtmlSourceElement", - "web-sys/HtmlSpanElement", "web-sys/HtmlTableElement", "web-sys/HtmlTableCellElement", - "web-sys/HtmlTableColElement", "web-sys/HtmlTableCaptionElement", "web-sys/HtmlTableRowElement", - "web-sys/HtmlTableSectionElement", "web-sys/HtmlTemplateElement", "web-sys/HtmlTextAreaElement", - "web-sys/HtmlTimeElement", "web-sys/HtmlTrackElement", "web-sys/HtmlUListElement", - "web-sys/HtmlVideoElement", "web-sys/InputEvent", "web-sys/KeyboardEvent", "web-sys/MouseEvent", - "web-sys/PointerEvent", "web-sys/WheelEvent", -] - [dependencies] xilem_core.workspace = true kurbo.workspace = true @@ -43,6 +18,9 @@ wasm-bindgen = "0.2.87" log = "0.4.19" gloo = { version = "0.8.1", default-features = false, features = ["events", "utils"] } +# TODO make all or most of the extra elements and events optional via a feature +# I think that at least HtmlElement + SvgElement (and the parent dom interfaces) should be enabled by default +# possibly all web_sys elements, but that needs careful benching/investigation etc. [dependencies.web-sys] version = "0.3.4" features = [ @@ -56,4 +34,64 @@ features = [ "SvgElement", "Text", "Window", + "FocusEvent", + "HtmlAnchorElement", + "HtmlAreaElement", + "HtmlAudioElement", + "HtmlBrElement", + "HtmlButtonElement", + "HtmlCanvasElement", + "HtmlDataElement", + "HtmlDataListElement", + "HtmlDetailsElement", + "HtmlDialogElement", + "HtmlDivElement", + "HtmlDListElement", + "HtmlEmbedElement", + "HtmlFieldSetElement", + "HtmlFormElement", + "HtmlHeadingElement", + "HtmlHrElement", + "HtmlIFrameElement", + "HtmlImageElement", + "HtmlInputElement", + "HtmlLabelElement", + "HtmlLegendElement", + "HtmlLiElement", + "HtmlMapElement", + "HtmlMenuElement", + "HtmlMeterElement", + "HtmlModElement", + "HtmlObjectElement", + "HtmlOListElement", + "HtmlOptGroupElement", + "HtmlOptionElement", + "HtmlOutputElement", + "HtmlParagraphElement", + "HtmlPictureElement", + "HtmlPreElement", + "HtmlProgressElement", + "HtmlQuoteElement", + "HtmlScriptElement", + "HtmlSelectElement", + "HtmlSlotElement", + "HtmlSourceElement", + "HtmlSpanElement", + "HtmlTableElement", + "HtmlTableCellElement", + "HtmlTableColElement", + "HtmlTableCaptionElement", + "HtmlTableRowElement", + "HtmlTableSectionElement", + "HtmlTemplateElement", + "HtmlTextAreaElement", + "HtmlTimeElement", + "HtmlTrackElement", + "HtmlUListElement", + "HtmlVideoElement", + "InputEvent", + "KeyboardEvent", + "MouseEvent", + "PointerEvent", + "WheelEvent" ] diff --git a/crates/xilem_html/src/dom/attribute.rs b/crates/xilem_html/src/attribute.rs similarity index 89% rename from crates/xilem_html/src/dom/attribute.rs rename to crates/xilem_html/src/attribute.rs index b92524324..25b88aac2 100644 --- a/crates/xilem_html/src/dom/attribute.rs +++ b/crates/xilem_html/src/attribute.rs @@ -25,8 +25,10 @@ impl> View for Attr { let _ = element .as_node_ref() .dyn_ref::() - // TODO remove the unwrap, make this safer... - .unwrap() + .expect( + "The Attr view can only be used by Views,\ + whose element inherits from `web_sys::Element`", + ) .set_attribute(&self.name, &value.serialize()); } (id, state, element) diff --git a/crates/xilem_html/src/element/attribute_value.rs b/crates/xilem_html/src/attribute_value.rs similarity index 100% rename from crates/xilem_html/src/element/attribute_value.rs rename to crates/xilem_html/src/attribute_value.rs diff --git a/crates/xilem_html/src/class.rs b/crates/xilem_html/src/class.rs deleted file mode 100644 index 1513c35ac..000000000 --- a/crates/xilem_html/src/class.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2023 the Druid Authors. -// SPDX-License-Identifier: Apache-2.0 - -use std::{any::Any, borrow::Cow}; - -use xilem_core::{Id, MessageResult}; - -use crate::{ - context::{ChangeFlags, Cx}, - view::{DomElement, View, ViewMarker}, -}; - -pub struct Class { - child: V, - // This could reasonably be static Cow also, but keep things simple - class: Cow<'static, str>, -} - -pub fn class(child: V, class: impl Into>) -> Class { - Class { - child, - class: class.into(), - } -} - -impl ViewMarker for Class {} - -// TODO: make generic over A (probably requires Phantom) -impl View for Class -where - V: View, - V::Element: DomElement, -{ - type State = V::State; - type Element = V::Element; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (id, child_state, element) = self.child.build(cx); - element - .as_element_ref() - .set_attribute("class", &self.class) - .unwrap(); - (id, child_state, element) - } - - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut V::Element, - ) -> ChangeFlags { - let prev_id = *id; - let mut changed = self.child.rebuild(cx, &prev.child, id, state, element); - if self.class != prev.class || prev_id != *id { - element - .as_element_ref() - .set_attribute("class", &self.class) - .unwrap(); - changed.insert(ChangeFlags::OTHER_CHANGE); - } - changed - } - - fn message( - &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult<()> { - self.child.message(id_path, state, message, app_state) - } -} diff --git a/crates/xilem_html/src/context.rs b/crates/xilem_html/src/context.rs index d0ab8818b..c40e6959f 100644 --- a/crates/xilem_html/src/context.rs +++ b/crates/xilem_html/src/context.rs @@ -1,7 +1,7 @@ use std::any::Any; use bitflags::bitflags; -use wasm_bindgen::JsCast; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; use web_sys::Document; use xilem_core::{Id, IdPath}; @@ -9,13 +9,37 @@ use xilem_core::{Id, IdPath}; use crate::{ app::AppRunner, diff::{diff_kv_iterables, Diff}, - element::{remove_attribute, set_attribute}, vecmap::VecMap, AttributeValue, Message, HTML_NS, SVG_NS, }; type CowStr = std::borrow::Cow<'static, str>; +fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { + // we have to special-case `value` because setting the value using `set_attribute` + // doesn't work after the value has been changed. + if name == "value" { + let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); + element.set_value(value) + } else if name == "checked" { + let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); + element.set_checked(true) + } else { + element.set_attribute(name, value).unwrap_throw(); + } +} + +fn remove_attribute(element: &web_sys::Element, name: &str) { + // we have to special-case `value` because setting the value using `set_attribute` + // doesn't work after the value has been changed. + if name == "checked" { + let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); + element.set_checked(false) + } else { + element.remove_attribute(name).unwrap_throw(); + } +} + // Note: xilem has derive Clone here. Not sure. pub struct Cx { id_path: IdPath, diff --git a/crates/xilem_html/src/dom/interfaces/node.rs b/crates/xilem_html/src/dom/interfaces/node.rs deleted file mode 100644 index 4941309c5..000000000 --- a/crates/xilem_html/src/dom/interfaces/node.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::dom::{attribute::Attr, event::EventListener}; - -use super::EventTarget; - -pub trait Node: EventTarget { - fn node_name(&self) -> &str; -} - -impl> Node for Attr { - fn node_name(&self) -> &str { - self.element.node_name() - } -} - -impl, Ev, F> Node for EventListener { - fn node_name(&self) -> &str { - self.element.node_name() - } -} diff --git a/crates/xilem_html/src/element/elements.rs b/crates/xilem_html/src/element/elements.rs deleted file mode 100644 index 4675cc9db..000000000 --- a/crates/xilem_html/src/element/elements.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Types that wrap [`Element`][super::Element] and represent specific element types. -//! -macro_rules! elements { - () => {}; - (($ty_name:ident, $name:ident, $web_sys_ty:ty), $($rest:tt)*) => { - element!($ty_name, $name, $web_sys_ty); - elements!($($rest)*); - }; -} - -macro_rules! element { - ($ty_name:ident, $name:ident, $web_sys_ty:ty) => { - /// A view representing a - #[doc = concat!("`", stringify!($name), "`")] - /// element. - pub struct $ty_name(crate::Element<$web_sys_ty, ViewSeq>); - - /// Builder function for a - #[doc = concat!("`", stringify!($name), "`")] - /// view. - pub fn $name(children: ViewSeq) -> $ty_name { - $ty_name(crate::element(stringify!($name), children)) - } - - impl $ty_name { - /// Set an attribute on this element. - /// - /// # Panics - /// - /// If the name contains characters that are not valid in an attribute name, - /// then the `View::build`/`View::rebuild` functions will panic for this view. - pub fn attr( - mut self, - name: impl Into>, - value: impl crate::IntoAttributeValue, - ) -> Self { - self.0.set_attr(name, value); - self - } - - /// Set an attribute on this element. - /// - /// # Panics - /// - /// If the name contains characters that are not valid in an attribute name, - /// then the `View::build`/`View::rebuild` functions will panic for this view. - pub fn set_attr( - &mut self, - name: impl Into>, - value: impl crate::IntoAttributeValue, - ) -> &mut Self { - self.0.set_attr(name, value); - self - } - - pub fn remove_attr(&mut self, name: &str) -> &mut Self { - self.0.remove_attr(name); - self - } - - pub fn after_update(mut self, after_update: impl Fn(&$web_sys_ty) + 'static) -> Self { - self.0 = self.0.after_update(after_update); - self - } - } - - impl crate::view::ViewMarker for $ty_name {} - - impl crate::view::View for $ty_name - where - ViewSeq: crate::view::ViewSequence, - { - type State = crate::ElementState; - type Element = $web_sys_ty; - - fn build( - &self, - cx: &mut crate::context::Cx, - ) -> (xilem_core::Id, Self::State, Self::Element) { - self.0.build(cx) - } - - fn rebuild( - &self, - cx: &mut crate::context::Cx, - prev: &Self, - id: &mut xilem_core::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> crate::ChangeFlags { - self.0.rebuild(cx, &prev.0, id, state, element) - } - - fn message( - &self, - id_path: &[xilem_core::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T_, - ) -> xilem_core::MessageResult { - self.0.message(id_path, state, message, app_state) - } - } - }; -} - -// void elements (those without children) are `area`, `base`, `br`, `col`, -// `embed`, `hr`, `img`, `input`, `link`, `meta`, `source`, `track`, `wbr` -elements!( - // the order is copied from - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element - // DOM interfaces copied from https://html.spec.whatwg.org/multipage/grouping-content.html and friends - - // content sectioning - (Address, address, web_sys::HtmlElement), - (Article, article, web_sys::HtmlElement), - (Aside, aside, web_sys::HtmlElement), - (Footer, footer, web_sys::HtmlElement), - (Header, header, web_sys::HtmlElement), - (H1, h1, web_sys::HtmlHeadingElement), - (H2, h2, web_sys::HtmlHeadingElement), - (H3, h3, web_sys::HtmlHeadingElement), - (H4, h4, web_sys::HtmlHeadingElement), - (H5, h5, web_sys::HtmlHeadingElement), - (H6, h6, web_sys::HtmlHeadingElement), - (Hgroup, hgroup, web_sys::HtmlElement), - (Main, main, web_sys::HtmlElement), - (Nav, nav, web_sys::HtmlElement), - (Section, section, web_sys::HtmlElement), - // text content - (Blockquote, blockquote, web_sys::HtmlQuoteElement), - (Dd, dd, web_sys::HtmlElement), - (Div, div, web_sys::HtmlDivElement), - (Dl, dl, web_sys::HtmlDListElement), - (Dt, dt, web_sys::HtmlElement), - (Figcaption, figcaption, web_sys::HtmlElement), - (Figure, figure, web_sys::HtmlElement), - (Hr, hr, web_sys::HtmlHrElement), - (Li, li, web_sys::HtmlLiElement), - (Menu, menu, web_sys::HtmlMenuElement), - (Ol, ol, web_sys::HtmlOListElement), - (P, p, web_sys::HtmlParagraphElement), - (Pre, pre, web_sys::HtmlPreElement), - (Ul, ul, web_sys::HtmlUListElement), - // inline text - (A, a, web_sys::HtmlAnchorElement), - (Abbr, abbr, web_sys::HtmlElement), - (B, b, web_sys::HtmlElement), - (Bdi, bdi, web_sys::HtmlElement), - (Bdo, bdo, web_sys::HtmlElement), - (Br, br, web_sys::HtmlBrElement), - (Cite, cite, web_sys::HtmlElement), - (Code, code, web_sys::HtmlElement), - (Data, data, web_sys::HtmlDataElement), - (Dfn, dfn, web_sys::HtmlElement), - (Em, em, web_sys::HtmlElement), - (I, i, web_sys::HtmlElement), - (Kbd, kbd, web_sys::HtmlElement), - (Mark, mark, web_sys::HtmlElement), - (Q, q, web_sys::HtmlQuoteElement), - (Rp, rp, web_sys::HtmlElement), - (Rt, rt, web_sys::HtmlElement), - (Ruby, ruby, web_sys::HtmlElement), - (S, s, web_sys::HtmlElement), - (Samp, samp, web_sys::HtmlElement), - (Small, small, web_sys::HtmlElement), - (Span, span, web_sys::HtmlSpanElement), - (Strong, strong, web_sys::HtmlElement), - (Sub, sub, web_sys::HtmlElement), - (Sup, sup, web_sys::HtmlElement), - (Time, time, web_sys::HtmlTimeElement), - (U, u, web_sys::HtmlElement), - (Var, var, web_sys::HtmlElement), - (Wbr, wbr, web_sys::HtmlElement), - // image and multimedia - (Area, area, web_sys::HtmlAreaElement), - (Audio, audio, web_sys::HtmlAudioElement), - (Img, img, web_sys::HtmlImageElement), - (Map, map, web_sys::HtmlMapElement), - (Track, track, web_sys::HtmlTrackElement), - (Video, video, web_sys::HtmlVideoElement), - // embedded content - (Embed, embed, web_sys::HtmlEmbedElement), - (Iframe, iframe, web_sys::HtmlIFrameElement), - (Object, object, web_sys::HtmlObjectElement), - (Picture, picture, web_sys::HtmlPictureElement), - (Portal, portal, web_sys::HtmlElement), - (Source, source, web_sys::HtmlSourceElement), - // SVG and MathML (TODO, svg and mathml elements) - (Svg, svg, web_sys::HtmlElement), - (Math, math, web_sys::HtmlElement), - // scripting - (Canvas, canvas, web_sys::HtmlCanvasElement), - (Noscript, noscript, web_sys::HtmlElement), - (Script, script, web_sys::HtmlScriptElement), - // demarcating edits - (Del, del, web_sys::HtmlModElement), - (Ins, ins, web_sys::HtmlModElement), - // tables - (Caption, caption, web_sys::HtmlTableCaptionElement), - (Col, col, web_sys::HtmlTableColElement), - (Colgroup, colgroup, web_sys::HtmlTableColElement), - (Table, table, web_sys::HtmlTableSectionElement), - (Tbody, tbody, web_sys::HtmlTableSectionElement), - (Td, td, web_sys::HtmlTableCellElement), - (Tfoot, tfoot, web_sys::HtmlTableSectionElement), - (Th, th, web_sys::HtmlTableCellElement), - (Thead, thead, web_sys::HtmlTableSectionElement), - (Tr, tr, web_sys::HtmlTableRowElement), - // forms - (Button, button, web_sys::HtmlButtonElement), - (Datalist, datalist, web_sys::HtmlDataListElement), - (Fieldset, fieldset, web_sys::HtmlFieldSetElement), - (Form, form, web_sys::HtmlFormElement), - (Input, input, web_sys::HtmlInputElement), - (Label, label, web_sys::HtmlLabelElement), - (Legend, legend, web_sys::HtmlLegendElement), - (Meter, meter, web_sys::HtmlMeterElement), - (Optgroup, optgroup, web_sys::HtmlOptGroupElement), - (Option, option, web_sys::HtmlOptionElement), - (Output, output, web_sys::HtmlOutputElement), - (Progress, progress, web_sys::HtmlProgressElement), - (Select, select, web_sys::HtmlSelectElement), - (Textarea, textarea, web_sys::HtmlTextAreaElement), - // interactive elements, - (Details, details, web_sys::HtmlDetailsElement), - (Dialog, dialog, web_sys::HtmlDialogElement), - (Summary, summary, web_sys::HtmlElement), - // web components, - (Slot, slot, web_sys::HtmlSlotElement), - (Template, template, web_sys::HtmlTemplateElement), -); diff --git a/crates/xilem_html/src/element/mod.rs b/crates/xilem_html/src/element/mod.rs deleted file mode 100644 index 4a4311c19..000000000 --- a/crates/xilem_html/src/element/mod.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! The HTML element view and associated types/functions. -//! -//! If you are writing your own views, we recommend adding -//! `use xilem_html::elements as el` or similar to the top of your file. -use crate::{ - context::{ChangeFlags, Cx}, - diff::{diff_kv_iterables, Diff}, - vecmap::VecMap, - view::{DomElement, Pod, View, ViewMarker, ViewSequence}, -}; - -use std::{borrow::Cow, fmt}; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use xilem_core::{Id, MessageResult, VecSplice}; - -mod attribute_value; -#[cfg(feature = "typed")] -pub mod elements; - -pub use attribute_value::{AttributeValue, IntoAttributeValue}; - -type CowStr = Cow<'static, str>; - -/// A view representing a HTML element. -/// -/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`). -pub struct Element { - name: CowStr, - attributes: VecMap, - children: Children, - #[allow(clippy::type_complexity)] - after_update: Option>, -} - -impl Element { - pub fn debug_as_el(&self) -> impl fmt::Debug + '_ { - struct DebugFmt<'a, El, VS>(&'a Element); - impl<'a, El, VS> fmt::Debug for DebugFmt<'a, El, VS> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "<{}", self.0.name)?; - for (name, value) in &self.0.attributes { - write!(f, " {name}=\"{}\"", value.serialize())?; - } - write!(f, ">") - } - } - DebugFmt(self) - } -} - -/// The state associated with a HTML element `View`. -/// -/// Stores handles to the child elements and any child state. -pub struct ElementState { - child_states: ViewSeqState, - child_elements: Vec, - scratch: Vec, -} - -/// Create a new element view -/// -/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`). -pub fn element(name: impl Into, children: ViewSeq) -> Element { - Element { - name: name.into(), - attributes: Default::default(), - children, - after_update: None, - } -} - -impl Element { - /// Set an attribute on this element. - /// - /// # Panics - /// - /// If the name contains characters that are not valid in an attribute name, - /// then the `View::build`/`View::rebuild` functions will panic for this view. - pub fn attr(mut self, name: impl Into, value: impl IntoAttributeValue) -> Self { - self.set_attr(name, value); - self - } - - /// Set an attribute on this element. - /// - /// # Panics - /// - /// If the name contains characters that are not valid in an attribute name, - /// then the `View::build`/`View::rebuild` functions will panic for this view. - pub fn set_attr(&mut self, name: impl Into, value: impl IntoAttributeValue) { - let name = name.into(); - if let Some(value) = value.into_attribute_value() { - self.attributes.insert(name, value); - } else { - self.remove_attr(&name); - } - } - - pub fn remove_attr(&mut self, name: &str) { - self.attributes.remove(name); - } - - /// Set a function to run after the new view tree has been created. - /// - /// This offers functionality similar to `ref` in React. - /// - /// # Rules for correct use - /// - /// It is important that the structure of the DOM tree is *not* modified using this function. - /// If the DOM tree is modified, then future reconciliation will have undefined and possibly - /// suprising results. - pub fn after_update(mut self, after_update: impl Fn(&El) + 'static) -> Self { - self.after_update = Some(Box::new(after_update)); - self - } -} - -impl ViewMarker for Element {} - -impl View for Element -where - Children: ViewSequence, - El: JsCast + DomElement, -{ - type State = ElementState; - type Element = El; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let el = cx.create_html_element(&self.name); - for (name, value) in &self.attributes { - el.set_attribute(name, &value.serialize()).unwrap_throw(); - } - - let mut child_elements = vec![]; - let (id, child_states) = cx.with_new_id(|cx| self.children.build(cx, &mut child_elements)); - for child in &child_elements { - el.append_child(child.0.as_node_ref()).unwrap_throw(); - } - - // Set the id used internally to the `data-debugid` attribute. - // This allows the user to see if an element has been re-created or only altered. - #[cfg(debug_assertions)] - el.set_attribute("data-debugid", &id.to_raw().to_string()) - .unwrap_throw(); - - let el = el.dyn_into().unwrap_throw(); - if let Some(after_update) = &self.after_update { - (after_update)(&el); - } - let state = ElementState { - child_states, - child_elements, - scratch: vec![], - }; - (id, state, el) - } - - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - let mut changed = ChangeFlags::empty(); - // update tag name - if prev.name != self.name { - // recreate element - let parent = element - .as_element_ref() - .parent_element() - .expect_throw("this element was mounted and so should have a parent"); - parent.remove_child(element.as_node_ref()).unwrap_throw(); - let new_element = cx.create_html_element(&self.name); - // TODO could this be combined with child updates? - while element.as_element_ref().child_element_count() > 0 { - new_element - .append_child(&element.as_element_ref().child_nodes().get(0).unwrap_throw()) - .unwrap_throw(); - } - *element = new_element.dyn_into().unwrap_throw(); - changed |= ChangeFlags::STRUCTURE; - } - - let element = element.as_element_ref(); - - // update attributes - for itm in diff_kv_iterables(&prev.attributes, &self.attributes) { - match itm { - Diff::Add(name, value) | Diff::Change(name, value) => { - set_attribute(element, name, &value.serialize()); - changed |= ChangeFlags::OTHER_CHANGE; - } - Diff::Remove(name) => { - remove_attribute(element, name); - changed |= ChangeFlags::OTHER_CHANGE; - } - } - } - - // update children - let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); - changed |= cx.with_id(*id, |cx| { - self.children - .rebuild(cx, &prev.children, &mut state.child_states, &mut splice) - }); - if changed.contains(ChangeFlags::STRUCTURE) { - // This is crude and will result in more DOM traffic than needed. - // The right thing to do is diff the new state of the children id - // vector against the old, and derive DOM mutations from that. - while let Some(child) = element.first_child() { - element.remove_child(&child).unwrap_throw(); - } - for child in &state.child_elements { - element.append_child(child.0.as_node_ref()).unwrap_throw(); - } - changed.remove(ChangeFlags::STRUCTURE); - } - if let Some(after_update) = &self.after_update { - (after_update)(element.dyn_ref().unwrap_throw()); - changed |= ChangeFlags::OTHER_CHANGE; - } - changed - } - - fn message( - &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { - self.children - .message(id_path, &mut state.child_states, message, app_state) - } -} - -#[cfg(feature = "typed")] -pub(crate) fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { - // we have to special-case `value` because setting the value using `set_attribute` - // doesn't work after the value has been changed. - if name == "value" { - let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); - element.set_value(value) - } else if name == "checked" { - let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); - element.set_checked(true) - } else { - element.set_attribute(name, value).unwrap_throw(); - } -} - -#[cfg(not(feature = "typed"))] -pub(crate) fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { - element.set_attribute(name, value).unwrap_throw(); -} - -#[cfg(feature = "typed")] -pub(crate) fn remove_attribute(element: &web_sys::Element, name: &str) { - // we have to special-case `value` because setting the value using `set_attribute` - // doesn't work after the value has been changed. - if name == "checked" { - let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); - element.set_checked(false) - } else { - element.remove_attribute(name).unwrap_throw(); - } -} - -#[cfg(not(feature = "typed"))] -pub(crate) fn remove_attribute(element: &web_sys::Element, name: &str) { - element.remove_attribute(name).unwrap_throw(); -} diff --git a/crates/xilem_html/src/dom/elements/generated.rs b/crates/xilem_html/src/elements/generated.rs similarity index 96% rename from crates/xilem_html/src/dom/elements/generated.rs rename to crates/xilem_html/src/elements/generated.rs index 12636023f..335c6dca9 100644 --- a/crates/xilem_html/src/dom/elements/generated.rs +++ b/crates/xilem_html/src/elements/generated.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use xilem_core::{Id, MessageResult, VecSplice}; -use crate::{dom::interfaces::Node, ChangeFlags, Cx, View, ViewMarker, ViewSequence}; +use crate::{interfaces::Node, ChangeFlags, Cx, View, ViewMarker, ViewSequence}; use super::ElementState; @@ -12,7 +12,7 @@ macro_rules! generate_dom_interface_impl { generate_dom_interface_impl!($ty_name, $name, $t, $a, $vs, $dom_interface, {}); }; ($ty_name:ident, $name:ident, $t:ident, $a:ident, $vs:ident, $dom_interface:ident, $body: tt) => { - impl<$t, $a, $vs> crate::dom::interfaces::$dom_interface<$t, $a> for $ty_name<$t, $a, $vs> + impl<$t, $a, $vs> crate::interfaces::$dom_interface<$t, $a> for $ty_name<$t, $a, $vs> where $vs: crate::view::ViewSequence<$t, $a>, $body @@ -21,8 +21,13 @@ macro_rules! generate_dom_interface_impl { macro_rules! impl_html_dom_interface { ($ty_name: ident, $name: ident, $t: ident, $a:ident, $vs:ident, Node) => { - impl<$t, $a, $vs> crate::dom::interfaces::EventTarget<$t, $a> for $ty_name<$t, $a, $vs> {} - impl<$t, $a, $vs> crate::dom::interfaces::Node<$t, $a> for $ty_name<$t, $a, $vs> { + impl<$t, $a, $vs: ViewSequence<$t, $a>> crate::interfaces::EventTarget<$t, $a> + for $ty_name<$t, $a, $vs> + { + } + impl<$t, $a, $vs: ViewSequence<$t, $a>> crate::interfaces::Node<$t, $a> + for $ty_name<$t, $a, $vs> + { fn node_name(&self) -> &str { stringify!($name) } diff --git a/crates/xilem_html/src/dom/elements/mod.rs b/crates/xilem_html/src/elements/mod.rs similarity index 94% rename from crates/xilem_html/src/dom/elements/mod.rs rename to crates/xilem_html/src/elements/mod.rs index 325649a35..f47e7fb6e 100644 --- a/crates/xilem_html/src/dom/elements/mod.rs +++ b/crates/xilem_html/src/elements/mod.rs @@ -25,6 +25,8 @@ pub struct ElementState { pub(crate) scratch: Vec, } +// TODO something like the `after_update` of the former `Element` view (likely as a wrapper view instead) + pub struct CustomElement { name: CowStr, children: Children, @@ -146,9 +148,9 @@ where } } -impl EventTarget for CustomElement {} +impl> EventTarget for CustomElement {} -impl Node for CustomElement { +impl> Node for CustomElement { fn node_name(&self) -> &str { &self.name } diff --git a/crates/xilem_html/src/dom/event.rs b/crates/xilem_html/src/event.rs similarity index 96% rename from crates/xilem_html/src/dom/event.rs rename to crates/xilem_html/src/event.rs index 8a8275a71..aa724e165 100644 --- a/crates/xilem_html/src/dom/event.rs +++ b/crates/xilem_html/src/event.rs @@ -8,8 +8,7 @@ use crate::{view::DomNode, ChangeFlags, Cx, OptionalAction, View, ViewMarker}; /// Wraps a [`View`] `V` and attaches an event listener. /// -/// The event type `E` contains both the [`web_sys::Event`] subclass for this event and the -/// [`web_sys::HtmlElement`] subclass that matches `V::Element`. +/// The event type `E` should inherit from [`web_sys::Event`] pub struct EventListener { pub(crate) element: V, pub(crate) event: Cow<'static, str>, diff --git a/crates/xilem_html/src/event/events.rs b/crates/xilem_html/src/event/events.rs deleted file mode 100644 index 933e25da4..000000000 --- a/crates/xilem_html/src/event/events.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! Macros to generate all the different html events -//! -macro_rules! events { - () => {}; - (($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty), $($rest:tt)*) => { - event!($ty_name, $builder_name, $name, $web_sys_ty); - events!($($rest)*); - }; -} - -macro_rules! event { - ($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => { - /// A view that listens for the - #[doc = concat!("`", $name, "`")] - /// event. - pub struct $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - inner: crate::OnEvent<$web_sys_ty, V, F>, - data: std::marker::PhantomData, - action: std::marker::PhantomData, - optional_action: std::marker::PhantomData, - } - - impl $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - /// Whether the event handler should be passive. (default = `true`) - /// - /// Passive event handlers can't prevent the browser's default action from - /// running (otherwise possible with `event.prevent_default()`), which - /// restricts what they can be used for, but reduces overhead. - pub fn passive(mut self, value: bool) -> Self { - self.inner.passive = value; - self - } - } - - /// Builder for the - #[doc = concat!("`", $name, "`")] - /// event listener. - pub fn $builder_name(child: V, callback: F) -> $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - $ty_name { - inner: crate::on_event($name, child, callback), - data: std::marker::PhantomData, - action: std::marker::PhantomData, - optional_action: std::marker::PhantomData, - } - } - - impl crate::view::ViewMarker for $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - } - - impl crate::view::View for $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - type State = crate::event::OnEventState; - type Element = V::Element; - - fn build( - &self, - cx: &mut crate::context::Cx, - ) -> (xilem_core::Id, Self::State, Self::Element) { - self.inner.build(cx) - } - - fn rebuild( - &self, - cx: &mut crate::context::Cx, - prev: &Self, - id: &mut xilem_core::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> crate::ChangeFlags { - self.inner.rebuild(cx, &prev.inner, id, state, element) - } - - fn message( - &self, - id_path: &[xilem_core::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> xilem_core::MessageResult { - self.inner.message(id_path, state, message, app_state) - } - } - }; -} - -// event list from -// https://html.spec.whatwg.org/multipage/webappapis.html#idl-definitions -// -// I didn't include the events on the window, since we aren't attaching -// any events to the window in xilem_html - -events!( - (OnAbort, on_abort, "abort", web_sys::Event), - (OnAuxClick, on_auxclick, "auxclick", web_sys::PointerEvent), - ( - OnBeforeInput, - on_beforeinput, - "beforeinput", - web_sys::InputEvent - ), - (OnBeforeMatch, on_beforematch, "beforematch", web_sys::Event), - ( - OnBeforeToggle, - on_beforetoggle, - "beforetoggle", - web_sys::Event - ), - (OnBlur, on_blur, "blur", web_sys::FocusEvent), - (OnCancel, on_cancel, "cancel", web_sys::Event), - (OnCanPlay, on_canplay, "canplay", web_sys::Event), - ( - OnCanPlayThrough, - on_canplaythrough, - "canplaythrough", - web_sys::Event - ), - (OnChange, on_change, "change", web_sys::Event), - (OnClick, on_click, "click", web_sys::MouseEvent), - (OnClose, on_close, "close", web_sys::Event), - (OnContextLost, on_contextlost, "contextlost", web_sys::Event), - ( - OnContextMenu, - on_contextmenu, - "contextmenu", - web_sys::PointerEvent - ), - ( - OnContextRestored, - on_contextrestored, - "contextrestored", - web_sys::Event - ), - (OnCopy, on_copy, "copy", web_sys::Event), - (OnCueChange, on_cuechange, "cuechange", web_sys::Event), - (OnCut, on_cut, "cut", web_sys::Event), - (OnDblClick, on_dblclick, "dblclick", web_sys::MouseEvent), - (OnDrag, on_drag, "drag", web_sys::Event), - (OnDragEnd, on_dragend, "dragend", web_sys::Event), - (OnDragEnter, on_dragenter, "dragenter", web_sys::Event), - (OnDragLeave, on_dragleave, "dragleave", web_sys::Event), - (OnDragOver, on_dragover, "dragover", web_sys::Event), - (OnDragStart, on_dragstart, "dragstart", web_sys::Event), - (OnDrop, on_drop, "drop", web_sys::Event), - ( - OnDurationChange, - on_durationchange, - "durationchange", - web_sys::Event - ), - (OnEmptied, on_emptied, "emptied", web_sys::Event), - (OnEnded, on_ended, "ended", web_sys::Event), - (OnError, on_error, "error", web_sys::Event), - (OnFocus, on_focus, "focus", web_sys::FocusEvent), - (OnFocusIn, on_focusin, "focusin", web_sys::FocusEvent), - (OnFocusOut, on_focusout, "focusout", web_sys::FocusEvent), - (OnFormData, on_formdata, "formdata", web_sys::Event), - (OnInput, on_input, "input", web_sys::InputEvent), - (OnInvalid, on_invalid, "invalid", web_sys::Event), - (OnKeyDown, on_keydown, "keydown", web_sys::KeyboardEvent), - (OnKeyUp, on_keyup, "keyup", web_sys::KeyboardEvent), - (OnLoad, on_load, "load", web_sys::Event), - (OnLoadedData, on_loadeddata, "loadeddata", web_sys::Event), - ( - OnLoadedMetadata, - on_loadedmetadata, - "loadedmetadata", - web_sys::Event - ), - (OnLoadStart, on_loadstart, "loadstart", web_sys::Event), - (OnMouseDown, on_mousedown, "mousedown", web_sys::MouseEvent), - ( - OnMouseEnter, - on_mouseenter, - "mouseenter", - web_sys::MouseEvent - ), - ( - OnMouseLeave, - on_mouseleave, - "mouseleave", - web_sys::MouseEvent - ), - (OnMouseMove, on_mousemove, "mousemove", web_sys::MouseEvent), - (OnMouseOut, on_mouseout, "mouseout", web_sys::MouseEvent), - (OnMouseOver, on_mouseover, "mouseover", web_sys::MouseEvent), - (OnMouseUp, on_mouseup, "mouseup", web_sys::MouseEvent), - (OnPaste, on_paste, "paste", web_sys::Event), - (OnPause, on_pause, "pause", web_sys::Event), - (OnPlay, on_play, "play", web_sys::Event), - (OnPlaying, on_playing, "playing", web_sys::Event), - (OnProgress, on_progress, "progress", web_sys::Event), - (OnRateChange, on_ratechange, "ratechange", web_sys::Event), - (OnReset, on_reset, "reset", web_sys::Event), - (OnResize, on_resize, "resize", web_sys::Event), - (OnScroll, on_scroll, "scroll", web_sys::Event), - (OnScrollEnd, on_scrollend, "scrollend", web_sys::Event), - ( - OnSecurityPolicyViolation, - on_securitypolicyviolation, - "securitypolicyviolation", - web_sys::Event - ), - (OnSeeked, on_seeked, "seeked", web_sys::Event), - (OnSeeking, on_seeking, "seeking", web_sys::Event), - (OnSelect, on_select, "select", web_sys::Event), - (OnSlotChange, on_slotchange, "slotchange", web_sys::Event), - (OnStalled, on_stalled, "stalled", web_sys::Event), - (OnSubmit, on_submit, "submit", web_sys::Event), - (OnSuspend, on_suspend, "suspend", web_sys::Event), - (OnTimeUpdate, on_timeupdate, "timeupdate", web_sys::Event), - (OnToggle, on_toggle, "toggle", web_sys::Event), - ( - OnVolumeChange, - on_volumechange, - "volumechange", - web_sys::Event - ), - (OnWaiting, on_waiting, "waiting", web_sys::Event), - ( - OnWebkitAnimationEnd, - on_webkitanimationend, - "webkitanimationend", - web_sys::Event - ), - ( - OnWebkitAnimationIteration, - on_webkitanimationiteration, - "webkitanimationiteration", - web_sys::Event - ), - ( - OnWebkitAnimationStart, - on_webkitanimationstart, - "webkitanimationstart", - web_sys::Event - ), - ( - OnWebkitTransitionEnd, - on_webkittransitionend, - "webkittransitionend", - web_sys::Event - ), - (OnWheel, on_wheel, "wheel", web_sys::WheelEvent), -); diff --git a/crates/xilem_html/src/event/mod.rs b/crates/xilem_html/src/event/mod.rs deleted file mode 100644 index c50ad21fe..000000000 --- a/crates/xilem_html/src/event/mod.rs +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2023 the Druid Authors. -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(feature = "typed")] -pub mod events; - -use std::{any::Any, marker::PhantomData, ops::Deref}; - -use gloo::events::{EventListener, EventListenerOptions}; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use xilem_core::{Id, MessageResult}; - -use crate::{ - context::{ChangeFlags, Cx}, - view::{DomNode, View, ViewMarker}, -}; - -/// Wraps a [`View`] `V` and attaches an event listener. -/// -/// The event type `E` contains both the [`web_sys::Event`] subclass for this event and the -/// [`web_sys::HtmlElement`] subclass that matches `V::Element`. -pub struct OnEvent { - // TODO changing this after creation is unsupported for now, - // please create a new view instead. - event: &'static str, - child: V, - passive: bool, - callback: F, - phantom_event_ty: PhantomData, -} - -impl OnEvent { - fn new(event: &'static str, child: V, callback: F) -> Self { - Self { - event, - child, - callback, - passive: true, - phantom_event_ty: PhantomData, - } - } - - /// Whether the event handler should be passive. (default = `true`) - /// - /// Passive event handlers can't prevent the browser's default action from - /// running (otherwise possible with `event.prevent_default()`), which - /// restricts what they can be used for, but reduces overhead. - pub fn passive(mut self, value: bool) -> Self { - self.passive = value; - self - } -} - -impl ViewMarker for OnEvent {} - -impl View for OnEvent -where - F: Fn(&mut T, &Event) -> OA, - V: View, - E: JsCast + 'static, - V::Element: 'static, - OA: OptionalAction, -{ - type State = OnEventState; - - type Element = V::Element; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (id, child_state, element) = self.child.build(cx); - let thunk = cx.with_id(id, |cx| cx.message_thunk()); - let listener = EventListener::new_with_options( - element.as_node_ref(), - self.event, - EventListenerOptions { - passive: self.passive, - ..Default::default() - }, - move |event: &web_sys::Event| { - let event = (*event).clone().dyn_into::().unwrap_throw(); - let event: Event = Event::new(event); - thunk.push_message(EventMsg { event }); - }, - ); - // TODO add `remove_listener_with_callback` to clean up listener? - let state = OnEventState { - listener, - child_state, - }; - (id, state, element) - } - - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - // TODO: if the child id changes (as can happen with AnyView), reinstall closure - self.child - .rebuild(cx, &prev.child, id, &mut state.child_state, element) - } - - fn message( - &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { - match message.downcast_ref::>>() { - Some(msg) if id_path.is_empty() => { - match (self.callback)(app_state, &msg.event).action() { - Some(a) => MessageResult::Action(a), - None => MessageResult::Nop, - } - } - _ => self - .child - .message(id_path, &mut state.child_state, message, app_state), - } - } -} - -// Attach an event listener to the child's element -pub fn on_event(name: &'static str, child: V, callback: F) -> OnEvent { - OnEvent::new(name, child, callback) -} - -/// State for the `OnEvent` view. -pub struct OnEventState { - #[allow(unused)] - listener: EventListener, - child_state: S, -} -struct EventMsg { - event: E, -} - -/// Wraps a `web_sys::Event` and provides auto downcasting for both the event and its target. -pub struct Event { - raw: Evt, - el: PhantomData, -} - -impl Event { - fn new(raw: Evt) -> Self { - Self { - raw, - el: PhantomData, - } - } -} - -impl Event -where - Evt: AsRef, - El: JsCast, -{ - /// Get the event target element. - /// - /// Because this type knows its child view's element type, we can downcast to this type here. - // TODO this is actually not safe, an event with the same name could bubble up to the element and this will throw an exception - pub fn target(&self) -> El { - let evt: &web_sys::Event = self.raw.as_ref(); - evt.target().unwrap_throw().dyn_into().unwrap_throw() - } -} - -impl Deref for Event { - type Target = Evt; - fn deref(&self) -> &Self::Target { - &self.raw - } -} - -/// Implement this trait for types you want to use as actions. -/// -/// The trait exists because otherwise we couldn't provide versions -/// of listeners that take `()`, `A` and `Option`. -pub trait Action {} - -/// Trait that allows callbacks to be polymorphic on return type -/// (`Action`, `Option` or `()`). An implementation detail. -pub trait OptionalAction: sealed::Sealed { - fn action(self) -> Option; -} -mod sealed { - pub trait Sealed {} -} - -impl sealed::Sealed for () {} -impl OptionalAction for () { - fn action(self) -> Option { - None - } -} - -impl sealed::Sealed for A {} -impl OptionalAction for A { - fn action(self) -> Option { - Some(self) - } -} - -impl sealed::Sealed for Option {} -impl OptionalAction for Option { - fn action(self) -> Option { - self - } -} diff --git a/crates/xilem_html/src/dom/interfaces/element.rs b/crates/xilem_html/src/interfaces/element.rs similarity index 97% rename from crates/xilem_html/src/dom/interfaces/element.rs rename to crates/xilem_html/src/interfaces/element.rs index 6d4a31c84..ce547829f 100644 --- a/crates/xilem_html/src/dom/interfaces/element.rs +++ b/crates/xilem_html/src/interfaces/element.rs @@ -2,10 +2,7 @@ use std::borrow::Cow; use wasm_bindgen::JsCast; -use crate::{ - dom::{attribute::Attr, event::EventListener}, - AttributeValue, IntoAttributeValue, OptionalAction, View, ViewMarker, -}; +use crate::{Attr, AttributeValue, EventListener, IntoAttributeValue, OptionalAction}; // TODO should the options be its own function `on_event_with_options`, // or should that be done via the builder pattern: `el.on_event().passive(false)`? @@ -40,7 +37,7 @@ macro_rules! event_handler_mixin { use super::Node; // TODO should Node or even EventTarget have the super trait View instead? -pub trait Element: Node + View + ViewMarker +pub trait Element: Node where Self: Sized, { diff --git a/crates/xilem_html/src/dom/interfaces/event_target.rs b/crates/xilem_html/src/interfaces/event_target.rs similarity index 57% rename from crates/xilem_html/src/dom/interfaces/event_target.rs rename to crates/xilem_html/src/interfaces/event_target.rs index 6c8f50782..b04ecb9e3 100644 --- a/crates/xilem_html/src/dom/interfaces/event_target.rs +++ b/crates/xilem_html/src/interfaces/event_target.rs @@ -3,12 +3,13 @@ use std::borrow::Cow; use gloo::events::EventListenerOptions; use wasm_bindgen::JsCast; -use crate::{ - dom::{attribute::Attr, event::EventListener}, - OptionalAction, -}; +use crate::{attribute::Attr, event::EventListener, OptionalAction, View, ViewMarker}; -pub trait EventTarget { +use super::Element; + +// TODO should this have the super trait View or should Node be the one? +// And/Or should the View::Element use EventTarget instead of Node (currently the trait `DomNode`)? +pub trait EventTarget: View + ViewMarker { fn on( self, event: impl Into>, @@ -39,5 +40,12 @@ pub trait EventTarget { } } -impl> EventTarget for Attr {} -impl, Ev, F> EventTarget for EventListener {} +impl> EventTarget for Attr {} +impl, Ev, F, OA> EventTarget for EventListener +where + F: Fn(&mut T, Ev) -> OA, + E: EventTarget, + Ev: JsCast + 'static, + OA: OptionalAction, +{ +} diff --git a/crates/xilem_html/src/dom/interfaces/generated.rs b/crates/xilem_html/src/interfaces/generated.rs similarity index 97% rename from crates/xilem_html/src/dom/interfaces/generated.rs rename to crates/xilem_html/src/interfaces/generated.rs index e0d739c82..078982ef5 100644 --- a/crates/xilem_html/src/dom/interfaces/generated.rs +++ b/crates/xilem_html/src/interfaces/generated.rs @@ -1,9 +1,6 @@ use wasm_bindgen::JsCast; -use crate::{ - dom::{attribute::Attr, event::EventListener, interfaces::Element}, - OptionalAction, -}; +use crate::{event::EventListener, interfaces::Element, Attr, OptionalAction}; macro_rules! dom_interface_trait_definitions { ($($dom_interface:ident : $super_dom_interface: ident $body: tt),*) => { diff --git a/crates/xilem_html/src/dom/interfaces/mod.rs b/crates/xilem_html/src/interfaces/mod.rs similarity index 100% rename from crates/xilem_html/src/dom/interfaces/mod.rs rename to crates/xilem_html/src/interfaces/mod.rs diff --git a/crates/xilem_html/src/interfaces/node.rs b/crates/xilem_html/src/interfaces/node.rs new file mode 100644 index 000000000..135fb0308 --- /dev/null +++ b/crates/xilem_html/src/interfaces/node.rs @@ -0,0 +1,27 @@ +use wasm_bindgen::JsCast; + +use crate::{attribute::Attr, event::EventListener, OptionalAction}; + +use super::{Element, EventTarget}; + +pub trait Node: EventTarget { + fn node_name(&self) -> &str; +} + +impl> Node for Attr { + fn node_name(&self) -> &str { + self.element.node_name() + } +} + +impl, Ev, F, OA> Node for EventListener +where + F: Fn(&mut T, Ev) -> OA, + E: Node, + Ev: JsCast + 'static, + OA: OptionalAction, +{ + fn node_name(&self) -> &str { + self.element.node_name() + } +} diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index f671fbe7a..95c9e7f8e 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -8,35 +8,32 @@ use wasm_bindgen::JsCast; mod app; -mod class; +mod attribute; +mod attribute_value; mod context; mod diff; -pub mod dom; -mod element; +pub mod elements; mod event; +pub mod interfaces; mod one_of; +mod optional_action; mod vecmap; mod view; -#[cfg(feature = "typed")] mod view_ext; pub use xilem_core::MessageResult; pub use app::App; -pub use class::class; +pub use attribute::Attr; +pub use attribute_value::{AttributeValue, IntoAttributeValue}; pub use context::{ChangeFlags, Cx}; -#[cfg(feature = "typed")] -pub use element::elements; -pub use element::{element, AttributeValue, Element, ElementState, IntoAttributeValue}; -#[cfg(feature = "typed")] -pub use event::events; -pub use event::{on_event, Action, Event, OnEvent, OnEventState, OptionalAction}; +pub use event::{EventListener, EventListenerOptions, EventListenerState}; pub use one_of::{OneOf2, OneOf3, OneOf4, OneOf5, OneOf6, OneOf7, OneOf8}; +pub use optional_action::{Action, OptionalAction}; pub use view::{ memoize, s, Adapt, AdaptState, AdaptThunk, AnyView, Memoize, Pod, View, ViewMarker, ViewSequence, }; -#[cfg(feature = "typed")] pub use view_ext::ViewExt; xilem_core::message!(); diff --git a/crates/xilem_html/src/dom/mod.rs b/crates/xilem_html/src/mod.rs similarity index 100% rename from crates/xilem_html/src/dom/mod.rs rename to crates/xilem_html/src/mod.rs diff --git a/crates/xilem_html/src/optional_action.rs b/crates/xilem_html/src/optional_action.rs new file mode 100644 index 000000000..de8021e99 --- /dev/null +++ b/crates/xilem_html/src/optional_action.rs @@ -0,0 +1,35 @@ +/// Implement this trait for types you want to use as actions. +/// +/// The trait exists because otherwise we couldn't provide versions +/// of listeners that take `()`, `A` and `Option`. +pub trait Action {} + +/// Trait that allows callbacks to be polymorphic on return type +/// (`Action`, `Option` or `()`). An implementation detail. +pub trait OptionalAction: sealed::Sealed { + fn action(self) -> Option; +} +mod sealed { + pub trait Sealed {} +} + +impl sealed::Sealed for () {} +impl OptionalAction for () { + fn action(self) -> Option { + None + } +} + +impl sealed::Sealed for A {} +impl OptionalAction for A { + fn action(self) -> Option { + Some(self) + } +} + +impl sealed::Sealed for Option {} +impl OptionalAction for Option { + fn action(self) -> Option { + self + } +} diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs index 140d3bb3b..2aef3037b 100644 --- a/crates/xilem_html/src/view.rs +++ b/crates/xilem_html/src/view.rs @@ -34,18 +34,6 @@ impl + 'static> DomNode for N { } } -/// This trait is implemented for types that implement `AsRef`. -/// It is an implementation detail. -pub trait DomElement: DomNode { - fn as_element_ref(&self) -> &web_sys::Element; -} - -impl> DomElement for N { - fn as_element_ref(&self) -> &web_sys::Element { - self.as_ref() - } -} - /// A trait for types that can be type-erased and impl `AsRef`. It is an /// implementation detail. pub trait AnyNode: sealed::Sealed { diff --git a/crates/xilem_html/src/view_ext.rs b/crates/xilem_html/src/view_ext.rs index 6f1d858ec..f1ffbcd99 100644 --- a/crates/xilem_html/src/view_ext.rs +++ b/crates/xilem_html/src/view_ext.rs @@ -1,69 +1,10 @@ // Copyright 2023 the Druid Authors. // SPDX-License-Identifier: Apache-2.0 -use std::borrow::Cow; +use crate::{view::View, Adapt, AdaptState, AdaptThunk}; -use crate::{ - class::Class, event::OptionalAction, events, view::View, Adapt, AdaptState, AdaptThunk, Event, -}; - -/// A trait that makes it possible to attach event listeners and more to views -/// in the continuation style. +/// A trait that makes it possible to use core views such as [`Adapt`] in the continuation/builder style. pub trait ViewExt: View + Sized { - // /// Add an `onclick` event listener. - // fn on_click< - // OA: OptionalAction, - // F: Fn(&mut T, &Event) -> OA, - // >( - // self, - // f: F, - // ) -> events::OnClick { - // events::on_click(self, f) - // } - - // /// Add an `ondblclick` event listener. - // fn on_dblclick< - // OA: OptionalAction, - // F: Fn(&mut T, &Event) -> OA, - // >( - // self, - // f: F, - // ) -> events::OnDblClick { - // events::on_dblclick(self, f) - // } - - // /// Add an `oninput` event listener. - // fn on_input< - // OA: OptionalAction, - // F: Fn(&mut T, &Event) -> OA, - // >( - // self, - // f: F, - // ) -> events::OnInput { - // events::on_input(self, f) - // } - - // /// Add an `onkeydown` event listener. - // fn on_keydown< - // OA: OptionalAction, - // F: Fn(&mut T, &Event) -> OA, - // >( - // self, - // f: F, - // ) -> events::OnKeyDown { - // events::on_keydown(self, f) - // } - - // fn on_blur< - // OA: OptionalAction, - // F: Fn(&mut T, &Event) -> OA, - // >( - // self, - // f: F, - // ) -> events::OnBlur { - // events::on_blur(self, f) - // } - fn adapt(self, f: F) -> Adapt where F: Fn(&mut ParentT, AdaptThunk) -> xilem_core::MessageResult, @@ -77,11 +18,6 @@ pub trait ViewExt: View + Sized { { AdaptState::new(f, self) } - - // /// Apply a CSS class to the child view. - // fn class(self, class: impl Into>) -> Class { - // crate::class::class(self, class) - // } } impl> ViewExt for V {} diff --git a/crates/xilem_html/web_examples/counter/src/main.rs b/crates/xilem_html/web_examples/counter/src/main.rs index 37963d58f..592db5423 100644 --- a/crates/xilem_html/web_examples/counter/src/main.rs +++ b/crates/xilem_html/web_examples/counter/src/main.rs @@ -1,9 +1,6 @@ use xilem_html::{ - document_body, - dom::{ - elements as el, - interfaces::{Element, HtmlButtonElement}, - }, + document_body, elements as el, + interfaces::{Element, HtmlButtonElement}, App, View, }; diff --git a/crates/xilem_html/web_examples/counter_untyped/Cargo.toml b/crates/xilem_html/web_examples/counter_custom_element/Cargo.toml similarity index 70% rename from crates/xilem_html/web_examples/counter_untyped/Cargo.toml rename to crates/xilem_html/web_examples/counter_custom_element/Cargo.toml index 9a22abad0..27cd7b653 100644 --- a/crates/xilem_html/web_examples/counter_untyped/Cargo.toml +++ b/crates/xilem_html/web_examples/counter_custom_element/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "counter_untyped" +name = "counter_custom_element" version = "0.1.0" publish = false license.workspace = true @@ -8,5 +8,5 @@ edition.workspace = true [dependencies] console_error_panic_hook = "0.1" wasm-bindgen = "0.2.87" -web-sys = { version = "0.3.64", features = ["HtmlButtonElement"] } +web-sys = { version = "0.3.64" } xilem_html = { path = "../..", default-features = false } diff --git a/crates/xilem_html/web_examples/counter_untyped/index.html b/crates/xilem_html/web_examples/counter_custom_element/index.html similarity index 100% rename from crates/xilem_html/web_examples/counter_untyped/index.html rename to crates/xilem_html/web_examples/counter_custom_element/index.html diff --git a/crates/xilem_html/web_examples/counter_untyped/src/main.rs b/crates/xilem_html/web_examples/counter_custom_element/src/main.rs similarity index 56% rename from crates/xilem_html/web_examples/counter_untyped/src/main.rs rename to crates/xilem_html/web_examples/counter_custom_element/src/main.rs index 9e78396dc..e71385a31 100644 --- a/crates/xilem_html/web_examples/counter_untyped/src/main.rs +++ b/crates/xilem_html/web_examples/counter_custom_element/src/main.rs @@ -1,4 +1,9 @@ -use xilem_html::{document_body, element, on_event, App, Event, View, ViewMarker}; +use xilem_html::{ + document_body, + elements::custom_element, + interfaces::{EventTarget, HtmlElement}, + App, View, +}; #[derive(Default)] struct AppState { @@ -17,24 +22,20 @@ impl AppState { } } -fn btn(label: &'static str, click_fn: F) -> impl View + ViewMarker -where - F: Fn(&mut AppState, &Event), -{ - on_event( - "click", - element("button", label), - move |state: &mut AppState, evt: &Event<_, _>| { - click_fn(state, evt); - }, - ) +fn btn( + label: &'static str, + click_fn: impl Fn(&mut AppState, web_sys::Event), +) -> impl HtmlElement { + custom_element("button", label).on("click", move |state: &mut AppState, evt| { + click_fn(state, evt); + }) } fn app_logic(state: &mut AppState) -> impl View { - element::( + custom_element( "div", ( - element::("span", format!("clicked {} times", state.clicks)), + custom_element("span", format!("clicked {} times", state.clicks)), btn("+1 click", |state, _| AppState::increment(state)), btn("-1 click", |state, _| AppState::decrement(state)), btn("reset clicks", |state, _| AppState::reset(state)), diff --git a/crates/xilem_html/web_examples/todomvc/src/main.rs b/crates/xilem_html/web_examples/todomvc/src/main.rs index 8b40c1e75..db02ed75c 100644 --- a/crates/xilem_html/web_examples/todomvc/src/main.rs +++ b/crates/xilem_html/web_examples/todomvc/src/main.rs @@ -4,18 +4,10 @@ use state::{AppState, Filter, Todo}; use wasm_bindgen::JsCast; use xilem_html::{ - dom::{ - elements::{self as el}, - event::EventListenerOptions, - interfaces::*, - }, - // events::on_click, + elements::{self as el}, get_element_by_id, - Action, - Adapt, - App, - MessageResult, - View, + interfaces::*, + Action, Adapt, App, EventListenerOptions, MessageResult, View, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce From 19cab002daade041f419a5b67ce5626502ece942 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Wed, 20 Sep 2023 19:16:15 +0200 Subject: [PATCH 6/6] General Cleanup and remove `on_with_options`, and "revert" back to builder pattern for EventListener options --- crates/xilem_html/src/event.rs | 87 +++--- crates/xilem_html/src/interfaces/element.rs | 291 +++++------------- .../xilem_html/src/interfaces/event_target.rs | 28 +- crates/xilem_html/src/interfaces/generated.rs | 4 +- crates/xilem_html/src/interfaces/node.rs | 4 +- crates/xilem_html/src/lib.rs | 2 +- .../web_examples/todomvc/src/main.rs | 44 ++- 7 files changed, 152 insertions(+), 308 deletions(-) diff --git a/crates/xilem_html/src/event.rs b/crates/xilem_html/src/event.rs index aa724e165..1e7712f90 100644 --- a/crates/xilem_html/src/event.rs +++ b/crates/xilem_html/src/event.rs @@ -4,12 +4,14 @@ pub use gloo::events::EventListenerOptions; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use xilem_core::{Id, MessageResult}; -use crate::{view::DomNode, ChangeFlags, Cx, OptionalAction, View, ViewMarker}; +use crate::{ + interfaces::EventTarget, view::DomNode, ChangeFlags, Cx, OptionalAction, View, ViewMarker, +}; /// Wraps a [`View`] `V` and attaches an event listener. /// /// The event type `E` should inherit from [`web_sys::Event`] -pub struct EventListener { +pub struct OnEvent { pub(crate) element: V, pub(crate) event: Cow<'static, str>, pub(crate) options: EventListenerOptions, @@ -17,12 +19,12 @@ pub struct EventListener { pub(crate) phantom_event_ty: PhantomData, } -impl EventListener +impl OnEvent where E: JsCast + 'static, { pub fn new(element: V, event: impl Into>, handler: F) -> Self { - EventListener { + OnEvent { element, event: event.into(), options: Default::default(), @@ -31,37 +33,33 @@ where } } - pub fn new_with_options( - element: V, - event: impl Into>, - handler: F, - options: EventListenerOptions, - ) -> Self { - EventListener { - element, - event: event.into(), - options, - handler, - phantom_event_ty: PhantomData, - } + /// Whether the event handler should be passive. (default = `true`) + /// + /// Passive event handlers can't prevent the browser's default action from + /// running (otherwise possible with `event.prevent_default()`), which + /// restricts what they can be used for, but reduces overhead. + pub fn passive(mut self, value: bool) -> Self { + self.options.passive = value; + self } +} - fn create_event_listener( - &self, - target: &web_sys::EventTarget, - cx: &Cx, - ) -> gloo::events::EventListener { - let thunk = cx.message_thunk(); - gloo::events::EventListener::new_with_options( - target, - self.event.clone(), - self.options, - move |event: &web_sys::Event| { - let event = (*event).clone().dyn_into::().unwrap_throw(); - thunk.push_message(event); - }, - ) - } +fn create_event_listener( + target: &web_sys::EventTarget, + event: impl Into>, + options: EventListenerOptions, + cx: &Cx, +) -> gloo::events::EventListener { + let thunk = cx.message_thunk(); + gloo::events::EventListener::new_with_options( + target, + event, + options, + move |event: &web_sys::Event| { + let event = (*event).clone().dyn_into::().unwrap_throw(); + thunk.push_message(event); + }, + ) } /// State for the `OnEvent` view. @@ -72,14 +70,14 @@ pub struct EventListenerState { child_state: S, } -impl ViewMarker for EventListener {} +impl ViewMarker for OnEvent {} -impl View for EventListener +impl View for OnEvent where + OA: OptionalAction, F: Fn(&mut T, E) -> OA, - V: View, + V: EventTarget, E: JsCast + 'static, - OA: OptionalAction, { type State = EventListenerState; @@ -87,9 +85,13 @@ where fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let (id, (element, state)) = cx.with_new_id(|cx| { - // id is already stored in element state let (child_id, child_state, element) = self.element.build(cx); - let listener = self.create_event_listener(element.as_node_ref(), cx); + let listener = create_event_listener::( + element.as_node_ref(), + self.event.clone(), + self.options, + cx, + ); let state = EventListenerState { child_state, child_id, @@ -114,7 +116,12 @@ where .rebuild(cx, &prev.element, id, &mut state.child_state, element); // TODO check equality of prev and current element somehow if prev.event != self.event || changed.contains(ChangeFlags::STRUCTURE) { - state.listener = self.create_event_listener(element.as_node_ref(), cx); + state.listener = create_event_listener::( + element.as_node_ref(), + self.event.clone(), + self.options, + cx, + ); changed |= ChangeFlags::OTHER_CHANGE; } changed diff --git a/crates/xilem_html/src/interfaces/element.rs b/crates/xilem_html/src/interfaces/element.rs index ce547829f..7d1e504e7 100644 --- a/crates/xilem_html/src/interfaces/element.rs +++ b/crates/xilem_html/src/interfaces/element.rs @@ -2,34 +2,22 @@ use std::borrow::Cow; use wasm_bindgen::JsCast; -use crate::{Attr, AttributeValue, EventListener, IntoAttributeValue, OptionalAction}; +use crate::{Attr, AttributeValue, IntoAttributeValue, OnEvent, OptionalAction}; // TODO should the options be its own function `on_event_with_options`, // or should that be done via the builder pattern: `el.on_event().passive(false)`? macro_rules! event_handler_mixin { - ($(($fn_name:ident, $fn_name_options:ident, $event:expr, $web_sys_event_type:ident),)*) => { + ($(($fn_name:ident, $event:expr, $web_sys_event_type:ident),)*) => { $( fn $fn_name( self, handler: EH, - ) -> EventListener + ) -> OnEvent where OA: OptionalAction, EH: Fn(&mut T, web_sys::$web_sys_event_type) -> OA, { - EventListener::new(self, $event, handler) - } - - fn $fn_name_options( - self, - handler: EH, - options: gloo::events::EventListenerOptions, - ) -> EventListener - where - OA: OptionalAction, - EH: Fn(&mut T, web_sys::$web_sys_event_type) -> OA, - { - EventListener::new_with_options(self, $event, handler, options) + OnEvent::new(self, $event, handler) } )* }; @@ -78,209 +66,84 @@ where // I didn't include the events on the window, since we aren't attaching // any events to the window in xilem_html event_handler_mixin!( - (on_abort, on_abort_with_options, "abort", Event), - ( - on_auxclick, - on_auxclick_with_options, - "auxclick", - PointerEvent - ), - ( - on_beforeinput, - on_beforeinput_with_options, - "beforeinput", - InputEvent - ), - ( - on_beforematch, - on_beforematch_with_options, - "beforematch", - Event - ), - ( - on_beforetoggle, - on_beforetoggle_with_options, - "beforetoggle", - Event - ), - (on_blur, on_blur_with_options, "blur", FocusEvent), - (on_cancel, on_cancel_with_options, "cancel", Event), - (on_canplay, on_canplay_with_options, "canplay", Event), - ( - on_canplaythrough, - on_canplaythrough_with_options, - "canplaythrough", - Event - ), - (on_change, on_change_with_options, "change", Event), - (on_click, on_click_with_options, "click", MouseEvent), - (on_close, on_close_with_options, "close", Event), - ( - on_contextlost, - on_contextlost_with_options, - "contextlost", - Event - ), - ( - on_contextmenu, - on_contextmenu_with_options, - "contextmenu", - PointerEvent - ), - ( - on_contextrestored, - on_contextrestored_with_options, - "contextrestored", - Event - ), - (on_copy, on_copy_with_options, "copy", Event), - (on_cuechange, on_cuechange_with_options, "cuechange", Event), - (on_cut, on_cut_with_options, "cut", Event), - ( - on_dblclick, - on_dblclick_with_options, - "dblclick", - MouseEvent - ), - (on_drag, on_drag_with_options, "drag", Event), - (on_dragend, on_dragend_with_options, "dragend", Event), - (on_dragenter, on_dragenter_with_options, "dragenter", Event), - (on_dragleave, on_dragleave_with_options, "dragleave", Event), - (on_dragover, on_dragover_with_options, "dragover", Event), - (on_dragstart, on_dragstart_with_options, "dragstart", Event), - (on_drop, on_drop_with_options, "drop", Event), - ( - on_durationchange, - on_durationchange_with_options, - "durationchange", - Event - ), - (on_emptied, on_emptied_with_options, "emptied", Event), - (on_ended, on_ended_with_options, "ended", Event), - (on_error, on_error_with_options, "error", Event), - (on_focus, on_focus_with_options, "focus", FocusEvent), - (on_focusin, on_focusin_with_options, "focusin", FocusEvent), - ( - on_focusout, - on_focusout_with_options, - "focusout", - FocusEvent - ), - (on_formdata, on_formdata_with_options, "formdata", Event), - (on_input, on_input_with_options, "input", InputEvent), - (on_invalid, on_invalid_with_options, "invalid", Event), - ( - on_keydown, - on_keydown_with_options, - "keydown", - KeyboardEvent - ), - (on_keyup, on_keyup_with_options, "keyup", KeyboardEvent), - (on_load, on_load_with_options, "load", Event), - ( - on_loadeddata, - on_loadeddata_with_options, - "loadeddata", - Event - ), - ( - on_loadedmetadata, - on_loadedmetadata_with_options, - "loadedmetadata", - Event - ), - (on_loadstart, on_loadstart_with_options, "loadstart", Event), - ( - on_mousedown, - on_mousedown_with_options, - "mousedown", - MouseEvent - ), - ( - on_mouseenter, - on_mouseenter_with_options, - "mouseenter", - MouseEvent - ), - ( - on_mouseleave, - on_mouseleave_with_options, - "mouseleave", - MouseEvent - ), - ( - on_mousemove, - on_mousemove_with_options, - "mousemove", - MouseEvent - ), - ( - on_mouseout, - on_mouseout_with_options, - "mouseout", - MouseEvent - ), - ( - on_mouseover, - on_mouseover_with_options, - "mouseover", - MouseEvent - ), - (on_mouseup, on_mouseup_with_options, "mouseup", MouseEvent), - (on_paste, on_paste_with_options, "paste", Event), - (on_pause, on_pause_with_options, "pause", Event), - (on_play, on_play_with_options, "play", Event), - (on_playing, on_playing_with_options, "playing", Event), - (on_progress, on_progress_with_options, "progress", Event), - ( - on_ratechange, - on_ratechange_with_options, - "ratechange", - Event - ), - (on_reset, on_reset_with_options, "reset", Event), - (on_resize, on_resize_with_options, "resize", Event), - (on_scroll, on_scroll_with_options, "scroll", Event), - (on_scrollend, on_scrollend_with_options, "scrollend", Event), - ( - on_securitypolicyviolation, - on_securitypolicyviolation_with_options, - "securitypolicyviolation", - Event - ), - (on_seeked, on_seeked_with_options, "seeked", Event), - (on_seeking, on_seeking_with_options, "seeking", Event), - (on_select, on_select_with_options, "select", Event), - ( - on_slotchange, - on_slotchange_with_options, - "slotchange", - Event - ), - (on_stalled, on_stalled_with_options, "stalled", Event), - (on_submit, on_submit_with_options, "submit", Event), - (on_suspend, on_suspend_with_options, "suspend", Event), - ( - on_timeupdate, - on_timeupdate_with_options, - "timeupdate", - Event - ), - (on_toggle, on_toggle_with_options, "toggle", Event), - ( - on_volumechange, - on_volumechange_with_options, - "volumechange", - Event - ), - (on_waiting, on_waiting_with_options, "waiting", Event), - (on_wheel, on_wheel_with_options, "wheel", WheelEvent), + (on_abort, "abort", Event), + (on_auxclick, "auxclick", PointerEvent), + (on_beforeinput, "beforeinput", InputEvent), + (on_beforematch, "beforematch", Event), + (on_beforetoggle, "beforetoggle", Event), + (on_blur, "blur", FocusEvent), + (on_cancel, "cancel", Event), + (on_canplay, "canplay", Event), + (on_canplaythrough, "canplaythrough", Event), + (on_change, "change", Event), + (on_click, "click", MouseEvent), + (on_close, "close", Event), + (on_contextlost, "contextlost", Event), + (on_contextmenu, "contextmenu", PointerEvent), + (on_contextrestored, "contextrestored", Event), + (on_copy, "copy", Event), + (on_cuechange, "cuechange", Event), + (on_cut, "cut", Event), + (on_dblclick, "dblclick", MouseEvent), + (on_drag, "drag", Event), + (on_dragend, "dragend", Event), + (on_dragenter, "dragenter", Event), + (on_dragleave, "dragleave", Event), + (on_dragover, "dragover", Event), + (on_dragstart, "dragstart", Event), + (on_drop, "drop", Event), + (on_durationchange, "durationchange", Event), + (on_emptied, "emptied", Event), + (on_ended, "ended", Event), + (on_error, "error", Event), + (on_focus, "focus", FocusEvent), + (on_focusin, "focusin", FocusEvent), + (on_focusout, "focusout", FocusEvent), + (on_formdata, "formdata", Event), + (on_input, "input", InputEvent), + (on_invalid, "invalid", Event), + (on_keydown, "keydown", KeyboardEvent), + (on_keyup, "keyup", KeyboardEvent), + (on_load, "load", Event), + (on_loadeddata, "loadeddata", Event), + (on_loadedmetadata, "loadedmetadata", Event), + (on_loadstart, "loadstart", Event), + (on_mousedown, "mousedown", MouseEvent), + (on_mouseenter, "mouseenter", MouseEvent), + (on_mouseleave, "mouseleave", MouseEvent), + (on_mousemove, "mousemove", MouseEvent), + (on_mouseout, "mouseout", MouseEvent), + (on_mouseover, "mouseover", MouseEvent), + (on_mouseup, "mouseup", MouseEvent), + (on_paste, "paste", Event), + (on_pause, "pause", Event), + (on_play, "play", Event), + (on_playing, "playing", Event), + (on_progress, "progress", Event), + (on_ratechange, "ratechange", Event), + (on_reset, "reset", Event), + (on_resize, "resize", Event), + (on_scroll, "scroll", Event), + (on_scrollend, "scrollend", Event), + (on_securitypolicyviolation, "securitypolicyviolation", Event), + (on_seeked, "seeked", Event), + (on_seeking, "seeking", Event), + (on_select, "select", Event), + (on_slotchange, "slotchange", Event), + (on_stalled, "stalled", Event), + (on_submit, "submit", Event), + (on_suspend, "suspend", Event), + (on_timeupdate, "timeupdate", Event), + (on_toggle, "toggle", Event), + (on_volumechange, "volumechange", Event), + (on_waiting, "waiting", Event), + (on_wheel, "wheel", WheelEvent), ); } impl> Element for Attr {} -impl Element for EventListener +impl Element for OnEvent where F: Fn(&mut T, Ev) -> OA, E: Element, diff --git a/crates/xilem_html/src/interfaces/event_target.rs b/crates/xilem_html/src/interfaces/event_target.rs index b04ecb9e3..f044a1ec6 100644 --- a/crates/xilem_html/src/interfaces/event_target.rs +++ b/crates/xilem_html/src/interfaces/event_target.rs @@ -1,47 +1,27 @@ use std::borrow::Cow; -use gloo::events::EventListenerOptions; use wasm_bindgen::JsCast; -use crate::{attribute::Attr, event::EventListener, OptionalAction, View, ViewMarker}; +use crate::{attribute::Attr, event::OnEvent, OptionalAction, View, ViewMarker}; use super::Element; // TODO should this have the super trait View or should Node be the one? // And/Or should the View::Element use EventTarget instead of Node (currently the trait `DomNode`)? pub trait EventTarget: View + ViewMarker { - fn on( - self, - event: impl Into>, - handler: EH, - ) -> EventListener + fn on(self, event: impl Into>, handler: EH) -> OnEvent where E: JsCast + 'static, OA: OptionalAction, EH: Fn(&mut T, E) -> OA, Self: Sized, { - EventListener::new(self, event, handler) - } - - fn on_with_options( - self, - event: impl Into>, - handler: EH, - options: EventListenerOptions, - ) -> EventListener - where - E: JsCast + 'static, - OA: OptionalAction, - EH: Fn(&mut T, E) -> OA, - Self: Sized, - { - EventListener::new_with_options(self, event, handler, options) + OnEvent::new(self, event, handler) } } impl> EventTarget for Attr {} -impl, Ev, F, OA> EventTarget for EventListener +impl, Ev, F, OA> EventTarget for OnEvent where F: Fn(&mut T, Ev) -> OA, E: EventTarget, diff --git a/crates/xilem_html/src/interfaces/generated.rs b/crates/xilem_html/src/interfaces/generated.rs index 078982ef5..1d1a43761 100644 --- a/crates/xilem_html/src/interfaces/generated.rs +++ b/crates/xilem_html/src/interfaces/generated.rs @@ -1,6 +1,6 @@ use wasm_bindgen::JsCast; -use crate::{event::EventListener, interfaces::Element, Attr, OptionalAction}; +use crate::{event::OnEvent, interfaces::Element, Attr, OptionalAction}; macro_rules! dom_interface_trait_definitions { ($($dom_interface:ident : $super_dom_interface: ident $body: tt),*) => { @@ -9,7 +9,7 @@ macro_rules! dom_interface_trait_definitions { impl> $dom_interface for Attr { } - impl $dom_interface for EventListener + impl $dom_interface for OnEvent where F: Fn(&mut T, Ev) -> OA, E: $dom_interface, diff --git a/crates/xilem_html/src/interfaces/node.rs b/crates/xilem_html/src/interfaces/node.rs index 135fb0308..3cc5e70f5 100644 --- a/crates/xilem_html/src/interfaces/node.rs +++ b/crates/xilem_html/src/interfaces/node.rs @@ -1,6 +1,6 @@ use wasm_bindgen::JsCast; -use crate::{attribute::Attr, event::EventListener, OptionalAction}; +use crate::{attribute::Attr, event::OnEvent, OptionalAction}; use super::{Element, EventTarget}; @@ -14,7 +14,7 @@ impl> Node for Attr { } } -impl, Ev, F, OA> Node for EventListener +impl, Ev, F, OA> Node for OnEvent where F: Fn(&mut T, Ev) -> OA, E: Node, diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index 95c9e7f8e..ae37c0ffa 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -27,7 +27,7 @@ pub use app::App; pub use attribute::Attr; pub use attribute_value::{AttributeValue, IntoAttributeValue}; pub use context::{ChangeFlags, Cx}; -pub use event::{EventListener, EventListenerOptions, EventListenerState}; +pub use event::{EventListenerOptions, EventListenerState, OnEvent}; pub use one_of::{OneOf2, OneOf3, OneOf4, OneOf5, OneOf6, OneOf7, OneOf8}; pub use optional_action::{Action, OptionalAction}; pub use view::{ diff --git a/crates/xilem_html/web_examples/todomvc/src/main.rs b/crates/xilem_html/web_examples/todomvc/src/main.rs index db02ed75c..3cd5463fe 100644 --- a/crates/xilem_html/web_examples/todomvc/src/main.rs +++ b/crates/xilem_html/web_examples/todomvc/src/main.rs @@ -7,7 +7,7 @@ use xilem_html::{ elements::{self as el}, get_element_by_id, interfaces::*, - Action, Adapt, App, EventListenerOptions, MessageResult, View, + Action, Adapt, App, MessageResult, View, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce @@ -60,20 +60,17 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { None } }) - .on_input_with_options( - |state: &mut Todo, evt| { - // TODO is there a less boilerplate but safe way to get to the value of the element? - let Some(target) = evt.target() else { - return; - }; - let Some(element) = target.dyn_ref::() else { - return; - }; + .on_input(|state: &mut Todo, evt| { + // TODO There could/should be further checks, if this is indeed the right event (same dom element) + if let Some(element) = evt + .target() + .and_then(|t| t.dyn_into::().ok()) + { evt.prevent_default(); state.title_editing = element.value(); - }, - EventListenerOptions::enable_prevent_default(), - ) + } + }) + .passive(true) .on_blur(|_, _| TodoAction::CancelEditing), )) .attr("class", class) @@ -196,20 +193,17 @@ fn app_logic(state: &mut AppState) -> impl View { state.create_todo(); } }) - .on_input_with_options( - |state: &mut AppState, evt| { - // TODO is there a less boilerplate but safe way to get to the value of the element? - let Some(target) = evt.target() else { - return; - }; - let Some(element) = target.dyn_ref::() else { - return; - }; + .on_input(|state: &mut AppState, evt| { + // TODO There could/should be further checks, if this is indeed the right event (same dom element) + if let Some(element) = evt + .target() + .and_then(|t| t.dyn_into::().ok()) + { state.update_new_todo(&element.value()); evt.prevent_default(); - }, - EventListenerOptions::enable_prevent_default(), - ), + } + }) + .passive(false), )) .attr("class", "header"), main,