mirror of https://github.com/linebender/xilem
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:
parent
5600017d4a
commit
09271e7beb
|
@ -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()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
Loading…
Reference in New Issue