xilem_html: Add support for different types in `el.attr("attr", <value>)` (#127)

Where `<value>` 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)`.
This commit is contained in:
Philipp Mildenberger 2023-08-16 17:35:15 +02:00 committed by GitHub
parent 5600017d4a
commit 09271e7beb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 136 additions and 42 deletions

View File

@ -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 <input checked>)
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<AttributeValue>;
}
impl<T: IntoAttributeValue> IntoAttributeValue for Option<T> {
fn into_attribute_value(self) -> Option<AttributeValue> {
if let Some(value) = self {
T::into_attribute_value(value)
} else {
None
}
}
}
impl IntoAttributeValue for bool {
fn into_attribute_value(self) -> Option<AttributeValue> {
self.then_some(AttributeValue::True)
}
}
impl IntoAttributeValue for AttributeValue {
fn into_attribute_value(self) -> Option<AttributeValue> {
Some(self)
}
}
impl IntoAttributeValue for u32 {
fn into_attribute_value(self) -> Option<AttributeValue> {
Some(AttributeValue::U32(self))
}
}
impl IntoAttributeValue for i32 {
fn into_attribute_value(self) -> Option<AttributeValue> {
Some(AttributeValue::I32(self))
}
}
impl IntoAttributeValue for f32 {
fn into_attribute_value(self) -> Option<AttributeValue> {
Some(AttributeValue::F32(self))
}
}
impl IntoAttributeValue for f64 {
fn into_attribute_value(self) -> Option<AttributeValue> {
Some(AttributeValue::F64(self))
}
}
impl IntoAttributeValue for String {
fn into_attribute_value(self) -> Option<AttributeValue> {
Some(AttributeValue::String(self.into()))
}
}
impl IntoAttributeValue for CowStr {
fn into_attribute_value(self) -> Option<AttributeValue> {
Some(AttributeValue::String(self))
}
}
impl IntoAttributeValue for &'static str {
fn into_attribute_value(self) -> Option<AttributeValue> {
Some(AttributeValue::String(self.into()))
}
}

View File

@ -32,7 +32,7 @@ macro_rules! element {
pub fn attr( pub fn attr(
mut self, mut self,
name: impl Into<std::borrow::Cow<'static, str>>, name: impl Into<std::borrow::Cow<'static, str>>,
value: impl Into<std::borrow::Cow<'static, str>>, value: impl crate::IntoAttributeValue,
) -> Self { ) -> Self {
self.0.set_attr(name, value); self.0.set_attr(name, value);
self self
@ -47,12 +47,20 @@ macro_rules! element {
pub fn set_attr( pub fn set_attr(
&mut self, &mut self,
name: impl Into<std::borrow::Cow<'static, str>>, name: impl Into<std::borrow::Cow<'static, str>>,
value: impl Into<std::borrow::Cow<'static, str>>, value: impl crate::IntoAttributeValue,
) -> &mut Self { ) -> &mut Self {
self.0.set_attr(name, value); self.0.set_attr(name, value);
self self
} }
pub fn remove_attr(
&mut self,
name: impl Into<std::borrow::Cow<'static, str>>,
) -> &mut Self {
self.0.remove_attr(name);
self
}
pub fn after_update(mut self, after_update: impl Fn(&$web_sys_ty) + 'static) -> 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.0 = self.0.after_update(after_update);
self self

View File

@ -13,15 +13,20 @@ use std::{borrow::Cow, fmt};
use wasm_bindgen::{JsCast, UnwrapThrowExt}; use wasm_bindgen::{JsCast, UnwrapThrowExt};
use xilem_core::{Id, MessageResult, VecSplice}; use xilem_core::{Id, MessageResult, VecSplice};
mod attribute_value;
#[cfg(feature = "typed")] #[cfg(feature = "typed")]
pub mod elements; pub mod elements;
pub use attribute_value::{AttributeValue, IntoAttributeValue};
type CowStr = Cow<'static, str>;
/// A view representing a HTML element. /// A view representing a HTML element.
/// ///
/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`). /// If the element has no children, use the unit type (e.g. `let view = element("div", ())`).
pub struct Element<El, Children = ()> { pub struct Element<El, Children = ()> {
name: Cow<'static, str>, name: CowStr,
attributes: VecMap<Cow<'static, str>, Cow<'static, str>>, attributes: VecMap<CowStr, AttributeValue>,
children: Children, children: Children,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
after_update: Option<Box<dyn Fn(&El)>>, after_update: Option<Box<dyn Fn(&El)>>,
@ -34,7 +39,7 @@ impl<El, ViewSeq> Element<El, ViewSeq> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<{}", self.0.name)?; write!(f, "<{}", self.0.name)?;
for (name, value) in &self.0.attributes { for (name, value) in &self.0.attributes {
write!(f, " {name}=\"{value}\"")?; write!(f, " {name}=\"{}\"", value.serialize())?;
} }
write!(f, ">") write!(f, ">")
} }
@ -55,13 +60,10 @@ pub struct ElementState<ViewSeqState> {
/// Create a new element view /// Create a new element view
/// ///
/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`). /// If the element has no children, use the unit type (e.g. `let view = element("div", ())`).
pub fn element<El, ViewSeq>( pub fn element<El, ViewSeq>(name: impl Into<CowStr>, children: ViewSeq) -> Element<El, ViewSeq> {
name: impl Into<Cow<'static, str>>,
children: ViewSeq,
) -> Element<El, ViewSeq> {
Element { Element {
name: name.into(), name: name.into(),
attributes: VecMap::default(), attributes: Default::default(),
children, children,
after_update: None, after_update: None,
} }
@ -74,11 +76,7 @@ impl<El, ViewSeq> Element<El, ViewSeq> {
/// ///
/// If the name contains characters that are not valid in an attribute name, /// 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. /// then the `View::build`/`View::rebuild` functions will panic for this view.
pub fn attr( pub fn attr(mut self, name: impl Into<CowStr>, value: impl IntoAttributeValue) -> Self {
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.set_attr(name, value); self.set_attr(name, value);
self self
} }
@ -89,12 +87,17 @@ impl<El, ViewSeq> Element<El, ViewSeq> {
/// ///
/// If the name contains characters that are not valid in an attribute name, /// 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. /// then the `View::build`/`View::rebuild` functions will panic for this view.
pub fn set_attr( pub fn set_attr(&mut self, name: impl Into<CowStr>, value: impl IntoAttributeValue) {
&mut self, let name = name.into();
name: impl Into<Cow<'static, str>>, if let Some(value) = value.into_attribute_value() {
value: impl Into<Cow<'static, str>>, self.attributes.insert(name, value);
) { } else {
self.attributes.insert(name.into(), value.into()); self.attributes.remove(&name);
}
}
pub fn remove_attr(&mut self, name: impl Into<CowStr>) {
self.attributes.remove(&name.into());
} }
/// Set a function to run after the new view tree has been created. /// 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) { fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let el = cx.create_html_element(&self.name); let el = cx.create_html_element(&self.name);
for (name, value) in &self.attributes { 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 mut child_elements = vec![];
let (id, child_states) = cx.with_new_id(|cx| self.children.build(cx, &mut child_elements)); let (id, child_states) = cx.with_new_id(|cx| self.children.build(cx, &mut child_elements));
for child in &child_elements { for child in &child_elements {
@ -185,7 +189,7 @@ where
for itm in diff_kv_iterables(&prev.attributes, &self.attributes) { for itm in diff_kv_iterables(&prev.attributes, &self.attributes) {
match itm { match itm {
Diff::Add(name, value) | Diff::Change(name, value) => { Diff::Add(name, value) | Diff::Change(name, value) => {
set_attribute(element, name, value); set_attribute(element, name, &value.serialize());
changed |= ChangeFlags::OTHER_CHANGE; changed |= ChangeFlags::OTHER_CHANGE;
} }
Diff::Remove(name) => { Diff::Remove(name) => {

View File

@ -26,7 +26,7 @@ pub use class::class;
pub use context::{ChangeFlags, Cx}; pub use context::{ChangeFlags, Cx};
#[cfg(feature = "typed")] #[cfg(feature = "typed")]
pub use element::elements; pub use element::elements;
pub use element::{element, Element, ElementState}; pub use element::{element, AttributeValue, Element, ElementState, IntoAttributeValue};
#[cfg(feature = "typed")] #[cfg(feature = "typed")]
pub use event::events; pub use event::events;
pub use event::{on_event, Action, Event, OnEvent, OnEventState, OptionalAction}; pub use event::{on_event, Action, Event, OnEvent, OnEventState, OptionalAction};

View File

@ -26,12 +26,10 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View<Todo, TodoAction> + Vi
if editing { if editing {
class.push_str(" editing"); class.push_str(" editing");
} }
let mut input = el::input(()) let input = el::input(())
.attr("class", "toggle") .attr("class", "toggle")
.attr("type", "checkbox"); .attr("type", "checkbox")
if todo.completed { .attr("checked", todo.completed);
input.set_attr("checked", "checked");
};
el::li(( el::li((
el::div(( el::div((
@ -86,13 +84,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl View<AppState
) )
}); });
let filter_class = |filter| { let filter_class = |filter| (state.filter == filter).then_some("selected");
if state.filter == filter {
"selected"
} else {
""
}
};
let mut footer = el::footer(( let mut footer = el::footer((
el::span(( el::span((
@ -162,13 +154,11 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl View<AppState>
) )
}) })
.collect(); .collect();
let mut toggle_all = el::input(()) let toggle_all = el::input(())
.attr("id", "toggle-all") .attr("id", "toggle-all")
.attr("class", "toggle-all") .attr("class", "toggle-all")
.attr("type", "checkbox"); .attr("type", "checkbox")
if state.are_all_complete() { .attr("checked", state.are_all_complete());
toggle_all.set_attr("checked", "true");
}
let mut section = el::section(( let mut section = el::section((
toggle_all.on_click(|state: &mut AppState, _| state.toggle_all_complete()), toggle_all.on_click(|state: &mut AppState, _| state.toggle_all_complete()),
el::label(()).attr("for", "toggle-all"), el::label(()).attr("for", "toggle-all"),
@ -190,7 +180,7 @@ fn app_logic(state: &mut AppState) -> impl View<AppState> {
.attr("class", "new-todo") .attr("class", "new-todo")
.attr("placeholder", "What needs to be done?") .attr("placeholder", "What needs to be done?")
.attr("value", state.new_todo.clone()) .attr("value", state.new_todo.clone())
.attr("autofocus", "true"); .attr("autofocus", true);
el::div(( el::div((
el::header(( el::header((
el::h1("TODOs"), el::h1("TODOs"),