percy_dom/
patch.rs

1//! Our Patch enum is intentionally kept in it's own file for easy inclusion into
2//! The Percy Book.
3
4use std::collections::{HashMap, HashSet};
5
6pub use apply_patches::patch;
7
8use crate::event::{EventHandler, EventName};
9use crate::{AttributeValue, VText, VirtualNode};
10
11mod apply_patches;
12
13// TODO: pub(crate) BreadthFirstNodeIdx(pub u32);
14type BreadthFirstNodeIdx = u32;
15
16/// A Patch encodes an operation that modifies a real DOM element.
17///
18/// To update the real DOM that a user sees you'll want to first diff your
19/// old virtual dom and new virtual dom.
20///
21/// This diff operation will generate `Vec<Patch>` with zero or more patches that, when
22/// applied to your real DOM, will make your real DOM look like your new virtual dom.
23///
24/// Each Patch has a u32 node index that helps us identify the real DOM node that it applies to.
25///
26/// Our old virtual dom's nodes are indexed breadth first, as shown in this illustration
27/// (0 being the root node, 1 being it's first child, 2 being it's second child).
28///
29/// ```text
30///             .─.
31///            ( 0 )
32///             `┬'
33///         ┌────┴──────┐
34///         │           │
35///         ▼           ▼
36///        .─.         .─.
37///       ( 1 )       ( 2 )
38///        `┬'         `─'
39///    ┌────┴───┐       │
40///    │        │       ├─────┬─────┐
41///    ▼        ▼       │     │     │
42///   .─.      .─.      ▼     ▼     ▼
43///  ( 3 )    ( 4 )    .─.   .─.   .─.
44///   `─'      `─'    ( 5 ) ( 6 ) ( 7 )
45///                    `─'   `─'   `─'
46/// ```
47///
48/// The patching process is tested in a real browser in crates/percy-dom/tests/diff_patch.rs
49// We use breadth-first traversal since it lets us know the indices of all of a node's siblings if we
50// that node's index. We make use of this when diffing keyed lists.
51// We haven't thought deeply through the implications of breadth first vs. depth first diffing.
52// We can worry about that whenever we optimize our diffing and patching algorithms.
53#[derive(Debug)]
54#[cfg_attr(any(test, feature = "__test-utils"), derive(PartialEq))]
55// TODO: Change all of these tuple structs with a `NodeIdx` to instead be `{old_idx: NodeIdx`} so
56//  we can more easily tell which patches use the old node's index vs. the new one's.
57pub enum Patch<'a> {
58    /// Append a vector of child nodes to a parent node id.
59    #[allow(missing_docs)]
60    AppendChildren {
61        parent_old_node_idx: BreadthFirstNodeIdx,
62        new_nodes: Vec<&'a VirtualNode>,
63    },
64    /// Move the nodes to be the last of their parent's children.
65    #[allow(missing_docs)]
66    MoveToEndOfSiblings {
67        parent_old_node_idx: BreadthFirstNodeIdx,
68        siblings_to_move: Vec<BreadthFirstNodeIdx>,
69    },
70    /// Remove child nodes.
71    RemoveChildren {
72        /// The parent of the node that is being removed.
73        parent_old_node_idx: BreadthFirstNodeIdx,
74        /// The nodes to remove.
75        to_remove: Vec<BreadthFirstNodeIdx>,
76    },
77    /// Replace a node with another node. This typically happens when a node's tag changes.
78    /// ex: <div> becomes <span>
79    #[allow(missing_docs)]
80    Replace {
81        old_idx: BreadthFirstNodeIdx,
82        new_node: &'a VirtualNode,
83    },
84    /// Insert a new element before some other sibling element.
85    #[allow(missing_docs)]
86    InsertBefore {
87        /// The node that isn't moving and is having another node inserted before it.
88        anchor_old_node_idx: BreadthFirstNodeIdx,
89        new_nodes: Vec<&'a VirtualNode>,
90    },
91    /// Move nodes to be before some other node.
92    #[allow(missing_docs)]
93    MoveNodesBefore {
94        /// The node that we are moving other nodes to come before.
95        /// This node is *NOT* moving during this patch.
96        anchor_old_node_idx: BreadthFirstNodeIdx,
97        /// The old node indices of the nodes that are being moved.
98        to_move: Vec<BreadthFirstNodeIdx>,
99    },
100    /// The value attribute of a textarea or input element has not changed, but we will still patch
101    /// it anyway in case something was typed into the field.
102    ValueAttributeUnchanged(BreadthFirstNodeIdx, &'a AttributeValue),
103    /// The checked attribute of a input element has not changed, but we will still patch
104    /// it anyway in case the checkbox was toggled by the user. Otherwise the checkbox
105    /// could have a different state to what is rendered.
106    CheckedAttributeUnchanged(BreadthFirstNodeIdx, &'a AttributeValue),
107    /// Add attributes that the new node has that the old node does not
108    AddAttributes(BreadthFirstNodeIdx, HashMap<&'a str, &'a AttributeValue>),
109    /// Remove attributes that the old node had that the new node doesn't
110    RemoveAttributes(BreadthFirstNodeIdx, Vec<&'a str>),
111    /// Change the text of a Text node.
112    ChangeText(BreadthFirstNodeIdx, &'a VText),
113    /// Patches that apply to [`SpecialAttributes`].
114    SpecialAttribute(PatchSpecialAttribute<'a>),
115    /// Insert events in the EventsByNodeIdx.
116    /// If it is a non-delegated event the event will also get added to the DOM node.
117    AddEvents(
118        BreadthFirstNodeIdx,
119        HashMap<&'a EventName, &'a EventHandler>,
120    ),
121    /// Remove events from the EventsByNodeIdx.
122    /// If it is a non-delegated event the event will also get removed from the DOM node.
123    RemoveEvents(BreadthFirstNodeIdx, Vec<(&'a EventName, &'a EventHandler)>),
124    /// Delete all events in the EventsByNodeIdx for the given index, since the node has been
125    /// removed from the DOM.
126    RemoveAllVirtualEventsWithNodeIdx(BreadthFirstNodeIdx),
127}
128
129/// Patches that apply to [`SpecialAttributes`].
130#[derive(Debug, PartialEq)]
131pub enum PatchSpecialAttribute<'a> {
132    /// Call the [`SpecialAttributes.on_create_elem`] function on the node.
133    ///
134    /// We only push this patch for existing nodes.
135    /// New nodes get their on_create_element called automatically when they are created.
136    CallOnCreateElemOnExistingNode(BreadthFirstNodeIdx, &'a VirtualNode),
137    /// Call the [`SpecialAttributes.on_remove_elem`] function on the node.
138    CallOnRemoveElem(BreadthFirstNodeIdx, &'a VirtualNode),
139    /// Set the node's innerHTML using the [`SpecialAttributes.dangerous_inner_html`].
140    SetDangerousInnerHtml(BreadthFirstNodeIdx, &'a VirtualNode),
141    /// Set the node's innerHTML to an empty string.
142    RemoveDangerousInnerHtml(BreadthFirstNodeIdx),
143}
144
145impl<'a> Patch<'a> {
146    /// Every Patch is meant to be applied to a specific node within the DOM. Get the
147    /// index of the DOM node that this patch should apply to. DOM nodes are indexed
148    /// depth first with the root node in the tree having index 0.
149    pub(crate) fn old_node_idx(&self) -> BreadthFirstNodeIdx {
150        match self {
151            Patch::AppendChildren {
152                parent_old_node_idx: old_idx,
153                ..
154            } => *old_idx,
155            Patch::Replace { old_idx, .. } => *old_idx,
156            Patch::AddAttributes(node_idx, _) => *node_idx,
157            Patch::RemoveAttributes(node_idx, _) => *node_idx,
158            Patch::ChangeText(node_idx, _) => *node_idx,
159            Patch::ValueAttributeUnchanged(node_idx, _) => *node_idx,
160            Patch::CheckedAttributeUnchanged(node_idx, _) => *node_idx,
161            Patch::SpecialAttribute(special) => match special {
162                PatchSpecialAttribute::CallOnCreateElemOnExistingNode(node_idx, _) => *node_idx,
163                PatchSpecialAttribute::SetDangerousInnerHtml(node_idx, _) => *node_idx,
164                PatchSpecialAttribute::RemoveDangerousInnerHtml(node_idx) => *node_idx,
165                PatchSpecialAttribute::CallOnRemoveElem(node_idx, _) => *node_idx,
166            },
167            Patch::RemoveEvents(node_idx, _) => *node_idx,
168            Patch::AddEvents(node_idx, _) => *node_idx,
169            Patch::RemoveAllVirtualEventsWithNodeIdx(node_idx) => {
170                // TODO: We don't actually need the old node for this particular patch..
171                //  so we should stop making use of this. Perhaps use `unreachable!()` and fix
172                //  the places where we use this to stop calling this `.old_node_idx` function.
173                *node_idx
174            }
175            Patch::InsertBefore {
176                anchor_old_node_idx: old_idx,
177                ..
178            } => *old_idx,
179            Patch::MoveNodesBefore {
180                anchor_old_node_idx,
181                ..
182            } => *anchor_old_node_idx,
183            Patch::MoveToEndOfSiblings {
184                parent_old_node_idx,
185                ..
186            } => *parent_old_node_idx,
187            Patch::RemoveChildren {
188                parent_old_node_idx: parent_old_idx,
189                ..
190            } => *parent_old_idx,
191        }
192    }
193
194    /// Insert the node indices that are needed in order to perform this patch.
195    pub(crate) fn insert_node_indices_to_find(&self, to_find: &mut HashSet<u32>) {
196        match self {
197            Patch::AppendChildren {
198                parent_old_node_idx: old_idx,
199                ..
200            } => {
201                to_find.insert(*old_idx);
202            }
203            Patch::Replace { old_idx, .. } => {
204                to_find.insert(*old_idx);
205            }
206            Patch::AddAttributes(node_idx, _) => {
207                to_find.insert(*node_idx);
208            }
209            Patch::RemoveAttributes(node_idx, _) => {
210                to_find.insert(*node_idx);
211            }
212            Patch::ChangeText(node_idx, _) => {
213                to_find.insert(*node_idx);
214            }
215            Patch::ValueAttributeUnchanged(node_idx, _) => {
216                to_find.insert(*node_idx);
217            }
218            Patch::CheckedAttributeUnchanged(node_idx, _) => {
219                to_find.insert(*node_idx);
220            }
221            Patch::SpecialAttribute(special) => match special {
222                PatchSpecialAttribute::CallOnCreateElemOnExistingNode(node_idx, _) => {
223                    to_find.insert(*node_idx);
224                }
225                PatchSpecialAttribute::SetDangerousInnerHtml(node_idx, _) => {
226                    to_find.insert(*node_idx);
227                }
228                PatchSpecialAttribute::RemoveDangerousInnerHtml(node_idx) => {
229                    to_find.insert(*node_idx);
230                }
231                PatchSpecialAttribute::CallOnRemoveElem(node_idx, _) => {
232                    to_find.insert(*node_idx);
233                }
234            },
235            Patch::RemoveEvents(node_idx, _) => {
236                to_find.insert(*node_idx);
237            }
238            Patch::AddEvents(node_idx, _) => {
239                to_find.insert(*node_idx);
240            }
241            Patch::RemoveAllVirtualEventsWithNodeIdx(node_idx) => {
242                // TODO: We don't actually need the old node for this particular patch..
243                //  so we should stop making use of this. Perhaps use `unreachable!()` and fix
244                //  the places where we use this to stop calling this `.old_node_idx` function.
245                to_find.insert(*node_idx);
246            }
247            Patch::InsertBefore {
248                anchor_old_node_idx: old_idx,
249                ..
250            } => {
251                to_find.insert(*old_idx);
252            }
253            Patch::MoveNodesBefore {
254                anchor_old_node_idx,
255                to_move,
256            } => {
257                to_find.insert(*anchor_old_node_idx);
258                for idx in to_move {
259                    to_find.insert(*idx);
260                }
261            }
262            Patch::MoveToEndOfSiblings {
263                parent_old_node_idx,
264                siblings_to_move,
265            } => {
266                to_find.insert(*parent_old_node_idx);
267                for idx in siblings_to_move {
268                    to_find.insert(*idx);
269                }
270            }
271            Patch::RemoveChildren {
272                parent_old_node_idx,
273                to_remove,
274            } => {
275                to_find.insert(*parent_old_node_idx);
276                for idx in to_remove {
277                    to_find.insert(*idx);
278                }
279            }
280        }
281    }
282}