Skip to content

Commit ce1d8fd

Browse files
authored
Merge pull request #9 from Picea/bpkkkp-codex/investigate-missing-node-errors-in-abies.conduit
Fix DOM patch errors by preserving element ids
2 parents 3f997bf + 3a561ed commit ce1d8fd

2 files changed

Lines changed: 98 additions & 5 deletions

File tree

Abies.Tests/DomBehaviorTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,35 @@ public void AttributeChanges_ShouldReflectInResult()
5858
Assert.Equal(Render.Html(newDom), Render.Html(result));
5959
}
6060

61+
[Fact]
62+
public void AttributeIdChange_ShouldNotRemoveAttribute()
63+
{
64+
var oldDom = new Element("1", "div",
65+
new DOMAttribute[] { new DOMAttribute("a1", "class", "foo") },
66+
System.Array.Empty<Node>());
67+
68+
var newDom = new Element("1", "div",
69+
new DOMAttribute[] { new DOMAttribute("a2", "class", "foo") },
70+
System.Array.Empty<Node>());
71+
72+
var patches = Operations.Diff(oldDom, newDom);
73+
var result = ApplyPatches(oldDom, patches, oldDom);
74+
75+
Assert.Equal(Render.Html(newDom), Render.Html(result));
76+
}
77+
78+
[Fact]
79+
public void Render_ShouldIncludeElementIds()
80+
{
81+
var dom = new Element("el1", "div", System.Array.Empty<DOMAttribute>(),
82+
new Element("child", "span", System.Array.Empty<DOMAttribute>(), new Text("t", "hi")));
83+
84+
var html = Render.Html(dom);
85+
86+
Assert.Contains("id=\"el1\"", html);
87+
Assert.Contains("id=\"child\"", html);
88+
}
89+
6190

6291
private static Node? ApplyPatches(Node? root, IEnumerable<Patch> patches, Node? initialRoot)
6392
{

Abies/Types.cs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Runtime.CompilerServices;
44
using System.Runtime.InteropServices;
55
using System.Runtime.InteropServices.JavaScript;
6+
using System.Threading;
67
using Abies;
78
using Abies.DOM;
89

@@ -164,6 +165,11 @@ public record Program<TApplication, TArguments, TModel> : Program
164165
private Node? _dom;
165166
// todo: clean up handlers when they are no longer needed
166167
private readonly ConcurrentDictionary<string, Message> _handlers = new();
168+
// Dispatch may be triggered from multiple threads (JavaScript events or
169+
// asynchronous API commands). DOM updates must run sequentially to keep
170+
// patches in order, otherwise operations like add/remove attribute can
171+
// target missing nodes.
172+
private readonly SemaphoreSlim _dispatchLock = new(1, 1);
167173

168174
public async Task Run(TArguments arguments)
169175
{
@@ -210,8 +216,57 @@ private void RegisterHandlers(Node node)
210216
}
211217
}
212218

219+
private static Node PreserveIds(Node? oldNode, Node newNode)
220+
{
221+
if (oldNode is Element oldElement && newNode is Element newElement && oldElement.Tag == newElement.Tag)
222+
{
223+
// Preserve attribute IDs so DiffAttributes can emit UpdateAttribute
224+
// instead of a remove/add pair. This avoids wiping attributes when
225+
// remove is processed after add.
226+
var attrs = new Abies.DOM.Attribute[newElement.Attributes.Length];
227+
for (int i = 0; i < newElement.Attributes.Length; i++)
228+
{
229+
var attr = newElement.Attributes[i];
230+
var oldAttr = Array.Find(oldElement.Attributes, a => a.Name == attr.Name);
231+
var attrId = oldAttr?.Id ?? attr.Id;
232+
233+
if (attr.Name == "id")
234+
{
235+
attrs[i] = attr with { Id = attrId, Value = oldElement.Id };
236+
}
237+
else
238+
{
239+
attrs[i] = attr with { Id = attrId };
240+
}
241+
}
242+
243+
var children = new Node[newElement.Children.Length];
244+
for (int i = 0; i < newElement.Children.Length; i++)
245+
{
246+
var oldChild = i < oldElement.Children.Length ? oldElement.Children[i] : null;
247+
children[i] = PreserveIds(oldChild, newElement.Children[i]);
248+
}
249+
250+
return new Element(oldElement.Id, newElement.Tag, attrs, children);
251+
}
252+
else if (newNode is Element newElem)
253+
{
254+
var children = new Node[newElem.Children.Length];
255+
for (int i = 0; i < newElem.Children.Length; i++)
256+
{
257+
children[i] = PreserveIds(null, newElem.Children[i]);
258+
}
259+
return new Element(newElem.Id, newElem.Tag, newElem.Attributes, children);
260+
}
261+
262+
return newNode;
263+
}
264+
213265
public async Task Dispatch(Message message)
214266
{
267+
await _dispatchLock.WaitAsync();
268+
try
269+
{
215270
if (model is null)
216271
{
217272
await Interop.WriteToConsole("Model not initialized");
@@ -232,8 +287,10 @@ public async Task Dispatch(Message message)
232287
// Generate new virtual DOM
233288
var newDocument = TApplication.View(newModel);
234289

290+
var alignedBody = PreserveIds(_dom, newDocument.Body);
291+
235292
// Compute the patches
236-
var patches = Operations.Diff(_dom, newDocument.Body);
293+
var patches = Operations.Diff(_dom, alignedBody);
237294

238295
// Apply patches and (de)register handlers
239296
foreach (var patch in patches)
@@ -257,14 +314,21 @@ public async Task Dispatch(Message message)
257314
}
258315

259316
// Update the current virtual DOM
260-
_dom = newDocument.Body;
317+
_dom = alignedBody;
261318
await Interop.SetTitle(newDocument.Title);
262319

263320
foreach (var command in commands)
264321
{
265322
await HandleCommand(command);
266323
}
267-
} private static async Task HandleCommand(Command command)
324+
}
325+
finally
326+
{
327+
_dispatchLock.Release();
328+
}
329+
}
330+
331+
private static async Task HandleCommand(Command command)
268332
{
269333
switch(command)
270334
{
@@ -432,7 +496,7 @@ private static void RenderNode(Node node, System.Text.StringBuilder sb)
432496
switch (node)
433497
{
434498
case Element element:
435-
sb.Append($"<{element.Tag}");
499+
sb.Append($"<{element.Tag} id=\"{element.Id}\"");
436500
foreach (var attr in element.Attributes)
437501
{
438502
if (attr is Handler handler)
@@ -450,7 +514,7 @@ private static void RenderNode(Node node, System.Text.StringBuilder sb)
450514
sb.Append($"</{element.Tag}>");
451515
break;
452516
case Text text:
453-
sb.Append($"<span id={text.Id}>{System.Web.HttpUtility.HtmlEncode(text.Value)}</span>");
517+
sb.Append($"<span id=\"{text.Id}\">{System.Web.HttpUtility.HtmlEncode(text.Value)}</span>");
454518
break;
455519
// Handle other node types if necessary
456520
default:

0 commit comments

Comments
 (0)