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/// Key extraction and serialization used to match server-rendered rows.
12#[doc(hidden)]
13pub struct HydratedKeys<F, E> {
14    pub key: F,
15    pub encode: E,
16}
17
18/// How a reconcile finds the previous rows whose keys are gone.
19enum Removal<'a, K> {
20    /// Merge every key against the ordered map.
21    Sorted(reconcile::SortedKeys<'a, K>),
22    /// The previous indices of the few removed keys.
23    Direct(Vec<usize>),
24}
25
26struct Row<T> {
27    // Held until the row drops, like the row's scope, even when the scope does
28    // not retain its item.
29    _state: Signal<T>,
30    scope: Scope,
31}
32
33impl Scope {
34    /// Reconcile a list by stable keys, retaining nodes, focus, and row scopes.
35    /// `render` runs once per inserted key. The row signal delivers later values.
36    /// This binding owns the container's children. Duplicate keys are errors.
37    pub fn keyed<T, K>(
38        &mut self,
39        target: impl ElementTarget,
40        items: impl Fn() -> Vec<T> + 'static,
41        key: impl Fn(&T) -> K + 'static,
42        render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
43    ) -> Result<(), JsValue>
44    where
45        T: Clone + PartialEq + 'static,
46        K: Ord + Clone + 'static,
47    {
48        self.keyed_inner(
49            target,
50            items,
51            key,
52            RowFactory {
53                render,
54                encode: None,
55            },
56        )
57    }
58
59    /// Generated shared templates carry serialized row identities in native HTML.
60    #[doc(hidden)]
61    pub fn keyed_hydrated<T, K>(
62        &mut self,
63        target: impl ElementTarget,
64        items: impl Fn() -> Vec<T> + 'static,
65        keys: HydratedKeys<
66            impl Fn(&T) -> K + 'static,
67            impl Fn(&K) -> Result<String, JsValue> + 'static,
68        >,
69        render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
70    ) -> Result<(), JsValue>
71    where
72        T: Clone + PartialEq + 'static,
73        K: Ord + Clone + 'static,
74    {
75        self.keyed_inner(
76            target,
77            items,
78            keys.key,
79            RowFactory {
80                render,
81                encode: Some(Box::new(keys.encode)),
82            },
83        )
84    }
85
86    fn keyed_inner<T, K, R>(
87        &mut self,
88        target: impl ElementTarget,
89        items: impl Fn() -> Vec<T> + 'static,
90        key: impl Fn(&T) -> K + 'static,
91        factory: RowFactory<K, R>,
92    ) -> Result<(), JsValue>
93    where
94        T: Clone + PartialEq + 'static,
95        K: Ord + Clone + 'static,
96        R: Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
97    {
98        let hydrating = self.is_hydrating();
99        let mut list = KeyedRows {
100            container: target.resolve(self)?,
101            rows: BTreeMap::new(),
102            order: Vec::new(),
103            states: Vec::new(),
104            initialized: false,
105            settled: false,
106            hydrating,
107            factory,
108            queue: self.mount_queue.clone(),
109        };
110        self.bind(move || {
111            let items = items();
112            untrack(|| {
113                let keys = items.iter().map(&key).collect();
114                list.update(items, keys)
115            })
116        })
117    }
118}
119
120struct RowFactory<K, R> {
121    render: R,
122    encode: Option<Box<EncodeKey<K>>>,
123}
124
125struct KeyedRows<T, K, R> {
126    container: Element,
127    // The map orders cleanup by key; order and states mirror DOM order.
128    rows: BTreeMap<K, Row<T>>,
129    order: Vec<K>,
130    states: Vec<Signal<T>>,
131    initialized: bool,
132    settled: bool,
133    hydrating: bool,
134    factory: RowFactory<K, R>,
135    queue: Option<std::rc::Rc<super::commit::CommitQueue>>,
136}
137
138struct PreparedRows<T, K> {
139    rows: BTreeMap<K, Row<T>>,
140    positions: Vec<usize>,
141    states: Vec<Signal<T>>,
142}
143
144impl<T, K, R> KeyedRows<T, K, R>
145where
146    T: Clone + PartialEq + 'static,
147    K: Ord + Clone + 'static,
148    R: Fn(Signal<T>) -> Result<Scope, JsValue>,
149{
150    fn update(&mut self, items: Vec<T>, keys: Vec<K>) -> Result<(), JsValue> {
151        let (retained, removal) = self.plan(&keys)?;
152        let native_rows = self.native_rows(&keys)?;
153        let document = document()?;
154        let focused = focused(&document, &self.container);
155        let mut staged = self.prepare_rows(&items, &keys, &retained, &native_rows)?;
156        self.initialize();
157        let next = self.reorder_states(&retained, staged.states);
158        self.remove_rows(removal);
159        // Settled rows need committing again only when descendants queued setup.
160        let fresh_keys = self.settled.then(|| staged.rows.keys().cloned().collect());
161        self.settled = false;
162        self.merge_rows(&mut staged.rows);
163        let stationary = reconcile::stationary(&staged.positions);
164        for (state, item) in next.iter().zip(items) {
165            state.set(item);
166        }
167        (self.order, self.states) = (keys, next);
168        self.position_rows(&stationary)?;
169        self.commit_rows(fresh_keys);
170        self.settled = true;
171        restore_focus(&document, &self.container, focused)
172    }
173
174    fn plan<'a>(&self, keys: &'a [K]) -> Result<(Vec<usize>, Removal<'a, K>), JsValue> {
175        let duplicate = || JsValue::from_str("fusor: duplicate key in list");
176        // Small edits resolve their changed keys directly; other edits use a sorted merge.
177        match reconcile::small_edit(&self.order, keys, |key| self.rows.contains_key(key)) {
178            Some(plan) => {
179                let reconcile::EditPlan { positions, removed } = plan.map_err(|()| duplicate())?;
180                Ok((positions, Removal::Direct(removed)))
181            }
182            None => {
183                let unique = reconcile::SortedKeys::new(keys).ok_or_else(duplicate)?;
184                let positions = reconcile::previous_positions(&self.order, &unique);
185                Ok((positions, Removal::Sorted(unique)))
186            }
187        }
188    }
189
190    fn native_rows(&self, keys: &[K]) -> Result<Vec<Element>, JsValue> {
191        if !self.hydrating || self.initialized {
192            return Ok(Vec::new());
193        }
194        let encode =
195            self.factory.encode.as_ref().ok_or_else(|| {
196                JsValue::from_str("hydrated lists require generated key metadata")
197            })?;
198        server_rows(&self.container, keys, encode)
199    }
200
201    fn prepare_rows(
202        &self,
203        items: &[T],
204        keys: &[K],
205        retained: &[usize],
206        native_rows: &[Element],
207    ) -> Result<PreparedRows<T, K>, JsValue> {
208        // Stage and validate every scope before changing the visible list.
209        let mut staged = PreparedRows {
210            rows: BTreeMap::new(),
211            positions: retained.to_vec(),
212            states: Vec::new(),
213        };
214        for (index, (key, item)) in keys.iter().zip(items).enumerate() {
215            if retained[index] != reconcile::NEW {
216                continue;
217            }
218            let state = signal(item.clone());
219            let native = native_rows.get(index);
220            let scope = with_native_root(native, || (self.factory.render)(state.clone()))?;
221            // Adopted rows already occupy their final positions.
222            if native.is_some() {
223                staged.positions[index] = index;
224            }
225            staged.states.push(state.clone());
226            staged.rows.insert(
227                key.clone(),
228                Row {
229                    _state: state,
230                    scope,
231                },
232            );
233        }
234        for row in staged.rows.values() {
235            row.scope.finish_prepare()?;
236        }
237        Ok(staged)
238    }
239
240    fn initialize(&mut self) {
241        if self.initialized {
242            return;
243        }
244        if !self.hydrating {
245            #[cfg(feature = "islands")]
246            super::delivery::dispose_tree(&self.container);
247            self.container.set_text_content(None);
248        }
249        self.initialized = true;
250    }
251
252    fn reorder_states(&mut self, retained: &[usize], fresh: Vec<Signal<T>>) -> Vec<Signal<T>> {
253        // Release the old order before removed rows drop their remaining references.
254        let mut previous: Vec<_> = std::mem::take(&mut self.states)
255            .into_iter()
256            .map(Some)
257            .collect();
258        let mut fresh = fresh.into_iter();
259        retained
260            .iter()
261            .map(|&position| match position {
262                reconcile::NEW => fresh.next().expect("staged row"),
263                position => previous[position].take().expect("retained row"),
264            })
265            .collect()
266    }
267
268    fn remove_rows(&mut self, mut removal: Removal<'_, K>) {
269        match &mut removal {
270            Removal::Sorted(unique) => self.rows.retain(|key, row| {
271                let keep = unique.contains_next(key);
272                if !keep {
273                    remove_tree(&row.scope.root);
274                }
275                keep
276            }),
277            Removal::Direct(removed) => {
278                // Match retain's ascending key order and detach before dropping the key.
279                reconcile::sort_few(removed, &self.order);
280                for &index in removed.iter() {
281                    if let Some(entry) = self.rows.remove_entry(&self.order[index]) {
282                        remove_tree(&entry.1.scope.root);
283                        drop(entry);
284                    }
285                }
286            }
287        }
288    }
289
290    fn merge_rows(&mut self, staged: &mut BTreeMap<K, Row<T>>) {
291        if self.rows.is_empty() {
292            self.rows = std::mem::take(staged);
293        } else if staged.len() <= self.rows.len() / (self.rows.len().ilog2() as usize + 1) {
294            // Sparse insertions preserve the existing tree; dense insertions merge linearly.
295            self.rows.extend(std::mem::take(staged));
296        } else {
297            self.rows.append(staged);
298        }
299    }
300
301    fn position_rows(&self, stationary: &[bool]) -> Result<(), JsValue> {
302        for (index, keep) in stationary.iter().enumerate().rev() {
303            if !keep {
304                let anchor = self
305                    .order
306                    .get(index + 1)
307                    .map(|key| self.rows[key].scope.root.as_ref());
308                strings::insert_before(
309                    &self.container,
310                    &self.rows[&self.order[index]].scope.root,
311                    anchor,
312                )?;
313            }
314        }
315        Ok(())
316    }
317
318    fn commit_rows(&self, fresh_keys: Option<Vec<K>>) {
319        let idle = self.queue.as_ref().is_none_or(|queue| queue.is_idle());
320        match fresh_keys.filter(|_| idle) {
321            Some(keys) => {
322                for key in &keys {
323                    self.rows[key].scope.commit();
324                }
325            }
326            None => {
327                for row in self.rows.values() {
328                    row.scope.commit();
329                }
330            }
331        }
332    }
333}
334
335/// Adopt the server-rendered rows, which must match `keys` in order.
336fn server_rows<K>(
337    container: &Element,
338    keys: &[K],
339    encode: &EncodeKey<K>,
340) -> Result<Vec<Element>, JsValue> {
341    // Compare every row natively in one call. Keys encode in order: a failed
342    // encoding is reported after the rows before it and its own row are
343    // checked, as when comparing one row at a time.
344    let mut encoded = String::new();
345    let mut failure = None;
346    let mut count = 0;
347    for key in keys {
348        match encode(key) {
349            // A separator inside an encoding needs the one-at-a-time path.
350            Ok(value) if value.contains('\n') => {
351                return server_rows_one_by_one(container, keys, encode);
352            }
353            Ok(value) => {
354                if count > 0 {
355                    encoded.push('\n');
356                }
357                encoded.push_str(&value);
358                count += 1;
359            }
360            Err(error) => {
361                failure = Some(error);
362                break;
363            }
364        }
365    }
366    let rows = strings::server_rows(container, &encoded, count as u32, failure.is_none())?;
367    if let Some(error) = failure {
368        return Err(error);
369    }
370    Ok((0..count as u32)
371        .map(|index| rows.get(index).unchecked_into())
372        .collect())
373}
374
375fn server_rows_one_by_one<K>(
376    container: &Element,
377    keys: &[K],
378    encode: &EncodeKey<K>,
379) -> Result<Vec<Element>, JsValue> {
380    let mut node = container.first_element_child();
381    let mut rows = Vec::with_capacity(keys.len());
382    for key in keys {
383        let row = node
384            .take()
385            .ok_or_else(|| JsValue::from_str("missing native row"))?;
386        if strings::attribute(&row, strings::Name::Key).as_deref() != Some(encode(key)?.as_str()) {
387            return Err(JsValue::from_str("native row key mismatch"));
388        }
389        node = row.next_element_sibling();
390        rows.push(row);
391    }
392    if node.is_some() {
393        return Err(JsValue::from_str("unexpected native row"));
394    }
395    Ok(rows)
396}
397
398type Selection = (u32, u32, String);
399
400/// The focused descendant of `container` and, for an input, its selection.
401fn focused(document: &Document, container: &Element) -> Option<(HtmlElement, Option<Selection>)> {
402    let focused = document
403        .active_element()
404        .filter(|node| container.contains(Some(node)))?
405        .dyn_into::<HtmlElement>()
406        .ok()?;
407    let selection = focused.dyn_ref::<HtmlInputElement>().and_then(|input| {
408        Some((
409            input.selection_start().ok()??,
410            input.selection_end().ok()??,
411            input.selection_direction().ok()??,
412        ))
413    });
414    Some((focused, selection))
415}
416
417// insertBefore can blur a node even when moving it within the same list.
418// Restore focus only if that original node survives.
419fn restore_focus(
420    document: &Document,
421    container: &Element,
422    focused: Option<(HtmlElement, Option<Selection>)>,
423) -> Result<(), JsValue> {
424    let Some((focused, selection)) = focused.filter(|(node, _)| container.contains(Some(node)))
425    else {
426        return Ok(());
427    };
428    if document
429        .active_element()
430        .is_some_and(|node| node.is_same_node(Some(&focused)))
431    {
432        return Ok(());
433    }
434    focused.focus()?;
435    if let (Some(input), Some((start, end, direction))) =
436        (focused.dyn_ref::<HtmlInputElement>(), selection)
437    {
438        input.set_selection_range_with_direction(start, end, &direction)?;
439    }
440    Ok(())
441}