|
| 1 | +using System.Collections.Generic; |
| 2 | +using System.Diagnostics.CodeAnalysis; |
| 3 | + |
| 4 | +namespace Abies.DOM |
| 5 | +{ |
| 6 | + /// <summary> |
| 7 | + /// Provides diffing and patching utilities for the virtual DOM. |
| 8 | + /// The implementation is inspired by Elm's VirtualDom diff algorithm |
| 9 | + /// and is written with performance in mind. |
| 10 | + /// </summary> |
| 11 | + public static class Operations |
| 12 | + { |
| 13 | + /// <summary> |
| 14 | + /// Apply a patch to the real DOM by invoking JavaScript interop. |
| 15 | + /// </summary> |
| 16 | + public static async Task Apply(Patch patch) |
| 17 | + { |
| 18 | + switch (patch) |
| 19 | + { |
| 20 | + case AddRoot addRoot: |
| 21 | + await Interop.SetAppContent(Render.Html(addRoot.Element)); |
| 22 | + break; |
| 23 | + case ReplaceChild replaceChild: |
| 24 | + await Interop.ReplaceChildHtml(replaceChild.OldElement.Id, Render.Html(replaceChild.NewElement)); |
| 25 | + break; |
| 26 | + case AddChild addChild: |
| 27 | + await Interop.AddChildHtml(addChild.Parent.Id, Render.Html(addChild.Child)); |
| 28 | + break; |
| 29 | + case RemoveChild removeChild: |
| 30 | + await Interop.RemoveChild(removeChild.Parent.Id, removeChild.Child.Id); |
| 31 | + break; |
| 32 | + case UpdateAttribute updateAttribute: |
| 33 | + await Interop.UpdateAttribute(updateAttribute.Element.Id, updateAttribute.Attribute.Name, updateAttribute.Value); |
| 34 | + break; |
| 35 | + case AddAttribute addAttribute: |
| 36 | + await Interop.AddAttribute(addAttribute.Element.Id, addAttribute.Attribute.Name, addAttribute.Attribute.Value); |
| 37 | + break; |
| 38 | + case RemoveAttribute removeAttribute: |
| 39 | + await Interop.RemoveAttribute(removeAttribute.Element.Id, removeAttribute.Attribute.Name); |
| 40 | + break; |
| 41 | + case AddHandler addHandler: |
| 42 | + await Interop.AddAttribute(addHandler.Element.Id, addHandler.Handler.Name, addHandler.Handler.Value); |
| 43 | + break; |
| 44 | + case RemoveHandler removeHandler: |
| 45 | + await Interop.RemoveAttribute(removeHandler.Element.Id, removeHandler.Handler.Name); |
| 46 | + break; |
| 47 | + case UpdateText updateText: |
| 48 | + await Interop.UpdateTextContent(updateText.Node.Id, updateText.Text); |
| 49 | + break; |
| 50 | + default: |
| 51 | + throw new InvalidOperationException("Unknown patch type"); |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + /// <summary> |
| 56 | + /// Compute the list of patches that transform <paramref name="oldNode"/> into <paramref name="newNode"/>. |
| 57 | + /// </summary> |
| 58 | + /// <param name="oldNode">The previous virtual DOM node. Can be <c>null</c> when rendering for the first time.</param> |
| 59 | + /// <param name="newNode">The new virtual DOM node.</param> |
| 60 | + public static List<Patch> Diff(Node? oldNode, Node newNode) |
| 61 | + { |
| 62 | + var patches = new List<Patch>(); |
| 63 | + if (oldNode is null) |
| 64 | + { |
| 65 | + patches.Add(new AddRoot((Element)newNode)); |
| 66 | + return patches; |
| 67 | + } |
| 68 | + |
| 69 | + DiffInternal(oldNode, newNode, null, patches); |
| 70 | + return patches; |
| 71 | + } |
| 72 | + |
| 73 | + private static void DiffInternal(Node oldNode, Node newNode, Element? parent, List<Patch> patches) |
| 74 | + { |
| 75 | + // Text nodes only need an update when the value changes |
| 76 | + if (oldNode is Text oldText && newNode is Text newText) |
| 77 | + { |
| 78 | + if (!string.Equals(oldText.Value, newText.Value, StringComparison.Ordinal)) |
| 79 | + patches.Add(new UpdateText(oldText, newText.Value)); |
| 80 | + return; |
| 81 | + } |
| 82 | + |
| 83 | + // Elements may need to be replaced when the tag differs or the node type changed |
| 84 | + if (oldNode is Element oldElement && newNode is Element newElement) |
| 85 | + { |
| 86 | + if (!string.Equals(oldElement.Tag, newElement.Tag, StringComparison.Ordinal)) |
| 87 | + { |
| 88 | + if (parent == null) |
| 89 | + patches.Add(new AddRoot(newElement)); |
| 90 | + else |
| 91 | + patches.Add(new ReplaceChild(oldElement, newElement)); |
| 92 | + return; |
| 93 | + } |
| 94 | + |
| 95 | + DiffAttributes(oldElement, newElement, patches); |
| 96 | + DiffChildren(oldElement, newElement, patches); |
| 97 | + return; |
| 98 | + } |
| 99 | + |
| 100 | + // Fallback for node type mismatch |
| 101 | + if (oldNode is Element oe && newNode is Element ne && parent != null) |
| 102 | + { |
| 103 | + patches.Add(new ReplaceChild(oe, ne)); |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + // Diff attribute collections using dictionaries for O(n) lookup |
| 108 | + private static void DiffAttributes(Element oldElement, Element newElement, List<Patch> patches) |
| 109 | + { |
| 110 | + var oldMap = new Dictionary<string, Attribute>(oldElement.Attributes.Length); |
| 111 | + foreach (var attr in oldElement.Attributes) |
| 112 | + oldMap[attr.Id] = attr; |
| 113 | + |
| 114 | + foreach (var newAttr in newElement.Attributes) |
| 115 | + { |
| 116 | + if (oldMap.TryGetValue(newAttr.Id, out var oldAttr)) |
| 117 | + { |
| 118 | + oldMap.Remove(newAttr.Id); |
| 119 | + if (!newAttr.Equals(oldAttr)) |
| 120 | + { |
| 121 | + if (oldAttr is Handler oldHandler) |
| 122 | + patches.Add(new RemoveHandler(oldElement, oldHandler)); |
| 123 | + else if (newAttr is Handler) |
| 124 | + patches.Add(new RemoveAttribute(oldElement, oldAttr)); |
| 125 | + |
| 126 | + if (newAttr is Handler newHandler) |
| 127 | + patches.Add(new AddHandler(newElement, newHandler)); |
| 128 | + else |
| 129 | + patches.Add(new UpdateAttribute(oldElement, newAttr, newAttr.Value)); |
| 130 | + } |
| 131 | + } |
| 132 | + else |
| 133 | + { |
| 134 | + if (newAttr is Handler handler) |
| 135 | + patches.Add(new AddHandler(newElement, handler)); |
| 136 | + else |
| 137 | + patches.Add(new AddAttribute(newElement, newAttr)); |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + // Any remaining old attributes must be removed |
| 142 | + foreach (var remaining in oldMap.Values) |
| 143 | + { |
| 144 | + if (remaining is Handler handler) |
| 145 | + patches.Add(new RemoveHandler(oldElement, handler)); |
| 146 | + else |
| 147 | + patches.Add(new RemoveAttribute(oldElement, remaining)); |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + private static void DiffChildren(Element oldParent, Element newParent, List<Patch> patches) |
| 152 | + { |
| 153 | + var oldChildren = oldParent.Children; |
| 154 | + var newChildren = newParent.Children; |
| 155 | + int shared = Math.Min(oldChildren.Length, newChildren.Length); |
| 156 | + |
| 157 | + // Diff children that exist in both trees |
| 158 | + for (int i = 0; i < shared; i++) |
| 159 | + DiffInternal(oldChildren[i], newChildren[i], oldParent, patches); |
| 160 | + |
| 161 | + // Remove extra old children |
| 162 | + for (int i = shared; i < oldChildren.Length; i++) |
| 163 | + { |
| 164 | + if (oldChildren[i] is Element oldChild) |
| 165 | + patches.Add(new RemoveChild(oldParent, oldChild)); |
| 166 | + } |
| 167 | + |
| 168 | + // Add additional new children |
| 169 | + for (int i = shared; i < newChildren.Length; i++) |
| 170 | + { |
| 171 | + if (newChildren[i] is Element newChild) |
| 172 | + patches.Add(new AddChild(newParent, newChild)); |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | +} |
0 commit comments