terrazzo_client/
attribute.rs1use 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#[nameth]
46pub struct XAttribute {
47    pub name: XAttributeName,
49
50    pub value: XAttributeValue,
52}
53
54#[derive(Clone, PartialEq, Eq, Hash)]
56pub enum XAttributeName {
57    Attribute(XString),
59
60    Style(XString),
75}
76
77pub enum XAttributeValue {
81    Null,
83
84    Static(XString),
86
87    Dynamic(XDynamicAttribute),
89
90    Generated {
92        template: XAttributeTemplate,
93        consumers: Consumers,
94    },
95}
96
97pub 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#[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}