Skip to main content

fusor/dom/
keyed.rs

1use super::{
2    ElementTarget, JsValue, Scope, document, reconcile, remove_tree, strings, with_native_root,
3};
4use crate::{Signal, signal, untrack};
5use std::collections::BTreeMap;
6use wasm_bindgen::JsCast;
7use web_sys::{Document, Element, HtmlElement, HtmlInputElement};
8
9type EncodeKey<K> = dyn Fn(&K) -> Result<String, JsValue>;
10
11/// How a reconcile finds the previous rows whose keys are gone.
12enum Removal<'a, K> {
13    /// Merge every key against the ordered map.
14    Sorted(reconcile::SortedKeys<'a, K>),
15    /// The previous indices of the few removed keys.
16    Direct(Vec<usize>),
17}
18
19struct Row<T> {
20    // Held until the row drops, like the row's scope, even when the scope does
21    // not retain its item.
22    _state: Signal<T>,
23    scope: Scope,
24}
25
26impl Scope {
27    /// Reconcile a list by stable keys, retaining nodes, focus, and row scopes.
28    /// `render` runs once per inserted key. The row signal delivers later values.
29    /// This binding owns the container's children. Duplicate keys are errors.
30    pub fn keyed<T, K>(
31        &mut self,
32        target: impl ElementTarget,
33        items: impl Fn() -> Vec<T> + 'static,
34        key: impl Fn(&T) -> K + 'static,
35        render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
36    ) -> Result<(), JsValue>
37    where
38        T: Clone + PartialEq + 'static,
39        K: Ord + Clone + 'static,
40    {
41        self.keyed_inner(target, items, key, render, None)
42    }
43
44    /// Generated shared templates carry serialized row identities in native HTML.
45    #[doc(hidden)]
46    pub fn keyed_hydrated<T, K>(
47        &mut self,
48        target: impl ElementTarget,
49        items: impl Fn() -> Vec<T> + 'static,
50        key: impl Fn(&T) -> K + 'static,
51        render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
52        encode: impl Fn(&K) -> Result<String, JsValue> + 'static,
53    ) -> Result<(), JsValue>
54    where
55        T: Clone + PartialEq + 'static,
56        K: Ord + Clone + 'static,
57    {
58        self.keyed_inner(target, items, key, render, Some(Box::new(encode)))
59    }
60
61    fn keyed_inner<T, K>(
62        &mut self,
63        target: impl ElementTarget,
64        items: impl Fn() -> Vec<T> + 'static,
65        key: impl Fn(&T) -> K + 'static,
66        render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
67        encode: Option<Box<EncodeKey<K>>>,
68    ) -> Result<(), JsValue>
69    where
70        T: Clone + PartialEq + 'static,
71        K: Ord + Clone + 'static,
72    {
73        let hydrating = self.is_hydrating();
74        let container = target.resolve(self)?;
75        // `rows` owns the rows and orders their lifecycle by key. `order` and
76        // `states` hold the same keys in DOM order with each row's signal, so
77        // rows that keep their index are updated without a map lookup.
78        let mut rows: BTreeMap<K, Row<T>> = BTreeMap::new();
79        let mut order: Vec<K> = Vec::new();
80        let mut states: Vec<Signal<T>> = Vec::new();
81        let mut initialized = false;
82        // Every row in `rows` was committed by a reconcile that completed.
83        let mut settled = false;
84        let queue = self.mount_queue.clone();
85        self.bind(move || {
86            let items = items();
87            untrack(|| {
88                let keys: Vec<K> = items.iter().map(&key).collect();
89                let duplicate = || JsValue::from_str("fusor: duplicate key in list");
90                // A small edit resolves its few changed keys directly. Any other
91                // change validates and merges through every key in order.
92                let (retained, mut removal) =
93                    match reconcile::small_edit(&order, &keys, |key| rows.contains_key(key)) {
94                        Some(plan) => {
95                            let (positions, removed) = plan.map_err(|()| duplicate())?;
96                            (positions, Removal::Direct(removed))
97                        }
98                        None => {
99                            let unique = reconcile::SortedKeys::new(&keys).ok_or_else(duplicate)?;
100                            let positions = reconcile::previous_positions(&order, &unique);
101                            (positions, Removal::Sorted(unique))
102                        }
103                    };
104                let native_rows = if hydrating && !initialized {
105                    let encode = encode.as_ref().ok_or_else(|| {
106                        JsValue::from_str("hydrated lists require generated key metadata")
107                    })?;
108                    server_rows(&container, &keys, encode)?
109                } else {
110                    Vec::new()
111                };
112                let document = document()?;
113                let focused = focused(&document, &container);
114                // Stage new scopes before touching the visible list. A failing
115                // render drops all staged listeners and leaves old rows intact.
116                let mut staged = BTreeMap::new();
117                let mut positions = retained.clone();
118                let mut fresh = Vec::new();
119                for (index, (key, item)) in keys.iter().zip(&items).enumerate() {
120                    if retained[index] != reconcile::NEW {
121                        continue;
122                    }
123                    let state = signal(item.clone());
124                    let native = native_rows.get(index);
125                    let scope = with_native_root(native, || render(state.clone()))?;
126                    // Server rows already occupy their final positions. Newly
127                    // rendered roots are detached and must be inserted.
128                    if native.is_some() {
129                        positions[index] = index;
130                    }
131                    fresh.push(state.clone());
132                    staged.insert(
133                        key.clone(),
134                        Row {
135                            _state: state,
136                            scope,
137                        },
138                    );
139                }
140                for row in staged.values() {
141                    row.scope.finish_prepare()?;
142                }
143                if !initialized {
144                    if !hydrating {
145                        #[cfg(feature = "islands")]
146                        super::delivery::dispose_tree(&container);
147                        container.set_text_content(None);
148                    }
149                    initialized = true;
150                }
151                // Release the previous order before removed rows drop, as
152                // their own rows hold the remaining references.
153                let mut previous: Vec<_> =
154                    std::mem::take(&mut states).into_iter().map(Some).collect();
155                let mut fresh = fresh.into_iter();
156                let next: Vec<Signal<T>> = retained
157                    .iter()
158                    .map(|&position| match position {
159                        reconcile::NEW => fresh.next().expect("staged row"),
160                        position => previous[position].take().expect("retained row"),
161                    })
162                    .collect();
163                drop(previous);
164                match &mut removal {
165                    Removal::Sorted(unique) => rows.retain(|key, row| {
166                        let keep = unique.contains_next(key);
167                        if !keep {
168                            remove_tree(&row.scope.root);
169                        }
170                        keep
171                    }),
172                    Removal::Direct(removed) => {
173                        // In ascending key order, as `retain` visits the map.
174                        reconcile::sort_few(removed, &order);
175                        for &index in removed.iter() {
176                            if let Some(entry) = rows.remove_entry(&order[index]) {
177                                // Like retain, detach before dropping the owned
178                                // key, then release the row and its cleanup.
179                                remove_tree(&entry.1.scope.root);
180                                drop(entry);
181                            }
182                        }
183                    }
184                }
185                // Committing a settled row again only finishes setup that a
186                // descendant queued, so without pending setup only new rows
187                // need committing.
188                let fresh_keys: Option<Vec<K>> = settled.then(|| staged.keys().cloned().collect());
189                settled = false;
190                if rows.is_empty() {
191                    rows = staged;
192                } else if staged.len() <= rows.len() / (rows.len().ilog2() as usize + 1) {
193                    // Avoid rebuilding the existing tree for sparse additions;
194                    // keep the linear sorted merge when insertions are dense.
195                    rows.extend(staged);
196                } else {
197                    rows.append(&mut staged);
198                }
199                let stationary = reconcile::stationary(&positions);
200                for (state, item) in next.iter().zip(items) {
201                    state.set(item);
202                }
203                (order, states) = (keys, next);
204                for (index, keep) in stationary.iter().enumerate().rev() {
205                    if !keep {
206                        let anchor = order
207                            .get(index + 1)
208                            .map(|key| rows[key].scope.root.as_ref());
209                        strings::insert_before(
210                            &container,
211                            &rows[&order[index]].scope.root,
212                            anchor,
213                        )?;
214                    }
215                }
216                let idle = queue.as_ref().is_none_or(|queue| queue.is_idle());
217                match fresh_keys.filter(|_| idle) {
218                    Some(keys) => {
219                        for key in &keys {
220                            rows[key].scope.commit();
221                        }
222                    }
223                    None => {
224                        for row in rows.values() {
225                            row.scope.commit();
226                        }
227                    }
228                }
229                settled = true;
230                restore_focus(&document, &container, focused)
231            })
232        })
233    }
234}
235
236/// Adopt the server-rendered rows, which must match `keys` in order.
237fn server_rows<K>(
238    container: &Element,
239    keys: &[K],
240    encode: &EncodeKey<K>,
241) -> Result<Vec<Element>, JsValue> {
242    // Compare every row natively in one call. Keys encode in order: a failed
243    // encoding is reported after the rows before it and its own row are
244    // checked, as when comparing one row at a time.
245    let mut encoded = String::new();
246    let mut failure = None;
247    let mut count = 0;
248    for key in keys {
249        match encode(key) {
250            // A separator inside an encoding needs the one-at-a-time path.
251            Ok(value) if value.contains('\n') => {
252                return server_rows_one_by_one(container, keys, encode);
253            }
254            Ok(value) => {
255                if count > 0 {
256                    encoded.push('\n');
257                }
258                encoded.push_str(&value);
259                count += 1;
260            }
261            Err(error) => {
262                failure = Some(error);
263                break;
264            }
265        }
266    }
267    let rows = strings::server_rows(container, &encoded, count as u32, failure.is_none())?;
268    if let Some(error) = failure {
269        return Err(error);
270    }
271    Ok((0..count as u32)
272        .map(|index| rows.get(index).unchecked_into())
273        .collect())
274}
275
276fn server_rows_one_by_one<K>(
277    container: &Element,
278    keys: &[K],
279    encode: &EncodeKey<K>,
280) -> Result<Vec<Element>, JsValue> {
281    let mut node = container.first_element_child();
282    let mut rows = Vec::with_capacity(keys.len());
283    for key in keys {
284        let row = node
285            .take()
286            .ok_or_else(|| JsValue::from_str("missing native row"))?;
287        if strings::attribute(&row, strings::Name::Key).as_deref() != Some(encode(key)?.as_str()) {
288            return Err(JsValue::from_str("native row key mismatch"));
289        }
290        node = row.next_element_sibling();
291        rows.push(row);
292    }
293    if node.is_some() {
294        return Err(JsValue::from_str("unexpected native row"));
295    }
296    Ok(rows)
297}
298
299type Selection = (u32, u32, String);
300
301/// The focused descendant of `container` and, for an input, its selection.
302fn focused(document: &Document, container: &Element) -> Option<(HtmlElement, Option<Selection>)> {
303    let focused = document
304        .active_element()
305        .filter(|node| container.contains(Some(node)))?
306        .dyn_into::<HtmlElement>()
307        .ok()?;
308    let selection = focused.dyn_ref::<HtmlInputElement>().and_then(|input| {
309        Some((
310            input.selection_start().ok()??,
311            input.selection_end().ok()??,
312            input.selection_direction().ok()??,
313        ))
314    });
315    Some((focused, selection))
316}
317
318// insertBefore can blur a node even when moving it within the same list.
319// Restore focus only if that original node survives.
320fn restore_focus(
321    document: &Document,
322    container: &Element,
323    focused: Option<(HtmlElement, Option<Selection>)>,
324) -> Result<(), JsValue> {
325    let Some((focused, selection)) = focused.filter(|(node, _)| container.contains(Some(node)))
326    else {
327        return Ok(());
328    };
329    if document
330        .active_element()
331        .is_some_and(|node| node.is_same_node(Some(&focused)))
332    {
333        return Ok(());
334    }
335    focused.focus()?;
336    if let (Some(input), Some((start, end, direction))) =
337        (focused.dyn_ref::<HtmlInputElement>(), selection)
338    {
339        input.set_selection_range_with_direction(start, end, &direction)?;
340    }
341    Ok(())
342}