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}