euv_ui/component/tree_select/view/fn.rs
1use super::*;
2
3/// A select whose option panel is a tree.
4///
5/// Renders a `c_euv_tree_select` wrapper holding the optional group label,
6/// a `c_euv_tree_select_trigger` button that shows the selected node's
7/// label in `c_euv_tree_select_value` — or the placeholder in
8/// `c_euv_tree_select_placeholder` while no node matches — next to a
9/// trailing `c_euv_tree_select_caret` marker, and the
10/// `c_euv_tree_select_panel` the tree lives in.
11///
12/// The panel is always mounted: opening and closing it swaps the
13/// `c_euv_tree_select_panel_open` and `c_euv_tree_select_panel_closed`
14/// classes rather than removing the tree from the tree, so the nodes
15/// survive every re-render.
16///
17/// The tree itself is the [`euv_tree`] component, so the panel reuses the
18/// tree classes — `c_euv_tree`, `c_euv_tree_node`, `c_euv_tree_node_row`,
19/// `c_euv_tree_node_row_active`, the toggle markers and the children
20/// containers — instead of carrying a second set of them. The children of
21/// a node stay mounted; expanding a branch is a class swap.
22///
23/// Choosing a node writes its value into the `value` signal, reports it to
24/// the `on_select` callback and closes the panel. The open state is owned
25/// by the component itself through a hook, so the caller never has to
26/// wire it up.
27///
28/// # Arguments
29///
30/// - `VirtualNode<EuvTreeSelectProps>` - The props node holding the tree, the value signal and the select callback.
31///
32/// # Returns
33///
34/// - `VirtualNode` - The tree select virtual DOM tree.
35#[component]
36pub fn euv_tree_select(node: VirtualNode<EuvTreeSelectProps>) -> VirtualNode {
37 let EuvTreeSelectProps {
38 nodes,
39 value,
40 placeholder,
41 label: label_text,
42 on_select,
43 }: EuvTreeSelectProps = node.try_get_props().unwrap_or_default();
44 let open: Signal<bool> = HookContext::use_hook(|| Signal::create(false));
45 let selected_value: String = value.get();
46 let selected_label: Option<&'static str> = tree_select_find_label(&nodes, &selected_value);
47 let trigger_text: &'static str = selected_label.unwrap_or(placeholder);
48 let has_selection: bool = selected_label.is_some();
49 let pick: Rc<dyn Fn(&'static str)> = tree_select_pick_handler(value, on_select, open);
50 html! {
51 div {
52 class: c_euv_tree_select()
53 if !label_text.is_empty() {
54 div {
55 class: c_form_label()
56 label_text
57 }
58 }
59 button {
60 type: "button"
61 class: c_euv_tree_select_trigger()
62 onclick: tree_select_toggle_handler(open)
63 span {
64 class: if has_selection {
65 c_euv_tree_select_value()
66 } else {
67 c_euv_tree_select_placeholder()
68 }
69 trigger_text
70 }
71 span {
72 class: c_euv_tree_select_caret()
73 "▾"
74 }
75 }
76 div {
77 class: c_euv_tree_select_panel()
78 class: if { open } {
79 c_euv_tree_select_panel_open()
80 } else {
81 c_euv_tree_select_panel_closed()
82 }
83 euv_tree {
84 nodes: nodes
85 selected: value
86 on_select: Some(pick)
87 default_expanded: true
88 }
89 }
90 }
91 }
92}
93
94/// Finds the label of the node carrying the given value.
95///
96/// The search walks the tree depth first, so a nested node is found by
97/// its value just like a root one.
98///
99/// # Arguments
100///
101/// - `&[EuvTreeNode]` - The nodes to walk.
102/// - `&str` - The value of the node to find.
103///
104/// # Returns
105///
106/// - `Option<&'static str>` - The label of the matching node, when there is one.
107pub fn tree_select_find_label(nodes: &[EuvTreeNode], value: &str) -> Option<&'static str> {
108 for node in nodes.iter() {
109 if node.value == value {
110 return Some(node.label);
111 }
112 if let Some(found) = tree_select_find_label(&node.children, value) {
113 return Some(found);
114 }
115 }
116 None
117}
118
119/// Builds the click handler that opens or closes the panel.
120///
121/// # Arguments
122///
123/// - `Signal<bool>` - The panel open state owned by the component.
124///
125/// # Returns
126///
127/// - `Option<Rc<dyn Fn(E)>>` - The toggle click handler.
128pub fn tree_select_toggle_handler<E: 'static>(open: Signal<bool>) -> Option<Rc<dyn Fn(E)>> {
129 Some(Rc::new(move |_: E| {
130 let is_open: bool = open.get();
131 open.set(!is_open);
132 }))
133}
134
135/// Builds the handler that picks a node of the tree.
136///
137/// # Arguments
138///
139/// - `Signal<String>` - The value signal owned by the caller.
140/// - `Option<Rc<dyn Fn(&'static str)>>` - The select callback.
141/// - `Signal<bool>` - The panel open state owned by the component.
142///
143/// # Returns
144///
145/// - `Rc<dyn Fn(&'static str)>` - The pick handler.
146fn tree_select_pick_handler(
147 value: Signal<String>,
148 on_select: Option<Rc<dyn Fn(&'static str)>>,
149 open: Signal<bool>,
150) -> Rc<dyn Fn(&'static str)> {
151 Rc::new(move |node_value: &'static str| {
152 tree_select_pick_node(value, on_select.clone(), open, node_value);
153 })
154}
155
156/// Picks a node: writes its value, reports it and closes the panel.
157///
158/// The value lands in the caller's signal first, so the callback and the
159/// closing of the panel both see the new selection.
160///
161/// # Arguments
162///
163/// - `Signal<String>` - The value signal owned by the caller.
164/// - `Option<Rc<dyn Fn(&'static str)>>` - The select callback.
165/// - `Signal<bool>` - The panel open state owned by the component.
166/// - `&'static str` - The value of the picked node.
167pub fn tree_select_pick_node(
168 value: Signal<String>,
169 on_select: Option<Rc<dyn Fn(&'static str)>>,
170 open: Signal<bool>,
171 node_value: &'static str,
172) {
173 value.set(String::from(node_value));
174 if let Some(callback) = &on_select {
175 callback(node_value);
176 }
177 open.set(false);
178}