sycamore_web/
attributes.rs

1use crate::*;
2
3/// A trait that represents an attribute that can be set. This is not "attribute" in the HTML spec
4/// sense. It can also represent JS properties (and possibly more ...) that can be set on an HTML
5/// element.
6pub trait AttributeValue: AttributeValueBoxed + 'static {
7    fn set_self(self, el: &mut HtmlNode, name: Cow<'static, str>);
8}
9
10/// Type alias representing a possibly dynamic string value.
11pub type StringAttribute = MaybeDyn<Option<Cow<'static, str>>>;
12impl AttributeValue for StringAttribute {
13    fn set_self(self, el: &mut HtmlNode, name: Cow<'static, str>) {
14        el.set_attribute(name, self);
15    }
16}
17
18/// Type alias respresenting a possibly dynamic boolean value.
19pub type BoolAttribute = MaybeDyn<bool>;
20impl AttributeValue for BoolAttribute {
21    fn set_self(self, el: &mut HtmlNode, name: Cow<'static, str>) {
22        el.set_bool_attribute(name, self);
23    }
24}
25
26impl AttributeValue for MaybeDyn<JsValue> {
27    fn set_self(self, el: &mut HtmlNode, name: Cow<'static, str>) {
28        el.set_property(name, self);
29    }
30}
31
32/// Trait used to implement `AttributeValue` for `Box<dyn AttributeValue>`.
33#[doc(hidden)]
34pub trait AttributeValueBoxed: 'static {
35    fn set_self_boxed(self: Box<Self>, el: &mut HtmlNode, name: Cow<'static, str>);
36}
37
38impl<T> AttributeValueBoxed for T
39where
40    T: AttributeValue,
41{
42    fn set_self_boxed(self: Box<Self>, el: &mut HtmlNode, name: Cow<'static, str>) {
43        self.set_self(el, name);
44    }
45}
46
47impl AttributeValue for Box<dyn AttributeValue> {
48    fn set_self(self, el: &mut HtmlNode, name: Cow<'static, str>) {
49        self.set_self_boxed(el, name);
50    }
51}
52
53/// Implemented for all types that can accept attributes ([`AttributeValue`]).
54pub trait SetAttribute {
55    fn set_attribute(&mut self, name: &'static str, value: impl AttributeValue);
56    fn set_event_handler(
57        &mut self,
58        name: &'static str,
59        value: impl FnMut(web_sys::Event) + 'static,
60    );
61}
62
63impl<T> SetAttribute for T
64where
65    T: AsHtmlNode,
66{
67    fn set_attribute(&mut self, name: &'static str, value: impl AttributeValue) {
68        value.set_self(self.as_html_node(), name.into());
69    }
70
71    fn set_event_handler(
72        &mut self,
73        name: &'static str,
74        value: impl FnMut(web_sys::Event) + 'static,
75    ) {
76        self.as_html_node().set_event_handler(name.into(), value);
77    }
78}
79
80/// A special prop type that can be used to spread attributes onto an element.
81#[derive(Default)]
82pub struct Attributes {
83    values: Vec<(Cow<'static, str>, Box<dyn AttributeValue>)>,
84    #[allow(clippy::type_complexity)]
85    event_handlers: Vec<(Cow<'static, str>, Box<dyn FnMut(web_sys::Event)>)>,
86}
87
88impl SetAttribute for Attributes {
89    fn set_attribute(&mut self, name: &'static str, value: impl AttributeValue) {
90        self.values.push((name.into(), Box::new(value)));
91    }
92
93    fn set_event_handler(
94        &mut self,
95        name: &'static str,
96        value: impl FnMut(web_sys::Event) + 'static,
97    ) {
98        self.event_handlers.push((name.into(), Box::new(value)));
99    }
100}
101
102impl Attributes {
103    /// Create a new empty [`Attributes`] instance.
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    pub fn apply_self(self, el: &mut HtmlNode) {
109        for (name, value) in self.values {
110            value.set_self(el, name);
111        }
112        for (name, handler) in self.event_handlers {
113            el.set_event_handler(name, handler);
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use expect_test::{expect, Expect};
121
122    use super::*;
123
124    fn check<T: Into<View>>(view: impl FnOnce() -> T, expect: Expect) {
125        let actual = render_to_string(move || view().into());
126        expect.assert_eq(&actual);
127    }
128
129    #[test]
130    fn attributes_apply_self() {
131        let mut attributes = Attributes::new();
132        attributes.set_attribute("class", StringAttribute::from("test-class"));
133        attributes.set_attribute("id", StringAttribute::from(move || "test-id"));
134
135        check(
136            move || crate::tags::div().spread(attributes),
137            expect![[r#"<div class="test-class" id="test-id" data-hk="0.0"></div>"#]],
138        );
139    }
140
141    #[test]
142    fn attributes_apply_self_macro() {
143        let mut attributes = Attributes::new();
144        attributes.set_attribute("class", StringAttribute::from("test-class"));
145        attributes.set_attribute("id", StringAttribute::from(move || "test-id"));
146
147        check(
148            move || view! { div(..attributes) },
149            expect![[r#"<div class="test-class" id="test-id" data-hk="0.0"></div>"#]],
150        );
151    }
152}