diff --git a/.changeset/binding-lifecycle-parity.md b/.changeset/binding-lifecycle-parity.md new file mode 100644 index 000000000..6b89323be --- /dev/null +++ b/.changeset/binding-lifecycle-parity.md @@ -0,0 +1,37 @@ +--- +"@tko/bind": minor +"@tko/binding.if": minor +"@tko/binding.component": minor +"@tko/binding.core": minor +--- + +Restore Knockout 3.5 binding-lifecycle semantics for `descendantsComplete` and +add `completeOn: "render"`. + +`@tko/bind` now owns the KO 3.5 async-completion bookkeeping +(`bindingEvent.startPossiblyAsyncContentBinding` / `AsyncCompleteContext`). +`childrenComplete` drives per-node tracking of pending async descendants, and +`descendantsComplete` fires once a node's children **and** every +asynchronously-completing descendant (components, conditionals) have bound. + +Behavior changes to be aware of: + +- **`descendantsComplete` re-arms per content cycle.** Previously it fired at + most once (when the initial binding pass settled), and never fired for a + conditional that started false. It now fires each time a node renders content + — a nested `if` becoming true re-fires the ancestor's `descendantsComplete`, + and toggling a conditional off then on fires it again. Consumers who attached + one-shot `descendantsComplete` callbacks may see additional calls. +- **`completeOn: "render"`** is now supported on `if`/`ifnot`/`with`. It defers + the node's `childrenComplete` (and any ancestor `descendantsComplete`, or a + component's `koDescendantsComplete`) until the binding actually renders + content. `completeOn` is a reserved binding key (as in KO 3.5). +- A component viewmodel's **`koDescendantsComplete`** now fires via the + element's `descendantsComplete` event, so inner components complete before + their outer component (KO 3.5 ordering), including across intermediate + bindings and several nesting layers. + +Fixes the root cause behind +[#414](https://github.com/knockout/tko/issues/414): `koDescendantsComplete` is +no longer suppressed when a component template contains a conditional that +starts false. diff --git a/builds/knockout/spec/components/componentBindingBehaviors.js b/builds/knockout/spec/components/componentBindingBehaviors.js index a3ef45967..065dd926d 100644 --- a/builds/knockout/spec/components/componentBindingBehaviors.js +++ b/builds/knockout/spec/components/componentBindingBehaviors.js @@ -295,9 +295,7 @@ describe('Components: Component binding', function () { expect(renderedCount).to.equal(1) }) - // @mbest - This fails b/c `test-component`'s koDescendantsComplete is called - // after the test function completes. Otherwise the result is correct. - it.skip("Inner components' koDescendantsComplete occurs before the outer component's", function () { + it("Inner components' koDescendantsComplete occurs before the outer component's", function () { after(function () { ko.components.unregister('sub-component') }) @@ -331,7 +329,7 @@ describe('Components: Component binding', function () { expect(renderedComponents).to.deep.equal(['sub-component1', 'sub-component2', 'test-component']) }) - it.skip('koDescendantsComplete occurs after all inner components even if outer component is rendered synchronously', function () { + it('koDescendantsComplete occurs after all inner components even if outer component is rendered synchronously', function () { after(function () { ko.components.unregister('sub-component') }) @@ -400,7 +398,7 @@ describe('Components: Component binding', function () { expect(renderedComponents).to.deep.equal(['sub-component1', 'sub-component2', 'test-component']) }) - it.skip('koDescendantsComplete waits for inner component to complete even if it is several layers down', function () { + it('koDescendantsComplete waits for inner component to complete even if it is several layers down', function () { after(function () { ko.components.unregister('sub-component') }) @@ -431,7 +429,7 @@ describe('Components: Component binding', function () { expect(renderedComponents).to.deep.equal(['sub-component1', 'test-component']) }) - it.skip('koDescendantsComplete waits for inner components that are not yet loaded', function () { + it('koDescendantsComplete waits for inner components that are not yet loaded', function () { restoreAfter(window, 'require') after(function () { ko.components.unregister('sub-component') @@ -997,7 +995,7 @@ describe('Components: Component binding', function () { expect(callbacks).to.deep.equal(1) }) - it.skip("Does not call outer component's koDescendantsComplete function if an inner component is re-rendered", function () { + it("Does not call outer component's koDescendantsComplete function if an inner component is re-rendered", function () { after(function () { ko.components.unregister('sub-component') }) diff --git a/packages/bind/src/applyBindings.ts b/packages/bind/src/applyBindings.ts index f469eb015..011e2d239 100644 --- a/packages/bind/src/applyBindings.ts +++ b/packages/bind/src/applyBindings.ts @@ -98,34 +98,35 @@ function applyBindingsToDescendantsInternal( ) { let nextInQueue: ChildNode | null = virtualElements.firstChild(elementOrVirtualElement) - if (!nextInQueue) { - return - } + if (nextInQueue) { + let currentChild: Node | null + const provider = getBindingProvider() + const preprocessNode = provider.preprocessNode + + // Preprocessing allows a binding provider to mutate a node before bindings are applied to it. For example it's + // possible to insert new siblings after it, and/or replace the node with a different one. This can be used to + // implement custom binding syntaxes, such as {{ value }} for string interpolation, or custom element types that + // trigger insertion of