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(
|
||||
mut self,
|
||||
name: impl Into<std::borrow::Cow<'static, str>>,
|
||||
value: impl Into<std::borrow::Cow<'static, str>>,
|
||||
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<std::borrow::Cow<'static, str>>,
|
||||
value: impl Into<std::borrow::Cow<'static, str>>,
|
||||
value: impl crate::IntoAttributeValue,
|
||||
) -> &mut Self {
|
||||
self.0.set_attr(name, value);
|
||||
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 {
|
||||
self.0 = self.0.after_update(after_update);
|
||||
self
|
||||
|
|
|
@ -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<El, Children = ()> {
|
||||
name: Cow<'static, str>,
|
||||
attributes: VecMap<Cow<'static, str>, Cow<'static, str>>,
|
||||
name: CowStr,
|
||||
attributes: VecMap<CowStr, AttributeValue>,
|
||||
children: Children,
|
||||
#[allow(clippy::type_complexity)]
|
||||
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 {
|
||||
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<ViewSeqState> {
|
|||
/// Create a new element view
|
||||
///
|
||||
/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`).
|
||||
pub fn element<El, ViewSeq>(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
children: ViewSeq,
|
||||
) -> Element<El, ViewSeq> {
|
||||
pub fn element<El, ViewSeq>(name: impl Into<CowStr>, children: ViewSeq) -> Element<El, ViewSeq> {
|
||||
Element {
|
||||
name: name.into(),
|
||||
attributes: VecMap::default(),
|
||||
attributes: Default::default(),
|
||||
children,
|
||||
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,
|
||||
/// then the `View::build`/`View::rebuild` functions will panic for this view.
|
||||
pub fn attr(
|
||||
mut self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
pub fn attr(mut self, name: impl Into<CowStr>, value: impl IntoAttributeValue) -> Self {
|
||||
self.set_attr(name, value);
|
||||
self
|
||||
}
|
||||
|
@ -89,12 +87,17 @@ impl<El, ViewSeq> Element<El, ViewSeq> {
|
|||
///
|
||||
/// 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<Cow<'static, str>>,
|
||||
value: impl Into<Cow<'static, str>>,
|
||||
) {
|
||||
self.attributes.insert(name.into(), value.into());
|
||||
pub fn set_attr(&mut self, name: impl Into<CowStr>, 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<CowStr>) {
|
||||
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) => {
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -26,12 +26,10 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View<Todo, TodoAction> + 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<AppState
|
|||
)
|
||||
});
|
||||
|
||||
let filter_class = |filter| {
|
||||
if state.filter == filter {
|
||||
"selected"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
};
|
||||
let filter_class = |filter| (state.filter == filter).then_some("selected");
|
||||
|
||||
let mut footer = el::footer((
|
||||
el::span((
|
||||
|
@ -162,13 +154,11 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl View<AppState>
|
|||
)
|
||||
})
|
||||
.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<AppState> {
|
|||
.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"),
|
||||
|
|
Loading…
Reference in New Issue