orbit/component/
composition.rs

1//! Component composition patterns for Orbit UI framework
2//!
3//! This module provides utilities for composing components together in various patterns
4//! including render props, slots, and compound components.
5
6use std::collections::HashMap;
7
8use crate::component::{Component, ComponentError, ComponentId, Context, Node};
9
10/// Trait for components that can render other components (render props pattern)
11pub trait RenderProp<T> {
12    /// Render function that takes data and returns nodes
13    fn render(&self, data: T) -> Result<Vec<Node>, ComponentError>;
14}
15
16/// Implementation of render prop for function pointers
17impl<T, F> RenderProp<T> for F
18where
19    F: Fn(T) -> Result<Vec<Node>, ComponentError>,
20{
21    fn render(&self, data: T) -> Result<Vec<Node>, ComponentError> {
22        self(data)
23    }
24}
25
26/// A component that uses render props pattern
27pub struct RenderPropComponent<T, R>
28where
29    R: RenderProp<T>,
30{
31    component_id: ComponentId,
32    data: T,
33    renderer: R,
34    #[allow(dead_code)]
35    context: Context,
36}
37
38impl<T, R> RenderPropComponent<T, R>
39where
40    T: Clone + Send + Sync + 'static,
41    R: RenderProp<T> + Send + Sync + 'static,
42{
43    pub fn new(data: T, renderer: R, context: Context) -> Self {
44        Self {
45            component_id: ComponentId::new(),
46            data,
47            renderer,
48            context,
49        }
50    }
51}
52
53/// Props for render prop component
54#[derive(Clone)]
55pub struct RenderPropProps<T, R>
56where
57    T: Clone,
58    R: Clone,
59{
60    pub data: T,
61    pub renderer: R,
62}
63
64// Note: Props implementation is handled by the generic impl in mod.rs
65
66impl<T, R> Component for RenderPropComponent<T, R>
67where
68    T: Clone + Send + Sync + 'static,
69    R: RenderProp<T> + Clone + Send + Sync + 'static,
70{
71    type Props = RenderPropProps<T, R>;
72
73    fn component_id(&self) -> ComponentId {
74        self.component_id
75    }
76
77    fn create(props: Self::Props, context: Context) -> Self {
78        Self::new(props.data, props.renderer, context)
79    }
80
81    fn update(&mut self, props: Self::Props) -> Result<(), ComponentError> {
82        self.data = props.data;
83        self.renderer = props.renderer;
84        Ok(())
85    }
86
87    fn render(&self) -> Result<Vec<Node>, ComponentError> {
88        self.renderer.render(self.data.clone())
89    }
90
91    fn as_any(&self) -> &dyn std::any::Any {
92        self
93    }
94
95    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
96        self
97    }
98}
99
100/// Slot-based composition system
101#[derive(Debug, Clone)]
102pub struct Slot {
103    pub name: String,
104    pub nodes: Vec<Node>,
105    pub required: bool,
106}
107
108impl Slot {
109    pub fn new(name: impl Into<String>) -> Self {
110        Self {
111            name: name.into(),
112            nodes: Vec::new(),
113            required: false,
114        }
115    }
116
117    pub fn required(mut self) -> Self {
118        self.required = true;
119        self
120    }
121
122    pub fn with_nodes(mut self, nodes: Vec<Node>) -> Self {
123        self.nodes = nodes;
124        self
125    }
126}
127
128/// Props for slotted components
129#[derive(Clone)]
130pub struct SlottedProps {
131    pub slots: HashMap<String, Slot>,
132}
133
134impl SlottedProps {
135    pub fn new() -> Self {
136        Self {
137            slots: HashMap::new(),
138        }
139    }
140
141    pub fn with_slot(mut self, slot: Slot) -> Self {
142        self.slots.insert(slot.name.clone(), slot);
143        self
144    }
145
146    pub fn get_slot(&self, name: &str) -> Option<&Slot> {
147        self.slots.get(name)
148    }
149    pub fn get_slot_nodes(&self, name: &str) -> Vec<Node> {
150        self.slots
151            .get(name)
152            .map(|slot| slot.nodes.clone())
153            .unwrap_or_default()
154    }
155}
156
157impl Default for SlottedProps {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163/// Trait for components that support slots
164pub trait Slotted {
165    /// Get the slots this component supports
166    fn supported_slots(&self) -> Vec<String>;
167
168    /// Validate that all required slots are provided
169    fn validate_slots(&self, props: &SlottedProps) -> Result<(), ComponentError> {
170        for slot_name in self.supported_slots() {
171            if let Some(slot) = props.get_slot(&slot_name) {
172                if slot.required && slot.nodes.is_empty() {
173                    return Err(ComponentError::InvalidProps(format!(
174                        "Required slot '{slot_name}' is empty"
175                    )));
176                }
177            }
178        }
179        Ok(())
180    }
181
182    /// Render with slots
183    fn render_with_slots(&self, props: &SlottedProps) -> Result<Vec<Node>, ComponentError>;
184}
185
186/// A basic slotted component implementation
187pub struct SlottedComponent {
188    component_id: ComponentId,
189    supported_slots: Vec<String>,
190    #[allow(dead_code)]
191    context: Context,
192}
193
194impl SlottedComponent {
195    pub fn new(supported_slots: Vec<String>, context: Context) -> Self {
196        Self {
197            component_id: ComponentId::new(),
198            supported_slots,
199            context,
200        }
201    }
202}
203
204impl Component for SlottedComponent {
205    type Props = SlottedProps;
206
207    fn component_id(&self) -> ComponentId {
208        self.component_id
209    }
210
211    fn create(props: Self::Props, context: Context) -> Self {
212        // Extract supported slots from props
213        let supported_slots = props.slots.keys().cloned().collect();
214        Self::new(supported_slots, context)
215    }
216
217    fn update(&mut self, _props: Self::Props) -> Result<(), ComponentError> {
218        Ok(())
219    }
220
221    fn render(&self) -> Result<Vec<Node>, ComponentError> {
222        // Default implementation - override for custom behavior
223        Ok(Vec::new())
224    }
225
226    fn as_any(&self) -> &dyn std::any::Any {
227        self
228    }
229
230    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
231        self
232    }
233}
234
235impl Slotted for SlottedComponent {
236    fn supported_slots(&self) -> Vec<String> {
237        self.supported_slots.clone()
238    }
239
240    fn render_with_slots(&self, props: &SlottedProps) -> Result<Vec<Node>, ComponentError> {
241        self.validate_slots(props)?;
242
243        let mut nodes = Vec::new();
244        for slot_name in &self.supported_slots {
245            nodes.extend(props.get_slot_nodes(slot_name));
246        }
247        Ok(nodes)
248    }
249}
250
251/// Compound component pattern - a component made of multiple sub-components
252pub trait CompoundComponent {
253    type SubComponents;
254
255    /// Get the sub-components of this compound component
256    fn sub_components(&self) -> &Self::SubComponents;
257
258    /// Get a mutable reference to sub-components
259    fn sub_components_mut(&mut self) -> &mut Self::SubComponents;
260
261    /// Render all sub-components in order
262    fn render_compound(&self) -> Result<Vec<Node>, ComponentError>;
263}
264
265/// A flexible compound component implementation
266pub struct FlexibleCompoundComponent {
267    component_id: ComponentId,
268    sub_components: Vec<Box<dyn Component<Props = FlexibleCompoundProps>>>,
269    #[allow(dead_code)]
270    context: Context,
271}
272
273#[derive(Clone)]
274pub struct FlexibleCompoundProps {
275    pub children: Vec<FlexibleCompoundProps>,
276}
277
278impl FlexibleCompoundComponent {
279    pub fn new(context: Context) -> Self {
280        Self {
281            component_id: ComponentId::new(),
282            sub_components: Vec::new(),
283            context,
284        }
285    }
286
287    pub fn add_sub_component(
288        &mut self,
289        component: Box<dyn Component<Props = FlexibleCompoundProps>>,
290    ) {
291        self.sub_components.push(component);
292    }
293}
294
295impl Component for FlexibleCompoundComponent {
296    type Props = FlexibleCompoundProps;
297
298    fn component_id(&self) -> ComponentId {
299        self.component_id
300    }
301    fn create(_props: Self::Props, context: Context) -> Self {
302        // Create compound component with sub-components based on props
303
304        // Add logic here to create sub-components from props
305        Self::new(context)
306    }
307
308    fn update(&mut self, props: Self::Props) -> Result<(), ComponentError> {
309        // Update all sub-components
310        for (i, child_props) in props.children.iter().enumerate() {
311            if let Some(sub_component) = self.sub_components.get_mut(i) {
312                sub_component.update(child_props.clone())?;
313            }
314        }
315        Ok(())
316    }
317
318    fn render(&self) -> Result<Vec<Node>, ComponentError> {
319        let mut all_nodes = Vec::new();
320        for sub_component in &self.sub_components {
321            all_nodes.extend(sub_component.render()?);
322        }
323        Ok(all_nodes)
324    }
325
326    fn as_any(&self) -> &dyn std::any::Any {
327        self
328    }
329
330    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
331        self
332    }
333}
334
335impl CompoundComponent for FlexibleCompoundComponent {
336    type SubComponents = Vec<Box<dyn Component<Props = FlexibleCompoundProps>>>;
337
338    fn sub_components(&self) -> &Self::SubComponents {
339        &self.sub_components
340    }
341
342    fn sub_components_mut(&mut self) -> &mut Self::SubComponents {
343        &mut self.sub_components
344    }
345
346    fn render_compound(&self) -> Result<Vec<Node>, ComponentError> {
347        self.render()
348    }
349}
350
351/// Composition utilities and builder patterns
352///
353/// Builder for creating complex component compositions
354pub struct CompositionBuilder {
355    components: Vec<Box<dyn Component<Props = FlexibleCompoundProps>>>,
356    context: Context,
357}
358
359impl CompositionBuilder {
360    pub fn new(context: Context) -> Self {
361        Self {
362            components: Vec::new(),
363            context,
364        }
365    }
366
367    pub fn add_component(
368        mut self,
369        component: Box<dyn Component<Props = FlexibleCompoundProps>>,
370    ) -> Self {
371        self.components.push(component);
372        self
373    }
374
375    pub fn build(self) -> FlexibleCompoundComponent {
376        let mut compound = FlexibleCompoundComponent::new(self.context);
377        for component in self.components {
378            compound.add_sub_component(component);
379        }
380        compound
381    }
382}
383
384/// Macros for easier composition
385///
386/// Create a slot with nodes
387#[macro_export]
388macro_rules! slot {
389    ($name:expr) => {
390        Slot::new($name)
391    };
392    ($name:expr, required) => {
393        Slot::new($name).required()
394    };
395    ($name:expr, $($node:expr),*) => {
396        Slot::new($name).with_nodes(vec![$($node),*])
397    };
398}
399
400/// Create slotted props easily
401#[macro_export]
402macro_rules! slotted_props {
403    ($($slot:expr),*) => {
404        {
405            let mut props = SlottedProps::new();
406            $(
407                props = props.with_slot($slot);
408            )*
409            props
410        }
411    };
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use crate::component::ComponentBase;
418
419    struct TestComponent {
420        base: ComponentBase,
421    }
422
423    #[derive(Clone)]
424    struct TestProps;
425    // Using the blanket implementation of Props instead of implementing it manually
426
427    impl Component for TestComponent {
428        type Props = TestProps;
429
430        fn component_id(&self) -> ComponentId {
431            self.base.id()
432        }
433
434        fn create(_props: Self::Props, context: Context) -> Self {
435            Self {
436                base: ComponentBase::new(context),
437            }
438        }
439
440        fn update(&mut self, _props: Self::Props) -> Result<(), ComponentError> {
441            Ok(())
442        }
443
444        fn render(&self) -> Result<Vec<Node>, ComponentError> {
445            Ok(vec![])
446        }
447
448        fn as_any(&self) -> &dyn std::any::Any {
449            self
450        }
451
452        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
453            self
454        }
455    }
456
457    #[test]
458    fn test_slot_creation() {
459        let slot = slot!("header");
460        assert_eq!(slot.name, "header");
461        assert!(!slot.required);
462
463        let required_slot = slot!("content", required);
464        assert!(required_slot.required);
465    }
466
467    #[test]
468    fn test_slotted_props() {
469        let props = slotted_props!(slot!("header"), slot!("content", required));
470
471        assert!(props.get_slot("header").is_some());
472        assert!(props.get_slot("content").is_some());
473        assert!(props.get_slot("footer").is_none());
474    }
475
476    #[test]
477    fn test_slotted_component() {
478        let context = Context::new();
479        let slots = vec!["header".to_string(), "content".to_string()];
480        let component = SlottedComponent::new(slots, context);
481
482        assert_eq!(component.supported_slots(), vec!["header", "content"]);
483    }
484
485    #[test]
486    fn test_composition_builder() {
487        let context = Context::new();
488        let builder = CompositionBuilder::new(context);
489        let compound = builder.build();
490
491        assert_eq!(compound.sub_components().len(), 0);
492    }
493}