cabin/html/elements/
common.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::ops::{Add, AddAssign};
4
5use cabin_macros::Attribute;
6
7use super::SerializeEventFn;
8use crate::error::InternalError;
9use crate::html::attributes::{Attributes, WithAttribute};
10
11pub trait Common: WithAttribute {
12    /// Unique identifier across the document.
13    fn id(self, id: impl Into<Cow<'static, str>>) -> Self::Output<Id> {
14        self.with_attribute(Id(id.into()))
15    }
16
17    /// The various classes that the element belongs to.
18    fn class(mut self, class: impl Into<Cow<'static, str>>) -> Self::Output<Class> {
19        let class = if let Some(existing) = self.get_attribute_mut::<Class>() {
20            Class(Cow::Owned(format!("{} {}", existing.0, class.into())))
21        } else {
22            Class(class.into())
23        };
24        self.with_attribute(class)
25    }
26
27    fn replace_class(self, class: impl Into<Cow<'static, str>>) -> Self::Output<Class> {
28        self.with_attribute(Class(class.into()))
29    }
30
31    fn on_click<E>(self, event: E) -> Self::Output<OnClick>
32    where
33        E: serde::Serialize + 'static,
34    {
35        self.with_attribute(OnClick(Box::new(move || {
36            use std::hash::{Hash, Hasher};
37
38            let mut hasher = twox_hash::XxHash32::default();
39            std::any::TypeId::of::<E>().hash(&mut hasher);
40            let hash = hasher.finish() as u32;
41            serde_json::to_string(&event)
42                .map_err(|err| InternalError::Serialize {
43                    what: "on_click event",
44                    err,
45                })
46                .map(|json| (hash, json))
47        })))
48    }
49}
50
51/// Unique identifier across the document.
52#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Attribute)]
53pub struct Id(pub Cow<'static, str>);
54
55/// The various classes that the element belongs to.
56// FIXME: make it Copy
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Attribute)]
58pub struct Class(pub Cow<'static, str>);
59
60pub struct OnClick(pub Box<SerializeEventFn>);
61
62impl Attributes for OnClick {
63    fn render(self, r: &mut crate::render::ElementRenderer) -> Result<(), crate::Error> {
64        // TODO: directly write into el?
65        let (id, payload) = &(self.0)()?;
66        r.attribute("cabin-click", id)
67            .map_err(crate::error::InternalError::from)?;
68        r.attribute("cabin-click-payload", payload)
69            .map_err(crate::error::InternalError::from)?;
70
71        Ok(())
72    }
73}
74
75impl Class {
76    pub fn append(self, other: Class) -> Class {
77        self + other
78    }
79
80    pub fn append_when(self, condition: bool, other: Class) -> Class {
81        if !condition {
82            self
83        } else {
84            self + other
85        }
86    }
87}
88
89impl Default for Class {
90    fn default() -> Self {
91        Class(Cow::Borrowed(""))
92    }
93}
94
95impl From<Class> for Cow<'static, str> {
96    fn from(value: Class) -> Self {
97        value.0
98    }
99}
100
101impl From<Option<&'static str>> for Class {
102    fn from(value: Option<&'static str>) -> Self {
103        Class(value.map(Cow::Borrowed).unwrap_or(Cow::Borrowed("")))
104    }
105}
106
107impl Add<Class> for Class {
108    type Output = Class;
109
110    fn add(self, rhs: Class) -> Self::Output {
111        // FIXME: avoid allocation
112        Class(Cow::Owned(format!("{self} {rhs}")))
113    }
114}
115
116impl Add<Option<Class>> for Class {
117    type Output = Class;
118
119    fn add(self, rhs: Option<Class>) -> Self::Output {
120        if let Some(rhs) = rhs {
121            // FIXME: avoid allocation
122            Class(Cow::Owned(format!("{self} {rhs}")))
123        } else {
124            self
125        }
126    }
127}
128
129impl Add<Class> for Option<Class> {
130    type Output = Class;
131
132    fn add(self, rhs: Class) -> Self::Output {
133        if let Some(lhs) = self {
134            // FIXME: avoid allocation
135            Class(Cow::Owned(format!("{lhs} {rhs}")))
136        } else {
137            rhs
138        }
139    }
140}
141
142impl AddAssign for Class {
143    fn add_assign(&mut self, rhs: Self) {
144        // FIXME: avoid allocation
145        *self = Class(Cow::Owned(format!("{self} {rhs}")))
146    }
147}
148
149impl fmt::Display for Class {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        self.0.fmt(f)
152    }
153}