Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document Descendants and Children Complete #65

Closed
brianmhunt opened this issue May 24, 2018 · 17 comments
Closed

Document Descendants and Children Complete #65

brianmhunt opened this issue May 24, 2018 · 17 comments

Comments

@brianmhunt
Copy link
Member

brianmhunt commented May 24, 2018

@mbest - I've been trying to implement the DescendantsComplete and ChildrenComplete events (i.e. pass all the unit tests), and it's been taking a bit longer than it should because TKO does a few internals differently now and I haven't fully figured out how these events should work.

It would help me a ton if you were able to put together a bit of documentation (either formally or informally here) that relayed how they act in the various circumstances. It's clearly well thought out, but because the TKO internals are different the don't align exactly.

A high level description would go a long way, but some code-comments on the AsyncCompleteContext (why we need it, how it differs from a regular binding context) would go a long way and be much appreciated!

@mbest
Copy link
Member

mbest commented May 25, 2018

childrenComplete is triggered as soon as bindings are completed on the node's children. Completed means that applyBindings was called and returned for each node. It's always triggered synchronously from the point of view of the code that applies bindings to the child nodes. It will be triggered again if the node's children are re-bound (usually after being cleared and re-rendered). It may be triggered even if the node has no children, although in that case, the event won't be pushed to callbacks set in the node's binding. It's also triggered by the foreach binding each time it's updated even though not all children nodes are re-bound.

descendantsComplete is triggered when all the node's descendant nodes are completed (they've all triggered the childrenComplete event). If any of the descendant nodes indicates that it will complete its children asynchronously, the event will be delayed until that node's children are completed. In the case where all descendants are bound and completed synchronously, it will be triggered synchronously.

@brianmhunt
Copy link
Member Author

Thanks Michael. Should descendantsComplete trigger repeatedly e.g. with foreach changes?

@mbest
Copy link
Member

mbest commented May 25, 2018

Should descendantsComplete trigger repeatedly e.g. with foreach changes?

I think it will since it's triggered by the childrenComplete event. I just noticed that I don't have any tests for these events and foreach. I'll have to work on that.

@brianmhunt
Copy link
Member Author

Got it, thanks.

Relatedly, in tko the applyBindings returns an instance of a class called BindingResult. I'm mulling what it should return, but in essence I'm hoping it can expose the binding results, events, and errors in a way that helps debugging and makes KO easier to implement in certain use-cases. Any thoughts on it?

@mbest
Copy link
Member

mbest commented May 25, 2018

Here's more on how the nodes need to interact to trigger a descendantsComplete event:

  1. A node that will complete asynchronously needs to mark itself as such; later it will need to clear that mark when it's complete.
  2. A marked node is considered complete if it's removed or cleaned (the mark is cleared).
  3. A node that will trigger a descendantsComplete event needs to wait for all descendant nodes to be complete. It can trigger the event once it has applied bindings to all of its child nodes and it has no descendant nodes that are marked as completing asynchronously.
  4. The event should be triggered as soon as possible (preferably synchronously) when all descendants are complete.

@mbest
Copy link
Member

mbest commented May 25, 2018

BindingResult is a good idea and I think you've got the essential stuff in there now.

@brianmhunt
Copy link
Member Author

Thanks Michael, that's very helpful. Glad you like the BindingResult idea.

@mbest
Copy link
Member

mbest commented May 25, 2018

The way I originally implemented the descendantsComplete logic went like this:

  1. A node that will complete asynchronously marked itself as such by setting a domData value that included a way for ancestor nodes to register as waiting for it.
  2. When the node was complete (descendantsComplete itself), it signaled to each of the waiting nodes that it was complete.
  3. A node with a registered descendantsComplete callback or that had been marked as above would, after the childrenComplete event, iterate through all of its descendant nodes and register to wait for any marked ones.

I didn't like the fact that this method required iterating through all of the descendant nodes, but I also wanted to avoid having all nodes keep track of such things. So that led me to create AsyncCompleteContext as a way for the descendant nodes to register themselves with their closest "interested" ancestor node.

@brianmhunt
Copy link
Member Author

I'll mark this as closed; thanks for the excellent comments @mbest, I think I've got it nailed.

TKO is down to 9 failing tests. 🍻 !

@mbest
Copy link
Member

mbest commented May 26, 2018

@brianmhunt Do you think you could use bindingResult.completionPromise to handle the above in TKO instead of AsyncCompleteContext?

@brianmhunt
Copy link
Member Author

@mbest I was planning that and that's how I had initially been digging into it, but it seemed to fail on some edge cases in the tests, mostly because completionPromise triggers only once and apparently I hadn't sorted the logic of when it should trigger/resolve. Which is why I started this thread. 😄

Generally speaking, for the binding process (i.e. applyBindings*) I'd prefer of returning promises since it's cleaner and more readily apparent what's going on, compared to events, and I suspect there'd be a big performance advantage since with the promises the events would not need to be triggered for every binding application.

Did you have something in mind? I didn't want to muddle with how Knockout is designed/working, but if you can help me get completionPromise working here, and update Knockout & tests accordingly, I think that'd be valuable.

@mbest
Copy link
Member

mbest commented May 27, 2018

Okay. I'll take a look when I have some time.

@brianmhunt
Copy link
Member Author

@mbest One problem I'm having is that descendantsComplete is called only when an if condition becomes truthy (i.e. the tests Should call a descendantsComplete callback function and following in ifBehaviors).

What's the reason for deferring descendantsComplete until the conditional is truthy?

I'm not certain it's what users will expect, and it's not entirely consistent with application elsewhere (e.g. should for-loops wait until there's one item?) . However, I might not fully appreciate the rationale here. Grateful for your thoughts.

brianmhunt added a commit that referenced this issue Jun 14, 2018
@mbest
Copy link
Member

mbest commented Jun 15, 2018

That's a good question. This came from the discussion in knockout/knockout#2342

I had forgotten about the treatment of the if binding while describing descendantsComplete above. The if binding (and ifnot and with) only trigger a childrenComplete when their values are truthy. Normally, this won't delay an ancestor's descendantsComplete because it doesn't actually check each descendant node. It only waits for descendants that are marked as such. There are two ways that a node is marked as possibly completing asynchronously. The first is if the control-flow binding sets the mark (by calling startPossiblyAsyncContentBinding, currently only component does this), and the other is if the node has a descendantsComplete binding (which calls startPossiblyAsyncContentBinding).

brianmhunt added a commit that referenced this issue Jun 15, 2018
brianmhunt added a commit that referenced this issue Jun 15, 2018
* yarn) Update the packages so rollup can be used in each package directory

* tko) Fix export of `when`

* knockout) Add knockout proper package, plus somewhat modified spec from 3.5

See packages/knockout/spec/README.md

* Fix `jasmine.Clock.useMockForTasks`

* Expand tests to all `spec/` subdirectories

Also allow package.json’s `karma.files` to overload the default (by e.g. making them watchable)

* Fix semicolon hoisting variables to global scope

* spec) Remove duplicate import

* Upgrade devDependencies to latest

* karma) Fix relative `__dirname` import

* rollup) Better re-use of the path replacement

* observableArray - fix `compareArrays` tests

* npm) Update to latest packages, add globals, use .es6 knockout for testing

If we use `dist/knockout.js` for testing the knockout package, then we have to recompile every single dependency.  If we attempt to link directly to the files `src/index.js` as we do for the .es6 variants then Typescript does not inject its tslib dependencies.

So we test off of `dist/knockout.es6.js`.  One still has to recompile knockout to re-test every change.

* expose `ko.selectExtensions`

* knockout/templating) Fix a variety of template-related tests

* Down to 76 failing tests; fixes for templating and expressionRewriting behaviors

* Fix tests with dependencyDetection, postJson, and $context.ko (62 fail)

See `testing.md` for details

* tko.utils.functionRewrite) Add a basic util for backwards compat with `funciton () {}` in binding strings

* Add Backers.md for Pateron support

* parser) Add basic function rewriter to the default parser

Adds option `bindingStringPreparsers`

* build) Respect a `—es6` option to `yarn build` to speed up compilation

* string) Remove legacy `stringifyJson`

* #56 ) Break out common elements of knockout/tko compilation to `tko.builder`

This may have caused a couple regressions in tests, but they should be easy to find.

* packages) Fix configs for tko.functionRewrite and tko.builder

* rollup) Fix renamed config option

* export `utils.cloneNodes`

* Support ES2015 object initializer short-hand `{name}`

* function-rewrite) Rewrite arguments passed to lambdas

Note: arguments are not yet respected by the parser/interpreter.

* knockout) Put the `options in the correct place

* dev) Ignore `.vscode`

* parser) Add tests for calling lambdas created by the parser

Some lambdas may be passed into bindings.

* Add monkeypatch to fix the broken test system by breaking it

Time consuming & frustrating to diagnose.

* knockout) Fix tests related to `task` `scheduler` and `onError`

* Expose `ko.computedContext` as alias of `ko.dependencyDetection`

* Fix test for .length

* Update npm dependencies to latest

* MultiProvider) Respect the antiquated mechanism for providing `preprocessNode`

There’s certainly code out there that performs a `ko.bindingProvider.instance.preprocessNode = …`

* ko 3.5) Add `childrenComplete`

Cross-link knockout/knockout#2310

* Add `expressionRewriting` to `ko` global for backwards compatibility

* Make `ko.getBindingHandler` an overloadable function

* Prevent infinite recursion with `MultiProvider.instance`

* ci) Fix test wrt tko.bind

* dom/data) Add `getOrSet` function for dom data

* Make the async/component binding completion more explicit

* Make sure the MultiProvider returns an overloaded instances’ new nodes

* Implement details from knockout/knockout#2319

Probably we’ll be ripping all this out since it duplicates what’s in TKO already in terms of async binding handlers (but implementation details aside, this should still pass the KO 3.5 tests)

* Foreach parity re. knockout/knockout#2324

* knockout/knockout#2380 - ko.contextFor after applyBindingAccessorsToNode

* knockout/knockout#2378 Add tests and comply with spec

* knockout/knockout#2379 Fix style binding / compare to prior value

* Fix `arrayFirst` returning `undefined` when found element is falsy

Fixes #63

* Fix `ko.when` for promises & callbacks

* Respect the `createChildContextWithAs` option in the `with` binding

* Respect `createChildContextWithAs` in the native `foreach` binding

* Fix export of extenders

* Fix html parsing regex

* tko.utils) Fix circular reference

* knockout/spec) Update to `master` in KO

* knockout/knockout#2341 Respect `cleanExternalData` overload

@mbest - what do you think of the `addCleaner` and `removeCleaner` API?  Or is that just overkill?

* Binding Provider) Better support for legacy `ko.bindingProvider.instance = …`

* template foreach) Fix the ko.bindingProvider being broke after if this test fails

* Add BindingResult class that exposes binding state

@mbest what do you think of this API?

Do you think `BindingResult` should include the node bound and binding context?  That would be useful for debugging, and open up other useful possibilities - but might be outside the clean, core API.

* apply bindings) Add `rootNode` and `bindingContext` to the BindingResult

* Fix missing comma

* optionsBehaviors) Modify the tests to support lack of `function` support

* applyBindings) call `descendantsComplete` as expected

* tko.bind) Add a special BindingHandler `DescendantBindingHandler`

This should be the superclass of every class that bindings descendants, e.g. `if`, `with`, `foreach`, `template`.

* tko.bind) Add async binding context

This patch should bring parity with #2364, subject to the individual bindings (to be a separate commit)

* tko.bind) Fix error w/ virtual element binding

* tko.bind) Make sure `extend` doesn’t call `valueAccessor(value)`

In tko, `valueAccessor` with a parameter sets the value of the underlying data/observable.  So we pass the value needed for extending an async context by binding the function’s `this`.

* Fix the builder failing to respect `ko.getBindingHandler` setter/getter

Also fix the upstream fix for a custom element tests misssing a `tick` and `template`.

* tko.bind/completion events) Use promises to verify that descendants completed binding

* backers) Update

* utils) Deprecate `proto` utilities, remove circular dependency

1. Fix the subscribable -> dependencyDetection -> … circular dependency
2. Always use `Object.setPrototypeOf`; this introduces a polyfill requirement/dependency for IE9.

* domNodeDisposal) Fix node cleaning test

Updated since merges from master.

* foreach) Fix `$ctx.$index` being overwritten on each update

It’s unusual & unexpected that `_contextExtensions` is being called multiple times; this may be a regression that needs investigation.

* tko.bind) Fix tests re. virtual element error

* tko.bind) Fix circular dependency for `BindingHandler` / `applyBindings`

* tko.bind) Reverting to `descendantsComplete` based on binding promises

@mbest - Note the failing tests and cross-linking #69 and #65.

* binding.template) Update double-unwrap test; disable “destroy” test

Note #69 re. foreachBehaviors.js:131

* util.parser) Skip argument exposure tests in lambdas

Noting for #65.
@mbest
Copy link
Member

mbest commented Jun 26, 2018

I'm going to revisit how it works with if. I'm thinking of adding a binding option named something like ifDelayedComplete, which if true will cause the if binding to delay notifying the childrenComplete and descendantComplete bindings until its value is true. Similar logic would apply to ifnot and with. @brianmhunt What do you think?

@brianmhunt
Copy link
Member Author

I think that's a good idea @mbest. Perhaps completeOn: "render" or something like that.

@mbest
Copy link
Member

mbest commented Jun 27, 2018

I like that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants