Skip to main content

topcoat_view/
attribute.rs

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