Skip to main content

rust_elm/compose/
component.rs

1use crate::cmd::Cmd;
2
3/// Scoped child slot — lifts child update into parent state/action space.
4pub struct Slot<ParentState, ParentAction, ChildState, ChildAction> {
5    pub get: fn(&ParentState) -> Option<&ChildState>,
6    pub get_mut: fn(&mut ParentState) -> Option<&mut ChildState>,
7    pub embed: fn(ChildAction) -> ParentAction,
8    pub child_update: fn(&mut ChildState, ChildAction) -> Cmd<ChildAction>,
9}
10
11impl<PS, PA: Send + 'static, CS, CA: Send + 'static> Slot<PS, PA, CS, CA> {
12    pub fn new(
13        get: fn(&PS) -> Option<&CS>,
14        get_mut: fn(&mut PS) -> Option<&mut CS>,
15        embed: fn(CA) -> PA,
16        child_update: fn(&mut CS, CA) -> Cmd<CA>,
17    ) -> Self {
18        Self {
19            get,
20            get_mut,
21            embed,
22            child_update,
23        }
24    }
25
26    pub fn lift(&self, parent: &mut PS, action: PA, extract: fn(PA) -> Option<CA>) -> Cmd<PA> {
27        let Some(child_action) = extract(action) else {
28            return Cmd::none();
29        };
30        let Some(child) = (self.get_mut)(parent) else {
31            return Cmd::none();
32        };
33        (self.child_update)(child, child_action).map(self.embed)
34    }
35}
36
37/// Lift a child `Cmd` into the parent action space.
38pub fn lift<PA: Send + 'static, CA: Send + 'static>(
39    cmd: Cmd<CA>,
40    embed: fn(CA) -> PA,
41) -> Cmd<PA> {
42    cmd.map(embed)
43}
44
45#[cfg(test)]
46#[allow(dead_code, clippy::bool_assert_comparison)]
47mod tests {
48    use super::*;
49
50    #[derive(Default)]
51    struct Parent {
52        child: Option<Child>,
53    }
54
55    #[derive(Default)]
56    struct Child {
57        n: i32,
58    }
59
60    #[derive(Clone, Copy)]
61    enum PMsg {
62        Child(CMsg),
63    }
64
65    #[derive(Clone, Copy)]
66    enum CMsg {
67        Inc,
68    }
69
70    fn child_update(c: &mut Child, msg: CMsg) -> Cmd<CMsg> {
71        match msg {
72            CMsg::Inc => c.n += 1,
73        }
74        Cmd::none()
75    }
76
77    #[test]
78    fn slot_lift_updates_nested_state() {
79        let slot = Slot::new(
80            |p: &Parent| p.child.as_ref(),
81            |p: &mut Parent| p.child.as_mut(),
82            PMsg::Child,
83            child_update,
84        );
85        let mut parent = Parent {
86            child: Some(Child::default()),
87        };
88        let extract = |m: PMsg| match m {
89            PMsg::Child(c) => Some(c),
90        };
91        let _ = slot.lift(&mut parent, PMsg::Child(CMsg::Inc), extract);
92        assert_eq!(parent.child.unwrap().n, 1);
93    }
94}