From 09271e7beba36dc01d73102355256afdc873c17b Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Wed, 16 Aug 2023 17:35:15 +0200 Subject: [PATCH] xilem_html: Add support for different types in `el.attr("attr", )` (#127) Where `` can be primitive values like numbers, booleans and strings, and options of these values. In case a `None` or `false` value is passed, the attribute is removed from the element, as even an empty string value for an attribute evaluates to `true` in javascript (relevant for e.g. `input.attr("checked", false)`). This is achieved via an enum that serializes to a string and syntax sugar via an extra trait `IntoAttributeValue` which converts a (currently) primitive value into this enum when passing it to the method. This also adds an additional method `el.remove_attr(name)`. --- .../xilem_html/src/element/attribute_value.rs | 92 +++++++++++++++++++ crates/xilem_html/src/element/elements.rs | 12 ++- crates/xilem_html/src/element/mod.rs | 46 +++++----- crates/xilem_html/src/lib.rs | 2 +- .../web_examples/todomvc/src/main.rs | 26 ++---- 5 files changed, 136 insertions(+), 42 deletions(-) create mode 100644 crates/xilem_html/src/element/attribute_value.rs diff --git a/crates/xilem_html/src/element/attribute_value.rs b/crates/xilem_html/src/element/attribute_value.rs new file mode 100644 index 00000000..050a3a68 --- /dev/null +++ b/crates/xilem_html/src/element/attribute_value.rs @@ -0,0 +1,92 @@ +type CowStr = std::borrow::Cow<'static, str>; + +#[derive(PartialEq, Debug)] +pub enum AttributeValue { + True, // for the boolean true, this serializes to an empty string (e.g. for ) + I32(i32), + U32(u32), + F32(f32), + F64(f64), + String(CowStr), +} + +impl AttributeValue { + pub fn serialize(&self) -> CowStr { + match self { + AttributeValue::True => "".into(), // empty string is equivalent to a true set attribute + AttributeValue::I32(n) => n.to_string().into(), + AttributeValue::U32(n) => n.to_string().into(), + AttributeValue::F32(n) => n.to_string().into(), + AttributeValue::F64(n) => n.to_string().into(), + AttributeValue::String(s) => s.clone(), + } + } +} + +pub trait IntoAttributeValue: Sized { + fn into_attribute_value(self) -> Option; +} + +impl IntoAttributeValue for Option { + fn into_attribute_value(self) -> Option { + if let Some(value) = self { + T::into_attribute_value(value) + } else { + None + } + } +} + +impl IntoAttributeValue for bool { + fn into_attribute_value(self) -> Option { + self.then_some(AttributeValue::True) + } +} + +impl IntoAttributeValue for AttributeValue { + fn into_attribute_value(self) -> Option { + Some(self) + } +} + +impl IntoAttributeValue for u32 { + fn into_attribute_value(self) -> Option { + Some(AttributeValue::U32(self)) + } +} + +impl IntoAttributeValue for i32 { + fn into_attribute_value(self) -> Option { + Some(AttributeValue::I32(self)) + } +} + +impl IntoAttributeValue for f32 { + fn into_attribute_value(self) -> Option { + Some(AttributeValue::F32(self)) + } +} + +impl IntoAttributeValue for f64 { + fn into_attribute_value(self) -> Option { + Some(AttributeValue::F64(self)) + } +} + +impl IntoAttributeValue for String { + fn into_attribute_value(self) -> Option { + Some(AttributeValue::String(self.into())) + } +} + +impl IntoAttributeValue for CowStr { + fn into_attribute_value(self) -> Option { + Some(AttributeValue::String(self)) + } +} + +impl IntoAttributeValue for &'static str { + fn into_attribute_value(self) -> Option { + Some(AttributeValue::String(self.into())) + } +} diff --git a/crates/xilem_html/src/element/elements.rs b/crates/xilem_html/src/element/elements.rs index ebc385be..ff2af42b 100644 --- a/crates/xilem_html/src/element/elements.rs +++ b/crates/xilem_html/src/element/elements.rs @@ -32,7 +32,7 @@ macro_rules! element { pub fn attr( mut self, name: impl Into>, - value: impl Into>, + value: impl crate::IntoAttributeValue, ) -> Self { self.0.set_attr(name, value); self @@ -47,12 +47,20 @@ macro_rules! element { pub fn set_attr( &mut self, name: impl Into>, - value: impl Into>, + value: impl crate::IntoAttributeValue, ) -> &mut Self { self.0.set_attr(name, value); self } + pub fn remove_attr( + &mut self, + name: impl Into>, + ) -> &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 diff --git a/crates/xilem_html/src/element/mod.rs b/crates/xilem_html/src/element/mod.rs index 8e079d33..49ac9337 100644 --- a/crates/xilem_html/src/element/mod.rs +++ b/crates/xilem_html/src/element/mod.rs @@ -13,15 +13,20 @@ 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: Cow<'static, str>, - attributes: VecMap, Cow<'static, str>>, + name: CowStr, + attributes: VecMap, children: Children, #[allow(clippy::type_complexity)] after_update: Option>, @@ -34,7 +39,7 @@ impl Element { 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}\"")?; + write!(f, " {name}=\"{}\"", value.serialize())?; } write!(f, ">") } @@ -55,13 +60,10 @@ pub struct ElementState { /// 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 { +pub fn element(name: impl Into, children: ViewSeq) -> Element { Element { name: name.into(), - attributes: VecMap::default(), + attributes: Default::default(), children, after_update: None, } @@ -74,11 +76,7 @@ impl Element { /// /// 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 Into>, - ) -> Self { + pub fn attr(mut self, name: impl Into, value: impl IntoAttributeValue) -> Self { self.set_attr(name, value); self } @@ -89,12 +87,17 @@ impl Element { /// /// 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 Into>, - ) { - self.attributes.insert(name.into(), value.into()); + 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.attributes.remove(&name); + } + } + + pub fn remove_attr(&mut self, name: impl Into) { + self.attributes.remove(&name.into()); } /// Set a function to run after the new view tree has been created. @@ -125,8 +128,9 @@ where 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).unwrap_throw(); + 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 { @@ -185,7 +189,7 @@ where 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); + set_attribute(element, name, &value.serialize()); changed |= ChangeFlags::OTHER_CHANGE; } Diff::Remove(name) => { diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index 60de2c7e..9e6f5078 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -26,7 +26,7 @@ pub use class::class; pub use context::{ChangeFlags, Cx}; #[cfg(feature = "typed")] pub use element::elements; -pub use element::{element, Element, ElementState}; +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}; diff --git a/crates/xilem_html/web_examples/todomvc/src/main.rs b/crates/xilem_html/web_examples/todomvc/src/main.rs index 33b2bbb5..1e877daf 100644 --- a/crates/xilem_html/web_examples/todomvc/src/main.rs +++ b/crates/xilem_html/web_examples/todomvc/src/main.rs @@ -26,12 +26,10 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View + Vi if editing { class.push_str(" editing"); } - let mut input = el::input(()) + let input = el::input(()) .attr("class", "toggle") - .attr("type", "checkbox"); - if todo.completed { - input.set_attr("checked", "checked"); - }; + .attr("type", "checkbox") + .attr("checked", todo.completed); el::li(( el::div(( @@ -86,13 +84,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl View impl View ) }) .collect(); - let mut toggle_all = el::input(()) + let toggle_all = el::input(()) .attr("id", "toggle-all") .attr("class", "toggle-all") - .attr("type", "checkbox"); - if state.are_all_complete() { - toggle_all.set_attr("checked", "true"); - } + .attr("type", "checkbox") + .attr("checked", state.are_all_complete()); let mut section = el::section(( toggle_all.on_click(|state: &mut AppState, _| state.toggle_all_complete()), el::label(()).attr("for", "toggle-all"), @@ -190,7 +180,7 @@ fn app_logic(state: &mut AppState) -> impl View { .attr("class", "new-todo") .attr("placeholder", "What needs to be done?") .attr("value", state.new_todo.clone()) - .attr("autofocus", "true"); + .attr("autofocus", true); el::div(( el::header(( el::h1("TODOs"),