maple_core/
flow.rs

1//! Iteration utility components for [`template`](crate::template).
2//!
3//! Iteration can be either _"keyed"_ or _"non keyed"_.
4//! Use the [`Keyed`] and [`Indexed`] utility components respectively.
5
6use std::cell::RefCell;
7use std::collections::{HashMap, HashSet};
8use std::hash::Hash;
9use std::mem;
10use std::rc::Rc;
11
12use wasm_bindgen::*;
13use web_sys::{Element, HtmlElement};
14
15use crate::internal::append;
16use crate::prelude::*;
17use crate::reactive::Owner;
18
19/// Props for [`Keyed`].
20pub struct KeyedProps<T: 'static, F, K, Key>
21where
22    F: Fn(T) -> TemplateResult,
23    K: Fn(&T) -> Key,
24    Key: Hash + Eq,
25{
26    pub iterable: StateHandle<Vec<T>>,
27    pub template: F,
28    pub key: K,
29}
30
31/// Keyed iteration. Use this instead of directly rendering an array of [`TemplateResult`]s.
32/// Using this will minimize re-renders instead of re-rendering every single node on every state change.
33///
34/// For non keyed iteration, see [`Indexed`].
35///
36/// # Example
37/// ```no_run
38/// use maple_core::prelude::*;
39///
40/// let count = Signal::new(vec![1, 2]);
41///
42/// let node = template! {
43///     Keyed(KeyedProps {
44///         iterable: count.handle(),
45///         template: |item| template! {
46///             li { (item) }
47///         },
48///         key: |item| *item,
49///     })
50/// };
51/// ```
52pub fn Keyed<T, F: 'static, K: 'static, Key: 'static>(
53    props: KeyedProps<T, F, K, Key>,
54) -> TemplateResult
55where
56    F: Fn(T) -> TemplateResult,
57    K: Fn(&T) -> Key,
58    Key: Clone + Hash + Eq,
59    T: Clone + PartialEq,
60{
61    let KeyedProps {
62        iterable,
63        template,
64        key: key_fn,
65    } = props;
66    let iterable = Rc::new(iterable);
67    let key_fn = Rc::new(key_fn);
68
69    type TemplateValue<T> = (Owner, T, TemplateResult, usize /* index */);
70
71    // A tuple with a value of type `T` and the `TemplateResult` produces by calling `props.template` with the first value.
72    let templates: Rc<RefCell<HashMap<Key, TemplateValue<T>>>> =
73        Rc::new(RefCell::new(HashMap::new()));
74
75    let fragment = web_sys::window()
76        .unwrap()
77        .document()
78        .unwrap()
79        .create_document_fragment();
80
81    let marker = web_sys::window()
82        .unwrap()
83        .document()
84        .unwrap()
85        .create_comment("");
86
87    append(&fragment, &marker);
88
89    create_effect({
90        let iterable = Rc::clone(&iterable);
91        let key_fn = Rc::clone(&key_fn);
92        let templates = Rc::clone(&templates);
93        let marker = marker.clone();
94        move || {
95            // Fast path for empty array. Remove all nodes from DOM in templates.
96            if iterable.get().is_empty() {
97                for (_, (owner, _value, template, _i)) in templates.borrow_mut().drain() {
98                    drop(owner); // destroy owner
99                    template.node.unchecked_into::<HtmlElement>().remove();
100                }
101                return;
102            }
103
104            // Remove old nodes not in iterable.
105            {
106                let mut templates = templates.borrow_mut();
107                let new_keys: HashSet<Key> =
108                    iterable.get().iter().map(|item| key_fn(item)).collect();
109
110                let excess_nodes = templates
111                    .iter()
112                    .filter(|item| new_keys.get(item.0).is_none())
113                    .map(|x| (x.0.clone(), (x.1 .2.clone(), x.1 .3)))
114                    .collect::<Vec<_>>();
115
116                for node in &excess_nodes {
117                    let removed_index = node.1 .1;
118                    templates.remove(&node.0);
119
120                    // Offset indexes of other templates by 1.
121                    for (_, _, _, i) in templates.values_mut() {
122                        if *i > removed_index {
123                            *i -= 1;
124                        }
125                    }
126                }
127
128                for node in excess_nodes {
129                    node.1 .0.node.unchecked_into::<Element>().remove();
130                }
131            }
132
133            struct PreviousData<T> {
134                value: T,
135                index: usize,
136            }
137
138            let previous_values: HashMap<_, PreviousData<T>> = {
139                let templates = templates.borrow();
140                templates
141                    .iter()
142                    .map(|x| {
143                        (
144                            (*x.0).clone(),
145                            PreviousData {
146                                value: x.1 .1.clone(),
147                                index: x.1 .3,
148                            },
149                        )
150                    })
151                    .collect()
152            };
153
154            // Find values that changed by comparing to previous_values.
155            for (i, item) in iterable.get().iter().enumerate() {
156                let key = key_fn(item);
157
158                let previous_value = previous_values.get(&key);
159
160                if previous_value.is_none() {
161                    // Create new DOM node.
162
163                    let mut new_template = None;
164                    let owner = create_root(|| new_template = Some(template(item.clone())));
165
166                    templates.borrow_mut().insert(
167                        key.clone(),
168                        (owner, item.clone(), new_template.clone().unwrap(), i),
169                    );
170
171                    if let Some(next_item) = iterable.get().get(i + 1) {
172                        let templates = templates.borrow();
173                        if let Some(next_node) = templates.get(&key_fn(next_item)) {
174                            next_node
175                                .2
176                                .node
177                                .unchecked_ref::<HtmlElement>()
178                                .before_with_node_1(&new_template.unwrap().node)
179                                .unwrap();
180                        } else {
181                            marker
182                                .before_with_node_1(&new_template.unwrap().node)
183                                .unwrap();
184                        }
185                    } else {
186                        marker
187                            .before_with_node_1(&new_template.unwrap().node)
188                            .unwrap();
189                    }
190                } else if match previous_value {
191                    Some(prev) => prev.index,
192                    _ => unreachable!(),
193                } != i
194                {
195                    // Location changed, move from old location to new location
196                    // Node was moved in the DOM. Move node to new index.
197
198                    let node = templates.borrow().get(&key).unwrap().2.node.clone();
199
200                    if let Some(next_item) = iterable.get().get(i + 1) {
201                        let templates = templates.borrow();
202                        let next_node = templates.get(&key_fn(next_item)).unwrap();
203                        next_node
204                            .2
205                            .node
206                            .unchecked_ref::<HtmlElement>()
207                            .before_with_node_1(&node)
208                            .unwrap(); // Move to before next node
209                    } else {
210                        marker.before_with_node_1(&node).unwrap(); // Move to end.
211                    }
212
213                    templates.borrow_mut().get_mut(&key).unwrap().3 = i;
214                } else if match previous_value {
215                    Some(prev) => &prev.value,
216                    _ => unreachable!(),
217                } != item
218                {
219                    // Value changed. Re-render node (with same previous key and index).
220
221                    // Destroy old template owner.
222                    let mut templates = templates.borrow_mut();
223                    let (old_owner, _, _, _) = templates
224                        .get_mut(&key)
225                        .expect("previous value is different but must be valid");
226                    let old_owner = mem::replace(old_owner, Owner::new() /* placeholder */);
227                    drop(old_owner);
228
229                    let mut new_template = None;
230                    let owner = create_root(|| new_template = Some(template(item.clone())));
231
232                    let (_, _, old_node, _) = mem::replace(
233                        templates.get_mut(&key).unwrap(),
234                        (owner, item.clone(), new_template.clone().unwrap(), i),
235                    );
236
237                    let parent = old_node.node.parent_node().unwrap();
238                    parent
239                        .replace_child(&new_template.unwrap().node, &old_node.node)
240                        .unwrap();
241                }
242            }
243        }
244    });
245
246    for item in iterable.get().iter() {
247        let key = key_fn(item);
248        let template = templates.borrow().get(&key).unwrap().2.clone();
249
250        marker.before_with_node_1(&template.node).unwrap();
251    }
252
253    TemplateResult::new(fragment.into())
254}
255
256/// Props for [`Indexed`].
257pub struct IndexedProps<T: 'static, F>
258where
259    F: Fn(T) -> TemplateResult,
260{
261    pub iterable: StateHandle<Vec<T>>,
262    pub template: F,
263}
264
265/// Non keyed iteration (or keyed by index). Use this instead of directly rendering an array of [`TemplateResult`]s.
266/// Using this will minimize re-renders instead of re-rendering every single node on every state change.
267///
268/// For keyed iteration, see [`Keyed`].
269///
270/// # Example
271/// ```no_run
272/// use maple_core::prelude::*;
273///
274/// let count = Signal::new(vec![1, 2]);
275///
276/// let node = template! {
277///     Indexed(IndexedProps {
278///         iterable: count.handle(),
279///         template: |item| template! {
280///             li { (item) }
281///         },
282///     })
283/// };
284/// ```
285pub fn Indexed<T, F: 'static>(props: IndexedProps<T, F>) -> TemplateResult
286where
287    T: Clone + PartialEq,
288    F: Fn(T) -> TemplateResult,
289{
290    let templates: Rc<RefCell<Vec<(Owner, TemplateResult)>>> = Rc::new(RefCell::new(Vec::new()));
291
292    // Previous values for diffing purposes.
293    let previous_values = RefCell::new(Vec::new());
294
295    let fragment = web_sys::window()
296        .unwrap()
297        .document()
298        .unwrap()
299        .create_document_fragment();
300
301    let marker = web_sys::window()
302        .unwrap()
303        .document()
304        .unwrap()
305        .create_comment("");
306
307    append(&fragment, &marker);
308
309    create_effect({
310        let templates = Rc::clone(&templates);
311        let marker = marker.clone();
312        move || {
313            // Fast path for empty array. Remove all nodes from DOM in templates.
314            if props.iterable.get().is_empty() {
315                for (owner, template) in templates.borrow_mut().drain(..) {
316                    drop(owner); // destroy owner
317                    template.node.unchecked_into::<HtmlElement>().remove();
318                }
319                return;
320            }
321
322            // Find values that changed by comparing to previous_values.
323            for (i, item) in props.iterable.get().iter().enumerate() {
324                let previous_values = previous_values.borrow();
325                let previous_value = previous_values.get(i);
326
327                if previous_value.is_none() || previous_value.unwrap() != item {
328                    // Value changed, re-render item.
329
330                    templates.borrow_mut().get_mut(i).and_then(|(owner, _)| {
331                        // destroy old owner
332                        let old_owner = mem::replace(owner, Owner::new() /* placeholder */);
333                        drop(old_owner);
334                        None::<()>
335                    });
336
337                    let mut new_template = None;
338                    let owner = create_root(|| new_template = Some((props.template)(item.clone())));
339
340                    if templates.borrow().get(i).is_some() {
341                        let old_node = mem::replace(
342                            &mut templates.borrow_mut()[i],
343                            (owner, new_template.as_ref().unwrap().clone()),
344                        );
345
346                        let parent = old_node.1.node.parent_node().unwrap();
347                        parent
348                            .replace_child(&new_template.unwrap().node, &old_node.1.node)
349                            .unwrap();
350                    } else {
351                        debug_assert!(templates.borrow().len() == i, "pushing new value scenario");
352
353                        templates
354                            .borrow_mut()
355                            .push((owner, new_template.as_ref().unwrap().clone()));
356
357                        marker
358                            .before_with_node_1(&new_template.unwrap().node)
359                            .unwrap();
360                    }
361                }
362            }
363
364            if templates.borrow().len() > props.iterable.get().len() {
365                let mut templates = templates.borrow_mut();
366                let excess_nodes = templates.drain(props.iterable.get().len()..);
367
368                for node in excess_nodes {
369                    node.1.node.unchecked_into::<Element>().remove();
370                }
371            }
372
373            *previous_values.borrow_mut() = (*props.iterable.get()).clone();
374        }
375    });
376
377    for template in templates.borrow().iter() {
378        marker.before_with_node_1(&template.1.node).unwrap();
379    }
380
381    TemplateResult::new(fragment.into())
382}