Skip to main content

shelly/
component.rs

1use crate::{Context, Event, Html, LiveResult};
2use std::fmt;
3
4/// Stable server-owned identifier for a LiveComponent.
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct ComponentId(String);
7
8impl ComponentId {
9    pub fn new(id: impl Into<String>) -> Self {
10        Self(id.into())
11    }
12
13    pub fn as_str(&self) -> &str {
14        &self.0
15    }
16}
17
18impl fmt::Display for ComponentId {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        f.write_str(&self.0)
21    }
22}
23
24impl From<&str> for ComponentId {
25    fn from(value: &str) -> Self {
26        Self::new(value)
27    }
28}
29
30impl From<String> for ComponentId {
31    fn from(value: String) -> Self {
32        Self::new(value)
33    }
34}
35
36/// Render result for one component after a scoped event.
37#[derive(Debug, Clone, PartialEq)]
38pub struct ComponentRender {
39    id: ComponentId,
40    html: Html,
41}
42
43impl ComponentRender {
44    pub fn new(id: impl Into<ComponentId>, html: Html) -> Self {
45        Self {
46            id: id.into(),
47            html,
48        }
49    }
50
51    pub fn id(&self) -> &ComponentId {
52        &self.id
53    }
54
55    pub fn html(&self) -> &Html {
56        &self.html
57    }
58
59    pub fn into_parts(self) -> (ComponentId, Html) {
60        (self.id, self.html)
61    }
62}
63
64/// Trait implemented by server-owned child components.
65///
66/// Components own a smaller state boundary than a full LiveView. Parent views
67/// decide how children are stored and rendered, while the session runtime can
68/// patch a component target independently after a scoped event.
69pub trait LiveComponent: Send + 'static {
70    /// Stable id that maps to the component root DOM node.
71    fn id(&self) -> ComponentId;
72
73    /// Called by parent views when they initialize child component state.
74    fn mount(&mut self, _ctx: &mut Context) -> LiveResult {
75        Ok(())
76    }
77
78    /// Called by parent views when a scoped event targets this component.
79    fn handle_event(&mut self, _event: Event, _ctx: &mut Context) -> LiveResult {
80        Ok(())
81    }
82
83    /// Render this component root HTML.
84    fn render(&self) -> Html;
85}
86
87#[cfg(test)]
88mod tests {
89    use super::{ComponentId, ComponentRender, LiveComponent};
90    use crate::{Context, Event, Html};
91
92    #[derive(Default)]
93    struct StubComponent;
94
95    impl LiveComponent for StubComponent {
96        fn id(&self) -> ComponentId {
97            ComponentId::new("stub")
98        }
99
100        fn render(&self) -> Html {
101            Html::new("<div>stub</div>")
102        }
103    }
104
105    #[test]
106    fn component_id_and_render_parts_round_trip() {
107        let render = ComponentRender::new("item-1", Html::new("<p>x</p>"));
108        assert_eq!(render.id().as_str(), "item-1");
109        assert_eq!(render.id().to_string(), "item-1");
110        assert_eq!(render.html().as_str(), "<p>x</p>");
111
112        let (id, html) = render.into_parts();
113        assert_eq!(id.as_str(), "item-1");
114        assert_eq!(html.as_str(), "<p>x</p>");
115
116        let from_string = ComponentId::from(String::from("item-2"));
117        assert_eq!(from_string.as_str(), "item-2");
118    }
119
120    #[test]
121    fn live_component_default_callbacks_return_ok() {
122        let mut component = StubComponent;
123        let mut ctx = Context::new("root");
124        assert!(component.mount(&mut ctx).is_ok());
125        assert!(component.handle_event(Event::new("noop"), &mut ctx).is_ok());
126        assert_eq!(component.id().as_str(), "stub");
127        assert_eq!(component.render().as_str(), "<div>stub</div>");
128    }
129}