-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
support observable view models at both the root and child context level #485
Conversation
This would also benefit my repeat binding, allowing it to use a child context for each item and still update the bindings when the array is updated. Right now, I'm just using a special context variable to set up the dependency. |
Here's a little history on this topic: I first noticed that the binding system would handle an observable view model when working on my first iteration of independent bindings. At that time, I wasn't sure what to do with an observable view model. Since there wasn't an easy way to support it with independent bindings, I basically disabled it--I still unwrapped it, but wouldn't update any bindings if it was updated. When I started working on my second iteration of independent bindings, I was ready to just drop support for observable view models. But then I decided to see if people might actually like the feature. So I posted a message about it on the forums. Since I got only positive feedback, I decided it was important enough to keep supporting. The "feature" was originally added to Knockout with this commit, but it isn't mentioned in the commit notes. Nor is there a spec to test it. So at this point, it's unsupported (and maybe accidental?). In this issue, I propose that we make it officially supported and expand its scope to cover both the root and child context levels. |
+1 |
This requires support in Knockout for observable child contexts (knockout/knockout#485), which is currently only available in my Knockout smart-binding fork (https://github.com/mbest/knockout)
+1 |
The current observable view model system has a problem because the Demonstration: http://jsfiddle.net/mbest/gdQFK/ We can fix part of this by making sure that the |
…xt" instead of a "viewModelOrBindingContext". Move the logic to conditionally create a binding context from applyBindingsToNodeInternal to each of the external applyBindings* functions. Note that this breaks the current implementation of observable view models, but that's something we can fix later (see #485). Make the "shouldBindDescendants" return value of applyBindingsToNodeInternal exported so external plugins can access it when calling ko.applyBindingsToNode.
…xt" instead of a "viewModelOrBindingContext". Move the logic to conditionally create a binding context from applyBindingsToNodeInternal to each of the external applyBindings* functions. Note that this breaks the current implementation of observable view models, but that's something we can fix later (see #485). Make the "shouldBindDescendants" return value of applyBindingsToNodeInternal exported so external plugins can access it when calling ko.applyBindingsToNode.
The current (2.x) implementation of observable view models work like this: The view model is unwrapped by the binding processor within the This system only works when the binding context can be created within the binding processor, in other words, for a top-level view model. Child observable view models (using Thus the behavior of root-level and child-level observable view models is different:
|
To officially support observable view models (in version 3.0), I think we should make sure support is consistent between the child and root levels. Of course, this gives us two options:
In my Knockout fork, I chose to implement option 2. I have ko.bindingHandlers['withlight'] = {
'flags': bindingFlags_contentBind | bindingFlags_canUseVirtual,
'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var innerContext = bindingContext['createChildContext'](function() {
return ko.utils.unwrapObservable(valueAccessor());
});
ko.applyBindingsToDescendants(innerContext, element);
}
}; |
Can you help me clarify exactly what change of behavior we should expect to see if we go for option 2? I'm unsure if this would be a further breaking change, but it sounds like it might be. My initial concern is with scenarios like the following: http://jsfiddle.net/ep27E/ In this scenario, the developer is benefitting from (and relying on) the existing behavior where the DOM is rebuilt. Would option 2 mean that old validation messages continue to appear even after the viewmodel is changed? If so, option 1 sounds much more promising, as not only is it easier to implement, but also the behavior is less breaking, since top-level observable viewmodels are far less common and haven't had such a clearly defined behavior in the past. |
Going with option 2 wouldn't necessitate changing the I tried your example, but I was not able to test the scenario you described. It always cleared the message as soon as I clicked on the drop-down. |
Perhaps we could allow developers to install a callback that gets run when the view model is about to change. There, they could tear down any modifications that were made outside of Knockout, such as in @SteveSanderson's jquery.validate example, or in the common case of apps that use jquery.draggable, jquery.droppable, jquery.resizable, etc.
|
I decided to put together the code changes for both options. Implementing option 1 (9a086db) was pretty straightforward and simple, and for option 2 (7ff1358), I was able to quickly adjust the code I already had in my fork. Both versions add the same three tests, but they have slight differences. The latter two tests for option 1 pass plain values to |
I definitely want to see something like my option 2 included in Knockout. There's a lot of demand for using control-flow bindings such as Having been thinking about this over the last week, I want to include both options, with option 1 being the default behavior when providing an observable view model to |
Code attached. |
Thanks for proposing this implementation! I expect that we will use most of this, but I'd like to suggest refining the design a bit further. I've been thinking this through over and over, because it feels like whichever option we take here, there are potential pitfalls and drawbacks. And taking no action isn't an option either :) Both options 1 and 2 have issues:
After some consideration, my preference would be to rank the design objectives as follows:
Apologies that this is such a big dump of thoughts! It's a tricky design issue. Michael, if you want to proceed with this approach, I think the considerable majority of your existing pull request code will be applicable, and we can just tweak the API surface a bit. I'm happy to have a go at implementing this, or I'm happy if you want to, but I'll wait for your views on this first. |
Steve, you're right that there are no easy answers here. I suggested making option 1 the default because it's a bit easier to support officially, as it works correctly with what we've supported for Knockout in the past. With this approach, any binding that works under I would prefer we move towards option 2, though, so I'm happy to see you support that as well. The biggest challenge of this approach is that certain binding handler interfaces and approaches are incompatible with it.
If we are to make observable view models a real official thing, we need to solve the above problems by changing the appropriate interfaces and/or documentation.
|
Thanks for the additional info. This is not an easy one. How far off do you think we are from being able to recreate the 2.x behavior in the 3.0 code? To avoid pushing 3.0 back further and introduction potential extra disincentives for people to upgrade, it would be great if we could re-establish at least the top-level observable view model behavior, even if it's not officially documented and supported. Then we can look at a bigger change to the binding interface in the future. In the future, I'd love us to review the whole custom bindings API entirely, and possibly produce an alternative that is a bit more object-oriented (so each usage is regular instance that can hold its own data, and you can just inherit from another binding and have it work as you expect) and get rid of the cruft around semi-obsolete parameters like What do you think? |
That would be |
The code was very close to ready. I removed the special-case code for top-level observable view models and made it so that the observable is instead passed directly to
I replaced the attached branch so that we have a cleaner history. The previous code is at 485-observable-view-model-combined. |
Makes sense and sounds right. Thanks for supplying the new version of the code as a clean diff on top of the existing v3 branch. I'm in the process of reading through it to understand the new mechanism, and hopefully will merge this soon. |
Thanks again for providing this! I've been trying to understand the bindings updating mechanism, but to be honest I'm finding it super hard to read, because there are lots of moving parts with generic names like I'm sure the actual logic and functionality is great, and no doubt I could eventually work out what each part of it does and what the design tradeoffs were, but it's not hugely efficient for me and all future readers of the KO source to each do that, when the info is already in your head :) Do you think you'd be able to add brief comments to each of the new bits of the mechanism that you've added, and where applicable give the new things more explicit (and possibly longer) names that clarify when and how they make a difference? Again, this is not a criticism of the implementation at all - just a note to try to ensure that KO's binding internals remain clear and explicit enough that a newcomer could read the implementation and feel confident that they've understood the intent! |
…ves the need to change the binding provider.
…x is up-to-date when using observable view models. Try to avoid double curly brace in binding string (messes up jQuery-tmpl). Remove support for temporary binding context when extending a child context. Instead, use new extendCallback parameter.
I took your advice and added a bunch of comments for the new code. After going through that process, I decided to remove the changes from bindingProvider.js and keep all of the dependency logic within bindingAttributeSyntax.js. Then I went for a walk and realized that it needs to be easier to add custom properties, such as So I hope that all makes sense. I expect you'll have questions and comments as you continue to go through the code. I didn't rename any variables because I don't think I'm very good at make those long names, but feel free to do so as you see fit. |
That's always one of the tricky tasks of an observer-based system, but hopefully in this case, we can make it as obvious as possible. The 2.x system for observable view models had one computed observable that would update the binding context (by creating a new instance), get the bindings, and update the binding handlers. In 3.x we had already separated those steps, with a computed observable only used for updating the bindings handlers. To support observable view models in 3.x, we need to re-enable updating the binding context and bindings. My implementation uses two new computed observables for that, giving a hierarchy of three computed observables:
Each level is dependent on the one before it, with the binding context dependent on the view model (and/or a parent context). So the dependency tree would look something like this (arrows show the direction of updates): |
…ectly set when the binding context is updated.
Thanks very much for the additional comments - those are hugely useful. And thanks for the excellent diagram too! Having been over this a few times now, I'm reasonably satisfied that I can make sense of the changes and the overall approach you've taken. I would admit I still feel some uncertainty about the tradeoff we're taking here - the significant complications to the binding system (e.g., the three layers of nested computables) could make maintenance and extension more complex or limited in the future, or be a potential source of bugs. This extra machinery and observably changing viewmodels also increases the burden on any developer who wants to write custom extensions to the binding system, or unusually sophisticated custom bindings. It's not completely clear that the benefit of this functionality, which relatively few KO developers have asked for, warrants it. Having said all that, I really do appreciate that you've made every effort to review and refactor the implementation multiple times in response to feedback, answered all questions, and have documented the code well and provided a good set of specs. You've distilled the implementation down as far as the design allows. I'm happy to trust your judgement on this, so if you feel satisfied that it doesn't risk significant performance or memory costs, is not likely to be a major source of edge-case bugs, and you judge that the value to developers outweighs any ongoing maintenance costs, then please go ahead and merge this to Thanks again for your flexibility and patience in reviewing and modifying this change with me! |
support observable view models at both the root and child context level
Thanks again, Steve. I've merged this code in now. |
This is a major part of #321, but can be implemented separately. Depends on #483 and #484.