sauron_core/dom/
component.rs1use 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
15pub trait Component {
22 type MSG: 'static;
24 type XMSG: 'static;
26
27 fn init(&mut self) -> Effects<Self::MSG, Self::XMSG> {
29 Effects::none()
30 }
31
32 fn update(&mut self, msg: Self::MSG) -> Effects<Self::MSG, Self::XMSG>;
35
36 fn view(&self) -> Node<Self::MSG>;
38
39 fn stylesheet() -> Vec<String>
41 where
42 Self: Sized,
43 {
44 vec![]
45 }
46
47 fn observed_attributes() -> Vec<AttributeName> {
49 vec![]
50 }
51
52 fn style(&self) -> Vec<String> {
55 vec![]
56 }
57
58 fn component_name() -> String
61 where
62 Self: Sized,
63 {
64 extract_simple_struct_name::<Self>()
65 }
66
67 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 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 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 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 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#[derive_where(Debug)]
154pub struct StatelessModel<MSG> {
155 pub view: Box<Node<MSG>>,
157 pub type_id: TypeId,
159}
160
161impl<MSG> StatelessModel<MSG> {
162 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 pub fn attribute_value(&self, name: &AttributeName) -> Option<Vec<&AttributeValue<MSG>>> {
177 self.view.attribute_value(name)
178 }
179
180 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
201pub 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}