How can I use knockout's $parent/$root pseudovariables from inside a .computed() observable?
Inside a knockout.js binding expression, I can use the $data
, $parent
, and $root
pseudovariables. How can I get the equivalent of those pseudovariables when I'm using a ko.computed
observable declared in JavaScript?
I've got a parent viewmodel with a collection of children, and the parent viewmodel has a selectedChild
observable. Given that, I can use databinding expressions to add a CSS class to whichever child is currently selected:
<ul data-bind="foreach: children">
<li data-bind="text: name,
css: {selected: $data === $root.selectedChild()},
click: $root.selectChild"></li>
</ul>
<script>
vm = {
selectedChild: ko.observable(),
children: [{name: 'Bob'}, {name: 'Ned'}],
selectChild: function(child) { vm.selectedChild(child); }
};
ko.applyBindings(vm);
</script>
But my viewmodels are going to get more complex, and I'd like "am I selected?" to be able to do more than just adding a single CSS class to a single element. I really want to make an isSelected
computed property on the child viewmodel, so I can then add other computed properties that depend on it.
I've tried just writing JavaScript that refers to $data
and $root
, on the off-chance that knockout might define those variables and somehow have them be in scope when it calls my computed
evaluator function:
{
name: 'Bob',
isSelected: ko.computed(function(){ return $data === $root.selectedChild(); })
}
But no such luck: inside my evaluator function
, both $data
and $root
are undefined
.
I've also tried using ko.contextFor
inside my evaluator, since it does give access to $data
and $root
. Unfortunately, inside my evaluator function, contextFor
also always returns undefined
. (I didn't hold out high hopes for this strategy anyway -- it's not clear how well knockout would be able to track the dependencies if I had to go behind its back like this.)
I could always manually set a property on each child viewmodel that refers back to the parent viewmodel. But I know that knockout has the ability to do this for me, and I'd like to at least explore whether I can use its mechanisms before I go writing my own.
It seems like it should be possible to translate the above binding expression to a computed observable -- after all, that's what knockout already does:
The other neat trick is that declarative bindings are simply implemented as computed observables.
But how do I go about dealing with the $data
and $root
pseudovariables when I'm writing my own computed observable?
Solution 1:
The pseudovariables are only available in the context of data binding. The view model itself ideally should not know about or have any dependencies on the view that is displaying it.
So, when adding computed observables in the view model, you have no knowledge of how it will be bound (like what is going to be $root). A view model or part of a view model could even be bound separately to multiple areas of the page at different levels, so the pseudo-variables would be different depending on the element that you are starting with.
It depends on what you are trying to accomplish, but if you want your child to have an isSelected
computed observable that indicates whether this item is the same as the selected item on the parent view model, then you will need to find a way to make the parent available to the child.
One option is to pass the parent into the constructor function of your child. You do not even need to add the pointer to the parent as a property of the child and can just use it in your computed observable directly.
Something like:
var Item = function(name, parent) {
this.name = ko.observable(name);
this.isSelected = ko.computed(function() {
return this === parent.selectedItem();
}, this);
};
var ViewModel = function() {
this.selectedItem = ko.observable();
this.items = ko.observableArray([
new Item("one", this),
new Item("two", this),
new Item("three", this)
]);
};
Sample here: http://jsfiddle.net/rniemeyer/BuH7N/
If all you care about is the selected status, then you can tweak it to pass a reference to the selectedItem
observable to the child constructor like: http://jsfiddle.net/rniemeyer/R5MtC/
If your parent view model is stored in a global variable, then you could consider not passing it to the child and using it directly like: http://jsfiddle.net/rniemeyer/3drUL/. I prefer to pass the reference to the child though.
Solution 2:
In my experience the approach in @RP Niemeyer's answer is fine if Item
s live for the duration of the application. But if not, it can lead to memory leaks, because Item
's computed observable sets up a reverse dependency from the ViewModel
. Again, that's ok if you never get rid of any Item
objects. But if you do try to get rid of Item
s they won't get garbage collected because knockout will still have that reverse dependency reference.
You could make sure to dispose() of the computed, maybe in a cleanup() method on Item
that gets called when the item goes away, but you have to remember to do that whenever removing Item
s.
Instead, why not make Item
a little less smart and have ViewModel
tell it when it is selected? Just make Item
's isSelected()
a regular old observable and then in ViewModel
subscribe to selectedItem
and update inside that subscription.
Or, use @RP Niemeyer's pub/sub solution. (To be fair, this solution came about after his answer here.) You'll still need to clean up, though, because it creates reverse dependencies, too. But at least there's less coupling.
See the answer to my recent question on this same topic for more details.