Skip to main content

topcoat_view/html/
attribute.rs

1mod attributes;
2mod collector;
3mod key;
4mod value;
5
6pub use attributes::*;
7pub(crate) use collector::*;
8pub use key::*;
9use topcoat_core::context::Cx;
10pub use value::*;
11
12use crate::{HtmlContext, PartsWriter};
13
14/// A single HTML attribute.
15///
16/// The value decides whether the attribute is present. For example, `None`
17/// and `false` values omit the attribute.
18#[derive(Debug, Clone)]
19pub struct Attribute<K, V> {
20    key: K,
21    value: V,
22}
23
24impl<K, V> Attribute<K, V> {
25    /// Creates an attribute from a key and value.
26    #[inline]
27    pub fn new(key: K, value: V) -> Self {
28        Self { key, value }
29    }
30}
31
32/// Converts one or more attributes into view parts.
33///
34/// Implement this trait to spread a type's attributes into an element in
35/// [`view!`](https://docs.rs/topcoat/latest/topcoat/view/macro.view.html):
36///
37/// ```rust
38/// # use topcoat::view::{Attributes, View, component, view};
39/// # #[component]
40/// # async fn example() -> topcoat::Result<impl View> {
41/// # let my_value = Attributes::new();
42/// Ok(view! {
43///     <input (my_value)>
44/// })
45/// # }
46/// ```
47///
48/// The emitted view parts must contain a leading space for each attribute to separate them from
49/// the element name or preceding attributes.
50pub trait AttributeViewParts {
51    /// Appends zero or more attributes to the view being built.
52    fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>);
53}
54
55impl<K, V> AttributeViewParts for Attribute<K, V>
56where
57    K: AttributeKeyViewParts,
58    V: AttributeValueViewParts,
59{
60    fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
61        if self.value.attribute_present() {
62            parts.push_promoted_str_unescaped(&" ");
63            parts.in_context(HtmlContext::AttributeKey, |parts| {
64                self.key.into_view_parts(cx, parts);
65            });
66            parts.push_promoted_str_unescaped(&"=\"");
67            parts.in_context(HtmlContext::AttributeValue, |parts| {
68                self.value.into_view_parts(cx, parts);
69            });
70            parts.push_promoted_str_unescaped(&"\"");
71        }
72    }
73}
74
75impl<T> AttributeViewParts for Option<T>
76where
77    T: AttributeViewParts,
78{
79    #[inline]
80    fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
81        if let Some(value) = self {
82            value.into_view_parts(cx, parts);
83        }
84    }
85}
86
87impl<T> AttributeViewParts for Vec<T>
88where
89    T: AttributeViewParts,
90{
91    #[inline]
92    fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
93        for value in self {
94            value.into_view_parts(cx, parts);
95        }
96    }
97}
98
99impl<'b, T: ?Sized> AttributeViewParts for &&'b T
100where
101    &'b T: AttributeViewParts,
102{
103    #[inline]
104    fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
105        (*self).into_view_parts(cx, parts);
106    }
107}
108
109macro_rules! impl_tuple {
110    ($($ty:ident),+) => {
111        impl<$($ty),+> AttributeViewParts for ($($ty,)+)
112        where
113            $($ty: AttributeViewParts,)+
114        {
115            #[inline]
116            #[allow(non_snake_case)]
117            fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
118                let ($($ty,)+) = self;
119                $($ty.into_view_parts(cx, parts);)+
120            }
121        }
122    };
123}
124
125impl_tuple!(T1);
126impl_tuple!(T1, T2);
127impl_tuple!(T1, T2, T3);
128impl_tuple!(T1, T2, T3, T4);
129impl_tuple!(T1, T2, T3, T4, T5);
130impl_tuple!(T1, T2, T3, T4, T5, T6);
131impl_tuple!(T1, T2, T3, T4, T5, T6, T7);
132impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
133impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
134impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
135impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
136impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::internal::Builder;
142
143    fn render(attribute: impl AttributeViewParts) -> String {
144        let cx = Cx::default();
145        Builder::build(&cx, |b| b.attributes(attribute)).render(&cx)
146    }
147
148    #[test]
149    fn renders_key_and_escaped_value() {
150        let rendered = render(Attribute::new("data-x", "a\"b<c"));
151        assert_eq!(rendered, " data-x=\"a&quot;b<c\"");
152    }
153
154    #[test]
155    fn omits_absent_value() {
156        assert_eq!(render(Attribute::new("disabled", false)), "");
157    }
158
159    #[test]
160    fn dynamic_key_is_validated() {
161        let rendered = render(Attribute::new(String::from("data-x"), "y"));
162        assert_eq!(rendered, " data-x=\"y\"");
163    }
164
165    #[test]
166    #[should_panic(expected = "invalid attribute key")]
167    fn dynamic_key_rejects_breakout() {
168        render(Attribute::new(String::from("x onmouseover"), "y"));
169    }
170}