sauron_core/dom/
component.rs

1use crate::html::attributes::{class, classes, Attribute};
2use crate::vdom::AttributeName;
3use crate::vdom::AttributeValue;
4use crate::vdom::Leaf;
5use crate::{dom::Effects, vdom::Node};
6use derive_where::derive_where;
7use std::any::TypeId;
8
9#[cfg(feature = "with-dom")]
10pub use stateful_component::{stateful_component, StatefulComponent, StatefulModel};
11
12#[cfg(feature = "with-dom")]
13mod stateful_component;
14
15/// A component has a view and can update itself.
16///
17/// The update function returns an effect which can contain
18/// follow ups and effects. Follow ups are executed on the next
19/// update loop of this component, while the effects are executed
20/// on the parent component that mounts it.
21pub trait Component {
22    ///
23    type MSG: 'static;
24    ///
25    type XMSG: 'static;
26
27    /// init the component
28    fn init(&mut self) -> Effects<Self::MSG, Self::XMSG> {
29        Effects::none()
30    }
31
32    /// Update the model of this component and return
33    /// follow up and/or effects that will be executed on the next update loop
34    fn update(&mut self, msg: Self::MSG) -> Effects<Self::MSG, Self::XMSG>;
35
36    /// the view of the component
37    fn view(&self) -> Node<Self::MSG>;
38
39    /// component can have static styles
40    fn stylesheet() -> Vec<String>
41    where
42        Self: Sized,
43    {
44        vec![]
45    }
46
47    /// specify which attribute names are observed for this Component
48    fn observed_attributes() -> Vec<AttributeName> {
49        vec![]
50    }
51
52    /// in addition, component can contain dynamic style
53    /// which can change when the model is updated
54    fn style(&self) -> Vec<String> {
55        vec![]
56    }
57
58    /// return the component name
59    /// defaults to the struct simplified name
60    fn component_name() -> String
61    where
62        Self: Sized,
63    {
64        extract_simple_struct_name::<Self>()
65    }
66
67    /// prefix the class bane
68    fn prefix_class(class_name: &str) -> String
69    where
70        Self: Sized,
71    {
72        let component_name = Self::component_name();
73        if class_name.is_empty() {
74            component_name
75        } else {
76            format!("{component_name}__{class_name}")
77        }
78    }
79
80    /// create a classname prepended with this component name
81    fn class_ns(class_name: &str) -> Attribute<Self::MSG>
82    where
83        Self: Sized,
84    {
85        let class_names: Vec<&str> = class_name.split(' ').collect();
86        let prefixed_classes = class_names
87            .iter()
88            .map(|c| Self::prefix_class(c))
89            .collect::<Vec<_>>()
90            .join(" ");
91        class(prefixed_classes)
92    }
93
94    /// create namespaced class names to pair that evaluates to true
95    fn classes_ns_flag(
96        pair: impl IntoIterator<Item = (impl ToString, bool)>,
97    ) -> Attribute<Self::MSG>
98    where
99        Self: Sized,
100    {
101        let class_list = pair.into_iter().filter_map(|(class, flag)| {
102            if flag {
103                Some(Self::prefix_class(&class.to_string()))
104            } else {
105                None
106            }
107        });
108
109        classes(class_list)
110    }
111
112    /// create a selector class prepended with this component name
113    fn selector_ns(class_name: &str) -> String
114    where
115        Self: Sized,
116    {
117        let component_name = Self::component_name();
118        if class_name.is_empty() {
119            format!(".{component_name}")
120        } else {
121            format!(".{component_name}__{class_name}")
122        }
123    }
124
125    /// create namesspaced selector from multiple classnames
126    fn selectors_ns(class_names: impl IntoIterator<Item = impl ToString>) -> String
127    where
128        Self: Sized,
129    {
130        let selectors: Vec<String> = class_names
131            .into_iter()
132            .map(|class_name| Self::selector_ns(&class_name.to_string()))
133            .collect();
134        selectors.join(" ")
135    }
136}
137
138pub(crate) fn extract_simple_struct_name<T: ?Sized>() -> String {
139    let type_name = std::any::type_name::<T>();
140    let name = if let Some(first) = type_name.split(['<', '>']).next() {
141        first
142    } else {
143        type_name
144    };
145    name.rsplit("::")
146        .next()
147        .map(|s| s.to_string())
148        .expect("must have a name")
149}
150
151/// Contains necessary information for creating template
152/// of the Component of this type_id
153#[derive_where(Debug)]
154pub struct StatelessModel<MSG> {
155    /// the view of this stateless model
156    pub view: Box<Node<MSG>>,
157    /// component type id
158    pub type_id: TypeId,
159}
160
161impl<MSG> StatelessModel<MSG> {
162    /// mape the msg of this Leaf such that `Leaf<MSG>` becomes `Leaf<MSG2>`
163    pub fn map_msg<F, MSG2>(self, cb: F) -> StatelessModel<MSG2>
164    where
165        F: Fn(MSG) -> MSG2 + Clone + 'static,
166        MSG2: 'static,
167        MSG: 'static,
168    {
169        StatelessModel {
170            type_id: self.type_id,
171            view: Box::new(self.view.map_msg(cb.clone())),
172        }
173    }
174
175    /// return the attribute values of the view node matching the attribute name `name`
176    pub fn attribute_value(&self, name: &AttributeName) -> Option<Vec<&AttributeValue<MSG>>> {
177        self.view.attribute_value(name)
178    }
179
180    ///
181    pub fn attributes(&self) -> Option<&[Attribute<MSG>]> {
182        self.view.attributes()
183    }
184}
185
186impl<MSG> Clone for StatelessModel<MSG> {
187    fn clone(&self) -> Self {
188        Self {
189            view: self.view.clone(),
190            type_id: self.type_id,
191        }
192    }
193}
194
195impl<MSG> PartialEq for StatelessModel<MSG> {
196    fn eq(&self, other: &Self) -> bool {
197        self.view == other.view && self.type_id == other.type_id
198    }
199}
200
201/// create a stateless component node
202pub fn component<COMP>(app: &COMP) -> Node<COMP::MSG>
203where
204    COMP: Component + 'static,
205{
206    let type_id = TypeId::of::<COMP>();
207    let app_view = app.view();
208    Node::Leaf(Leaf::StatelessComponent(StatelessModel {
209        view: Box::new(app_view),
210        type_id,
211    }))
212}
213
214#[cfg(test)]
215mod test {
216    use super::*;
217    use crate::html::*;
218    use std::marker::PhantomData;
219
220    #[test]
221    fn test_extract_component_name() {
222        enum Msg {}
223        struct AwesomeEditor {}
224
225        impl Component for AwesomeEditor {
226            type MSG = Msg;
227            type XMSG = ();
228
229            fn update(&mut self, _msg: Msg) -> Effects<Msg, ()> {
230                Effects::none()
231            }
232            fn view(&self) -> Node<Msg> {
233                div([], [])
234            }
235        }
236
237        let name = extract_simple_struct_name::<AwesomeEditor>();
238        assert_eq!("AwesomeEditor", name);
239    }
240
241    #[test]
242    fn test_name_with_generics() {
243        struct ComplexEditor<XMSG> {
244            _phantom2: PhantomData<XMSG>,
245        }
246
247        enum Xmsg {}
248
249        let name = extract_simple_struct_name::<ComplexEditor<Xmsg>>();
250        assert_eq!("ComplexEditor", name);
251    }
252
253    #[test]
254    fn test_name_with_2_generics() {
255        struct ComplexEditor<MSG, XMSG> {
256            _phantom1: PhantomData<MSG>,
257            _phantom2: PhantomData<XMSG>,
258        }
259
260        enum Msg {}
261        enum Xmsg {}
262
263        let name = extract_simple_struct_name::<ComplexEditor<Msg, Xmsg>>();
264        assert_eq!("ComplexEditor", name);
265    }
266}