Skip to content

fix(binding.foreach): add afterRender/beforeMove/afterMove callbacks (#329)#369

Open
phillipc wants to merge 6 commits intomainfrom
fix/329-foreachbinding-does-not-match-the-documented-callback-api-of-foreach
Open

fix(binding.foreach): add afterRender/beforeMove/afterMove callbacks (#329)#369
phillipc wants to merge 6 commits intomainfrom
fix/329-foreachbinding-does-not-match-the-documented-callback-api-of-foreach

Conversation

@phillipc
Copy link
Copy Markdown
Member

@phillipc phillipc commented Apr 24, 2026

Summary

Closes #329.

Adds the documented foreach lifecycle callbacks that were missing from ForEachBinding in the reference build, without breaking the existing afterAdd/beforeRemove batch contract:

  • afterRender(nodes, value) — fires once per item when a new nodeset is rendered from the template (initial render and later additions).
  • beforeMove(node, newIndex, value) / afterMove(node, newIndex, value) — fire per retained node whose index changed; move detection compares the tracked firstLastNodesList entries against the current array values and only runs if at least one move callback is registered.
  • afterAdd({ nodeOrArrayInserted, foreachInstance }) and beforeRemove({ nodesToRemove, foreachInstance }) are unchanged, including the tko-specific thenable-return shortcut for deferred node removal.
  • Documentation (foreach-binding.md Note 8) now calls out the reference build's batch-shaped afterAdd/beforeRemove and the thenable extension as deviations from the original Knockout contract.

Test plan

  • bun run verify (5395 tests pass, 0 failures)
  • New spec coverage:
    • afterRender fires for initial and added items with (nodes, value)
    • beforeMove/afterMove fire with (node, newIndex, value) when retained items shift
  • Existing afterAdd/beforeRemove tests continue to pass unchanged (batch signature + thenable removal)
  • verified-behaviors.json updated
  • Changeset added (patch, additive only)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Foreach binding adds per-item lifecycle callbacks: afterRender (newly rendered items), beforeMove and afterMove (items that change position).
  • Documentation

    • Foreach docs updated with per-item callback signatures, compatibility notes, and that beforeRemove may return a thenable (tko-specific).
  • Tests

    • New tests cover afterRender, beforeMove, afterMove, move detection, and non-reactivity of observables read during afterRender.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@phillipc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 12 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 44 minutes and 12 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc11298e-8ae4-4290-8c38-ab1f08526924

📥 Commits

Reviewing files that changed from the base of the PR and between 3700a2c and 5ac8af4.

📒 Files selected for processing (2)
  • packages/binding.foreach/spec/eachBehavior.ts
  • packages/binding.foreach/src/foreach.ts
📝 Walkthrough

Walkthrough

Adds per-item lifecycle callbacks to ForEachBinding: implements afterRender(nodes, value), beforeMove(node, newIndex, value), and afterMove(node, newIndex, value); tracks item value per nodeset, computes moves during queue processing, and defers afterRender until DOM updates complete. Tests, docs, and metadata updated.

Changes

Cohort / File(s) Summary
Implementation
packages/binding.foreach/src/foreach.ts
Implements beforeMove/afterMove and afterRender support: stores value on nodeset entries, adds pendingAfterRender, computes moves in processQueue(), invokes move callbacks around the add/remove work, refactors node-range traversal (walkSiblingRange), and flushes deferred afterRender calls.
Tests
packages/binding.foreach/spec/eachBehavior.ts
Adds comprehensive tests for afterRender (initial, added, reused nodesets; non-reactivity of observables read during callback; error clearing of pending calls) and for beforeMove/afterMove (retained node identity, index/value correctness, primitives/no-move behavior, multi-root templates).
Docs & Metadata
.changeset/fix-foreach-callback-api.md, packages/binding.foreach/verified-behaviors.json, tko.io/src/content/docs/bindings/foreach-binding.md
Documents per-item callback signatures for afterRender/beforeMove/afterMove, clarifies afterAdd/beforeRemove remain batch-shaped, notes beforeRemove may return a thenable in this build, and updates verified-behaviors metadata.

Sequence Diagram(s)

sequenceDiagram
    participant Binding as ForEachBinding
    participant DOM as DOM / Template Engine
    participant Callback as User Callbacks
    rect rgba(200,230,201,0.5)
    Binding->>Binding: computeMoves(oldList, newList)
    Binding->>Callback: call beforeMove(node, newIndex, value)
    end
    rect rgba(187,222,251,0.5)
    Binding->>DOM: apply add/remove/move operations
    DOM-->>Binding: DOM updated
    end
    rect rgba(255,224,178,0.5)
    Binding->>Callback: call afterMove(node, newIndex, value)
    Binding->>Callback: flush pendingAfterRender: afterRender(nodes, value)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through lists and matched each little head,
BeforeMove I paused, then AfterMove I led.
AfterRender whispered to nodes freshly spun,
Values tucked beside them when the shifting was done.
A rabbit applauds — callbacks neatly run!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the primary changes: adding three missing lifecycle callbacks (afterRender, beforeMove, afterMove) to ForEachBinding.
Linked Issues check ✅ Passed The PR fully addresses issue #329 by implementing all three missing callbacks (afterRender, beforeMove, afterMove) while preserving existing batch-shaped afterAdd/beforeRemove behavior and documenting deviations.
Out of Scope Changes check ✅ Passed All changes directly support the core objective: adding lifecycle callbacks to ForEachBinding. No unrelated modifications detected across implementation, tests, documentation, and metadata files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/329-foreachbinding-does-not-match-the-documented-callback-api-of-foreach

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]

This comment was marked as outdated.

Co-authored-by: Copilot <copilot@github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/binding.foreach/spec/eachBehavior.ts (1)

894-929: Coverage works, but consider broadening the move scenarios.

The single splice-at-0 case correctly exercises both phases — parentText: 'first' before and 'addedfirst' after — which neatly captures the processQueue ordering (beforeMove → adds → afterMove, see packages/binding.foreach/src/foreach.ts:241-272). One scenario does leave gaps worth a follow-up:

  • No assertion that beforeMove/afterMove fire for multi-node moves (e.g., obs.reverse() on the existing "sorting complex data moves all DOM nodes" fixture).
  • No coverage that the callbacks are not invoked when an item's index is unchanged (only true moves should fire).
  • No guard against the registration optimization in processMoves — i.e., that move detection still works when only one of beforeMove/afterMove is registered.
♻️ Example additional case
it('fires for every retained node when the array is reversed', function () {
  const items = ['a', 'b', 'c'].map(name => ({ name }))
  const calls: Array<{ phase: string; index: number; value: any }> = []
  const arr = observableArray(items)
  const target = $(
    "<ul data-bind='foreach: { data: arr, beforeMove: beforeMove, afterMove: afterMove }'><li data-bind='text: name'></li></div>"
  )
  applyBindings(
    {
      arr,
      beforeMove: (_n, index, value) => calls.push({ phase: 'before', index, value }),
      afterMove: (_n, index, value) => calls.push({ phase: 'after', index, value })
    },
    target[0]
  )
  arr.reverse()
  // Expect 6 entries: 3 before + 3 after, each with the new index of the retained node
  assert.equal(calls.length, 6)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/binding.foreach/spec/eachBehavior.ts` around lines 894 - 929, Add
broader move-scenario tests to eachBehavior.ts: add a test that reverses an
observableArray to assert beforeMove/afterMove fire for every retained node
(e.g., expect 3 before + 3 after for a 3-item array), a test ensuring callbacks
are not invoked when an item's index remains unchanged, and a test where only
beforeMove or only afterMove is provided to ensure move-detection still
registers (refer to the existing beforeMove/afterMove handlers and the
processMoves logic). Locate the foreach binding setup in the current spec (the
target/applyBindings block) and duplicate similar setups for these scenarios,
use assertions on calls length and entries (phase, index, value, parentText
where applicable) to validate correct ordering and invocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/binding.foreach/spec/eachBehavior.ts`:
- Around line 894-929: Add broader move-scenario tests to eachBehavior.ts: add a
test that reverses an observableArray to assert beforeMove/afterMove fire for
every retained node (e.g., expect 3 before + 3 after for a 3-item array), a test
ensuring callbacks are not invoked when an item's index remains unchanged, and a
test where only beforeMove or only afterMove is provided to ensure
move-detection still registers (refer to the existing beforeMove/afterMove
handlers and the processMoves logic). Locate the foreach binding setup in the
current spec (the target/applyBindings block) and duplicate similar setups for
these scenarios, use assertions on calls length and entries (phase, index,
value, parentText where applicable) to validate correct ordering and invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5afcc42c-4fd4-4c4f-82db-83575547161d

📥 Commits

Reviewing files that changed from the base of the PR and between 43e69da and 7f298f3.

📒 Files selected for processing (1)
  • packages/binding.foreach/spec/eachBehavior.ts

phillipc and others added 2 commits April 25, 2026 08:57
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
packages/binding.foreach/src/foreach.ts (3)

342-349: Defensive reset for pendingAfterRender if a user callback throws.

If this.afterRender throws partway through the loop, pendingAfterRender is never cleared and stale entries will re-fire on the next processQueue. Wrapping the iteration in try { … } finally { this.pendingAfterRender = new Array() } would isolate user-callback failures. Niche edge case (and consistent with other callback sites here that don't catch), so feel free to defer.

🛡️ Optional hardening
   flushPendingAfterRender() {
-    if (typeof this.afterRender === 'function') {
-      arrayForEach(this.pendingAfterRender, item => {
-        this.afterRender(item.nodes, item.value)
-      })
-    }
-    this.pendingAfterRender = new Array()
+    const queue = this.pendingAfterRender
+    this.pendingAfterRender = new Array()
+    if (typeof this.afterRender === 'function') {
+      arrayForEach(queue, item => {
+        this.afterRender(item.nodes, item.value)
+      })
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/binding.foreach/src/foreach.ts` around lines 342 - 349, The loop in
flushPendingAfterRender uses this.afterRender and if a user callback throws it
never clears this.pendingAfterRender; wrap the arrayForEach invocation (or the
whole conditional block in flushPendingAfterRender) in a try { ... } finally {
this.pendingAfterRender = new Array() } so pendingAfterRender is always reset
even on exceptions from afterRender (refer to flushPendingAfterRender,
this.pendingAfterRender and this.afterRender).

328-340: Consolidate getNodesForEntry with the existing getNodesForIndex.

The new helper duplicates the sibling-walk logic in getNodesForIndex (lines 457–467); only the input shape differs. Consider sharing the implementation, e.g.:

♻️ Proposed consolidation
-  getNodesForEntry(entry: { first: Node; last: Node }) {
-    const result: Node[] = []
-    let ptr: Node | null = entry.first
-    const last = entry.last
-    result.push(ptr)
-    while (ptr && ptr !== last) {
-      ptr = ptr.nextSibling
-      if (ptr) {
-        result.push(ptr)
-      }
-    }
-    return result
-  }
@@
-  getNodesForIndex(index) {
-    const result = new Array()
-    let ptr = this.firstLastNodesList[index].first
-    const last = this.firstLastNodesList[index].last
-    result.push(ptr)
-    while (ptr && ptr !== last) {
-      ptr = ptr.nextSibling!
-      result.push(ptr)
-    }
-    return result
-  }
+  getNodesForEntry(entry: { first: Node; last: Node }): Node[] {
+    const result: Node[] = [entry.first]
+    let ptr: Node | null = entry.first
+    while (ptr && ptr !== entry.last) {
+      ptr = ptr.nextSibling
+      if (ptr) result.push(ptr)
+    }
+    return result
+  }
+
+  getNodesForIndex(index: number): Node[] {
+    return this.getNodesForEntry(this.firstLastNodesList[index])
+  }

Also applies to: 457-467

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/binding.foreach/src/foreach.ts` around lines 328 - 340, Duplicate
sibling-walk logic exists in getNodesForEntry and getNodesForIndex; consolidate
by reusing one implementation: either extract a shared helper (e.g.,
walkSiblingRange(first: Node, last: Node): Node[]) containing the loop and push
logic, then have getNodesForEntry(entry) call walkSiblingRange(entry.first,
entry.last) and have getNodesForIndex(index) call the same helper (or adapt
getNodesForIndex to accept first/last), or make getNodesForEntry construct the
same shape that getNodesForIndex expects and forward to it; update references to
getNodesForEntry and getNodesForIndex accordingly to remove the duplicated loop.

309-326: Add multi-root template tests for beforeMove/afterMove to explicitly document per-node callback invocation.

The code correctly invokes beforeMove/afterMove once per DOM node in a moved nodeset (not once per item like afterRender). Tests confirm this behavior for single-root templates, but multi-root cases are not covered. Add specs for multi-root templates to lock in this behavior and clarify the contract in documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/binding.foreach/src/foreach.ts` around lines 309 - 326, Add tests
that verify beforeMove/afterMove callbacks are invoked once per DOM node for
multi-root templates by exercising fireMoveCallback (which uses
firstLastNodesList and getNodesForEntry). Specifically, create multi-root
template scenarios where a logical item corresponds to multiple DOM nodes and
assert that the callback passed into fireMoveCallback is called per node (not
per item) with the correct node, newIndex, and value; cover both lookupKey
variants ('oldIndex' and 'newIndex') and beforeMove/afterMove usage to lock the
documented contract.
packages/binding.foreach/spec/eachBehavior.ts (2)

859-880: Tighten the reused-nodeset assertion.

includeMembers([a, c]) + notInclude(b) allows extra unexpected entries past index 3 to slip through silently. Pinning calls.length (or asserting calls.slice(3).length === 2) would lock the behavior more precisely against future regressions.

♻️ Proposed tightening
     arr.reverse()
     assert.equal(target.text(), 'cba')
-    const reusedValues = calls.slice(3).map(call => call.value)
+    const reusedCalls = calls.slice(3)
+    assert.equal(reusedCalls.length, 2)
+    const reusedValues = reusedCalls.map(call => call.value)
     assert.includeMembers(reusedValues, [a, c])
     assert.notInclude(reusedValues, b)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/binding.foreach/spec/eachBehavior.ts` around lines 859 - 880, The
assertion on reused nodes is too loose; tighten it by asserting the exact number
of afterRender calls for the reinsertion step. After the existing
assert.equal(calls.length, 3) and arr.reverse(), add an assertion that either
calls.slice(3).length === 2 or assert.equal(calls.length, 5), then keep the
existing check that the reusedValues (calls.slice(3).map(...)) contains exactly
the expected items [a, c] and does not include b; reference the calls array,
arr.reverse(), the afterRender callback cb, and reusedValues to locate where to
add the length check.

917-975: Move-callback coverage looks good; consider a multi-root template case.

The single-root template here exercises one callback per move, which masks the per-DOM-node firing in fireMoveCallback. A spec with a multi-root template (e.g., <li>x</li><li>y</li> per item, or a virtual block with multiple nodes) would lock down whether the callback should fire once per node or once per item — useful given this is a documented deviation point.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/binding.foreach/spec/eachBehavior.ts` around lines 917 - 975, Add a
new spec that uses a multi-root template to verify move callbacks fire per DOM
node: create observableArray with an object item, define beforeMove and
afterMove handlers that record node.textContent, index and value, use a template
that renders multiple roots per item (e.g., "<li data-bind='text: name'></li><li
data-bind='text: name'></li>" or a virtual multi-node block), applyBindings with
applyBindings(..., target[0]), perform an arr.splice to cause a shift, and
assert that the recorded calls from beforeMove/afterMove (and therefore
fireMoveCallback) equal the number of DOM nodes moved and contain the expected
node texts, indices and values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/binding.foreach/spec/eachBehavior.ts`:
- Around line 859-880: The assertion on reused nodes is too loose; tighten it by
asserting the exact number of afterRender calls for the reinsertion step. After
the existing assert.equal(calls.length, 3) and arr.reverse(), add an assertion
that either calls.slice(3).length === 2 or assert.equal(calls.length, 5), then
keep the existing check that the reusedValues (calls.slice(3).map(...)) contains
exactly the expected items [a, c] and does not include b; reference the calls
array, arr.reverse(), the afterRender callback cb, and reusedValues to locate
where to add the length check.
- Around line 917-975: Add a new spec that uses a multi-root template to verify
move callbacks fire per DOM node: create observableArray with an object item,
define beforeMove and afterMove handlers that record node.textContent, index and
value, use a template that renders multiple roots per item (e.g., "<li
data-bind='text: name'></li><li data-bind='text: name'></li>" or a virtual
multi-node block), applyBindings with applyBindings(..., target[0]), perform an
arr.splice to cause a shift, and assert that the recorded calls from
beforeMove/afterMove (and therefore fireMoveCallback) equal the number of DOM
nodes moved and contain the expected node texts, indices and values.

In `@packages/binding.foreach/src/foreach.ts`:
- Around line 342-349: The loop in flushPendingAfterRender uses this.afterRender
and if a user callback throws it never clears this.pendingAfterRender; wrap the
arrayForEach invocation (or the whole conditional block in
flushPendingAfterRender) in a try { ... } finally { this.pendingAfterRender =
new Array() } so pendingAfterRender is always reset even on exceptions from
afterRender (refer to flushPendingAfterRender, this.pendingAfterRender and
this.afterRender).
- Around line 328-340: Duplicate sibling-walk logic exists in getNodesForEntry
and getNodesForIndex; consolidate by reusing one implementation: either extract
a shared helper (e.g., walkSiblingRange(first: Node, last: Node): Node[])
containing the loop and push logic, then have getNodesForEntry(entry) call
walkSiblingRange(entry.first, entry.last) and have getNodesForIndex(index) call
the same helper (or adapt getNodesForIndex to accept first/last), or make
getNodesForEntry construct the same shape that getNodesForIndex expects and
forward to it; update references to getNodesForEntry and getNodesForIndex
accordingly to remove the duplicated loop.
- Around line 309-326: Add tests that verify beforeMove/afterMove callbacks are
invoked once per DOM node for multi-root templates by exercising
fireMoveCallback (which uses firstLastNodesList and getNodesForEntry).
Specifically, create multi-root template scenarios where a logical item
corresponds to multiple DOM nodes and assert that the callback passed into
fireMoveCallback is called per node (not per item) with the correct node,
newIndex, and value; cover both lookupKey variants ('oldIndex' and 'newIndex')
and beforeMove/afterMove usage to lock the documented contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1cdac2ee-d63d-441c-a9d2-476d58d91714

📥 Commits

Reviewing files that changed from the base of the PR and between 7f298f3 and f675c26.

📒 Files selected for processing (3)
  • packages/binding.foreach/spec/eachBehavior.ts
  • packages/binding.foreach/src/foreach.ts
  • tko.io/src/content/docs/bindings/foreach-binding.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tko.io/src/content/docs/bindings/foreach-binding.md

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/binding.foreach/src/foreach.ts (1)

254-287: ⚠️ Potential issue | 🟠 Major

Cleanup scope is too narrow — beforeMove/loop/afterMove throws can wedge the binding.

The try/finally only wraps flushPendingAfterRender() + endQueueFlush(), but three other call sites in processQueue can run user code that throws:

  • Line 260: fireMoveCallback(this.beforeMove, …)
  • Line 262–267: arrayForEach(this.changeQueue, …) (which invokes added()applyBindingsToDescendants, plus beforeRemove)
  • Line 276: fireMoveCallback(this.afterMove, …)

If any of these throws, this.changeQueue is never cleared. Worse, if beforeMove or the change-queue loop throws (i.e., before line 269), this.rendering_queued stays true, so subsequent onArrayChange calls will keep appending to changeQueue but will never schedule another processQueue (the guard at lines 229–230 short-circuits). The binding is effectively wedged until dispose.

The new spec at lines 959-992 (updates conditional state and does not replay a completed queue after afterRender throws) covers only the afterRender path; beforeMove/afterMove errors hit the unprotected window.

🛡️ Suggested wider cleanup scope
   processQueue() {
     const isEmpty = !unwrap(this.data).length
     let lowestIndexChanged = MAX_LIST_SIZE
     const moves = this.computeMoves(unwrap(this.data) || [])

-    this.startQueueFlush()
-    this.fireMoveCallback(this.beforeMove, moves, 'oldIndex')
-
-    arrayForEach(this.changeQueue, changeItem => {
-      if (typeof changeItem.index === 'number') {
-        lowestIndexChanged = Math.min(lowestIndexChanged, changeItem.index)
-      }
-      this[changeItem.status](changeItem)
-    })
-    this.flushPendingDeletes()
-    this.rendering_queued = false
-
-    // Update our indexes.
-    if (this.$indexHasBeenRequested) {
-      this.updateIndexes(lowestIndexChanged)
-    }
-
-    this.fireMoveCallback(this.afterMove, moves, 'newIndex')
     try {
+      this.startQueueFlush()
+      this.fireMoveCallback(this.beforeMove, moves, 'oldIndex')
+
+      arrayForEach(this.changeQueue, changeItem => {
+        if (typeof changeItem.index === 'number') {
+          lowestIndexChanged = Math.min(lowestIndexChanged, changeItem.index)
+        }
+        this[changeItem.status](changeItem)
+      })
+      this.flushPendingDeletes()
+
+      if (this.$indexHasBeenRequested) {
+        this.updateIndexes(lowestIndexChanged)
+      }
+
+      this.fireMoveCallback(this.afterMove, moves, 'newIndex')
       this.flushPendingAfterRender()
       this.endQueueFlush()
     } finally {
+      this.rendering_queued = false
       this.changeQueue = new Array()
-      // Update the conditional exposed on the domData
       if (isEmpty !== !this.isNotEmpty()) {
         this.isNotEmpty(!isEmpty)
       }
     }
   }

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d5fb5956-876e-41e7-8527-df228f4695be

📥 Commits

Reviewing files that changed from the base of the PR and between f675c26 and 3700a2c.

📒 Files selected for processing (2)
  • packages/binding.foreach/spec/eachBehavior.ts
  • packages/binding.foreach/src/foreach.ts

@phillipc
Copy link
Copy Markdown
Member Author

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ℹ️ Review info

Done.

@phillipc phillipc added the bug label May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ForEachBinding does not match the documented callback API of foreach

1 participant