Off‑by‑one error in `TControl.Loaded` flips consecutive `ChildOrder`s

1 What happens

I have 20+ forms in my project and on them 200+ controls. Every now and then, the carefully created rendering order of the controls gets scrambled. Even if the controls are perfectly ordered in the DFM file and the ChildOrder values are carefully set, it sometimes happens that controls are rendered in the wrong order into the html. If e.g. some controls are aligned alTop to each other, the bug may flip the vertical visual presentation in the browser, making a control with say ChildOrder 5 visually appear on top of the control with ChildOrder 4, which of course should be the other way around, i.e. they are vertically flipped. I was able to trace this problem down to procedure WEBLib.Controls.pasTControl.Loaded


2 Why it happens

In WEBLib.Controls.pasTControl.Loaded the framework re‑orders children like this:

if (Controls[i].ChildOrder < ChildContainer.childNodes.Length) then
 begin
   ChildContainer.insertBefore(Controls[i].Container,
    ChildContainer.childNodes[Controls[i].ChildOrder]);
 end;

When the control must move forward (ChildOrder > current index) the node is first removed, all later nodes shift –1, and the reference childNodes[ChildOrder] is now one position too early. The control re‑inserts at ChildOrder – 1, overtaking its neighbour.


3 Minimal patch (no API change)

Add a one‑slot compensation before calling insertBefore:

{ inside the loop }

      TargetIdx  := C.ChildOrder;
      TargetNode := C.Container;

      // fast path: want a slot beyond current list -> simple append
      if TargetIdx >= NodeLen then
      begin
        ChildContainer.appendChild(TargetNode);
        C.Loaded;
        Continue;
      end;

      // slow path: node may need compensation if moving forward
      asm
        // Find current index of TargetNode inside NodeList
        CurIdx = Array.prototype.indexOf.call(NodeList, TargetNode);
        // If it moves forward, compensate for the removal
        if (CurIdx !== -1 && TargetIdx > CurIdx) { TargetIdx++; }
      end;
      if CurIdx=-1 then; // compiler honey

      // fetch reference node for insertBefore (or nil if append)
      if TargetIdx < NodeLen then
        RefNode := NodeList.item(TargetIdx)
      else
        RefNode := nil; // should not happen after the ++ above

      if Assigned(RefNode) then
        ChildContainer.insertBefore(TargetNode, RefNode)
      else
        ChildContainer.appendChild(TargetNode);

(Full patched TControl.Loaded attached separately; the key is adjusting TargetIdx before insertBefore.)

TControl.Loaded.pas (3.0 KB)

Credits go to ChatGPT in helping analyzing and fixing this edge case. The provided fix did it for me, but I did not check for any side effects. Also, I was not able to reproduce this problem in a small demonstration project, but the following screen shots of my project demonstrate the issue.

This is how the unpatched Loaded procedure renders my stacked panels. As you can see, the panels with ChildOrders 3 and 4 are wrongly rendered in the order 4-3 (see TWebPanel.Name displayed to the right of the panels).

When applying the fix, the panels are rendered in the correct 3-4 order.

Thanks for this suggested improvement.
We are testing it. So far, we could not see regressions. We'll keep testing and when no regression can be found, it will be integrated.
Thanks again!