terrazzo_client/
attribute.rs

1//! Attributes of generated HTML elements
2
3use nameth::NamedType as _;
4use nameth::nameth;
5use wasm_bindgen::JsCast as _;
6use web_sys::Element;
7use web_sys::HtmlElement;
8
9use self::inner::AttributeTemplateInner;
10use crate::debug_correlation_id::DebugCorrelationId;
11use crate::prelude::OrElseLog;
12use crate::prelude::diagnostics::trace;
13use crate::prelude::diagnostics::warn;
14use crate::signal::depth::Depth;
15use crate::signal::reactive_closure::reactive_closure_builder::Consumers;
16use crate::string::XString;
17use crate::template::IsTemplate;
18use crate::template::IsTemplated;
19use crate::utils::Ptr;
20
21/// Represents an attribute of an HTML node.
22///
23/// Example: the HTML tag `<input type="text" name="username" value="LamparoS@Pavy.one" />`
24/// would have an attribute
25/// ```
26/// # use terrazzo_client::prelude::XAttribute;
27/// # let _ =
28/// XAttribute {
29///     name: "name".into(),
30///     value: "username".into(),
31/// }
32/// # ;
33/// ```
34///
35/// and an attribute
36/// ```
37/// # use terrazzo_client::prelude::XAttribute;
38/// # let _ =
39/// XAttribute {
40///     name: "value".into(),
41///     value: "LamparoS@Pavy.one".into(),
42/// }
43/// # ;
44/// ```
45#[nameth]
46pub struct XAttribute {
47    /// Name of the attribute
48    pub name: XAttributeName,
49
50    /// Value of the attribute
51    pub value: XAttributeValue,
52}
53
54/// Represents the name of an [XAttribute].
55#[derive(Clone, PartialEq, Eq, Hash)]
56pub enum XAttributeName {
57    /// Represents the name of an HTML attribute.
58    Attribute(XString),
59
60    /// Represents the name of a CSS property.
61    ///
62    /// Example:
63    /// `<div style="width:100%'> ... </div>`
64    /// would have the following [XAttribute]:
65    /// ```
66    /// # use terrazzo_client::prelude::*;
67    /// # let _ =
68    /// XAttribute {
69    ///     name: XAttributeName::Style("width".into()).into(),
70    ///     value: "100%".into(),
71    /// }
72    /// # ;
73    /// ```
74    Style(XString),
75}
76
77/// Represents the value of an [XAttribute].
78///
79/// Usually the `#[template]` macro takes care of generating the code for [XAttributeValue]s.
80pub enum XAttributeValue {
81    /// When the value is not set, like [Option::None].
82    Null,
83
84    /// When the attribute as some value.
85    Static(XString),
86
87    /// When the attribute must be computed by some reactive closure.
88    Dynamic(XDynamicAttribute),
89
90    /// When the dynamic attribute is computed and owned by the reactive closure.
91    Generated {
92        template: XAttributeTemplate,
93        consumers: Consumers,
94    },
95}
96
97/// Represents the callback that generates a dynamic [XAttribute].
98pub struct XDynamicAttribute(pub Box<dyn Fn(XAttributeTemplate) -> Consumers>);
99
100impl<F: Fn(XAttributeTemplate) -> Consumers + 'static> From<F> for XDynamicAttribute {
101    fn from(value: F) -> Self {
102        let _ = XAttribute {
103            name: "type".into(),
104            value: "text".into(),
105        };
106        Self(Box::new(value))
107    }
108}
109
110/// Represents the template that generates a dynamic [XAttribute].
111#[derive(Clone)]
112pub struct XAttributeTemplate(Ptr<AttributeTemplateInner>);
113
114mod inner {
115    use std::ops::Deref;
116
117    use web_sys::Element;
118
119    use super::XAttributeName;
120    use super::XAttributeTemplate;
121    use crate::debug_correlation_id::DebugCorrelationId;
122    use crate::signal::depth::Depth;
123
124    pub struct AttributeTemplateInner {
125        pub element: Element,
126        pub attribute_name: XAttributeName,
127        pub(super) debug_id: DebugCorrelationId<String>,
128        pub(super) depth: Depth,
129    }
130
131    impl Deref for XAttributeTemplate {
132        type Target = AttributeTemplateInner;
133
134        fn deref(&self) -> &Self::Target {
135            &self.0
136        }
137    }
138}
139
140impl IsTemplate for XAttributeTemplate {
141    type Value = XAttributeValue;
142
143    fn apply<R: Into<Self::Value>>(self, new: impl FnOnce() -> R) {
144        let mut new = XAttribute {
145            name: self.attribute_name.clone(),
146            value: new().into(),
147        };
148        new.merge(self.depth, None, &self.element);
149    }
150
151    fn depth(&self) -> Depth {
152        self.depth
153    }
154
155    fn debug_id(&self) -> &DebugCorrelationId<impl std::fmt::Display> {
156        &self.debug_id
157    }
158}
159
160impl IsTemplated for XAttributeValue {
161    type Template = XAttributeTemplate;
162}
163
164impl<T> From<T> for XAttributeValue
165where
166    XString: From<T>,
167{
168    fn from(value: T) -> Self {
169        Self::Static(value.into())
170    }
171}
172
173impl<T> From<Option<T>> for XAttributeValue
174where
175    XString: From<T>,
176{
177    fn from(value: Option<T>) -> Self {
178        match value {
179            Some(value) => Self::Static(value.into()),
180            None => Self::Null,
181        }
182    }
183}
184
185impl XAttribute {
186    pub fn merge(
187        &mut self,
188        depth: Depth,
189        old_attribute_value: Option<XAttributeValue>,
190        element: &Element,
191    ) {
192        let new_attribute = self;
193        let attribute_name = &new_attribute.name;
194        match &new_attribute.value {
195            XAttributeValue::Null => {
196                merge_static_atttribute(element, attribute_name, None, old_attribute_value);
197            }
198            XAttributeValue::Static(new_attribute_value) => {
199                merge_static_atttribute(
200                    element,
201                    attribute_name,
202                    Some(new_attribute_value),
203                    old_attribute_value,
204                );
205            }
206            XAttributeValue::Dynamic(XDynamicAttribute(new_attribute_value)) => {
207                new_attribute.value = merge_dynamic_atttribute(
208                    depth,
209                    element,
210                    attribute_name,
211                    new_attribute_value,
212                    old_attribute_value,
213                );
214            }
215            XAttributeValue::Generated { .. } => {
216                warn!("Illegal {} state", XAttribute::type_name());
217                debug_assert!(false);
218            }
219        }
220    }
221}
222
223fn merge_static_atttribute(
224    element: &Element,
225    attribute_name: &XAttributeName,
226    new_value: Option<&XString>,
227    old_value: Option<XAttributeValue>,
228) {
229    if let Some((XAttributeValue::Static(old_attribute_value), new_value)) =
230        old_value.as_ref().zip(new_value)
231    {
232        if new_value == old_attribute_value {
233            trace!("Attribute '{attribute_name}' is still '{new_value}'");
234            return;
235        }
236    }
237    drop(old_value);
238    let Some(new_value) = new_value else {
239        match attribute_name {
240            XAttributeName::Attribute(name) => match element.remove_attribute(name.as_str()) {
241                Ok(()) => trace!("Removed attribute {name}"),
242                Err(error) => warn!("Removed attribute {name} failed: {error:?}"),
243            },
244            XAttributeName::Style(name) => {
245                let html_element: &HtmlElement = element.dyn_ref().or_throw("HtmlElement");
246                let style = html_element.style();
247                match style.remove_property(name) {
248                    Ok(value) => trace!("Removed style {name}: {value}"),
249                    Err(error) => warn!("Removed style {name} failed: {error:?}"),
250                }
251            }
252        }
253        return;
254    };
255    match attribute_name {
256        XAttributeName::Attribute(name) => match element.set_attribute(name, new_value) {
257            Ok(()) => trace!("Set attribute '{name}' to '{new_value}'"),
258            Err(error) => warn!("Set attribute '{name}' to '{new_value}' failed: {error:?}"),
259        },
260        XAttributeName::Style(name) => {
261            let html_element: &HtmlElement = element.dyn_ref().or_throw("HtmlElement");
262            let style = html_element.style();
263            match style.set_property(name, new_value) {
264                Ok(()) => trace!("Set style {name}: {new_value}"),
265                Err(error) => warn!("Set style {name}: {new_value} failed: {error:?}"),
266            }
267        }
268    }
269}
270
271fn merge_dynamic_atttribute(
272    depth: Depth,
273    element: &Element,
274    attribute_name: &XAttributeName,
275    new_attribute_value: &dyn Fn(XAttributeTemplate) -> Consumers,
276    old_attribute_value: Option<XAttributeValue>,
277) -> XAttributeValue {
278    let new_template = if let Some(XAttributeValue::Generated {
279        template: old_template,
280        consumers: old_consumers,
281    }) = old_attribute_value
282    {
283        trace!("Reuse exising attribute template {attribute_name}");
284        drop(old_consumers);
285        old_template
286    } else {
287        trace!("Create a new attribute template {attribute_name}");
288        XAttributeTemplate(Ptr::new(AttributeTemplateInner {
289            element: element.clone(),
290            attribute_name: attribute_name.clone(),
291            debug_id: DebugCorrelationId::new(|| format!("attribute_template:{attribute_name}")),
292            depth: depth.next(),
293        }))
294    };
295    XAttributeValue::Generated {
296        template: new_template.clone(),
297        consumers: new_attribute_value(new_template),
298    }
299}
300
301impl<T> From<T> for XAttributeName
302where
303    T: Into<XString>,
304{
305    fn from(value: T) -> Self {
306        Self::Attribute(value.into())
307    }
308}
309
310impl std::fmt::Display for XAttributeName {
311    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312        match self {
313            Self::Attribute(name) => std::fmt::Display::fmt(name, f),
314            Self::Style(name) => write!(f, "style::{name}"),
315        }
316    }
317}