I very often use the if
binding in knockout to hide something, with the added bonus that I don't need to worry about null reference errors inside the if
. In this example if address()
is null then the whole block is removed so you avoid having to deal with null checking for every property. This would not be the case had I used the visible
binding.
<div data-bind="if: address()">
You live at:
<p data-bind="text: address().street.toUpperCase()"></p>
</div>
This is the simplest case above - and yes I would generally use this pattern with the <!-- ko -->
comment syntax.
What is actually causing me problems is when I use a more complex computed
value and enable the ko.options.deferUpdates
option :
<div data-bind="if: hasAddress()">
You live at:
<p data-bind="text: address().street.toUpperCase()"></p>
</div>
The simplest implementation of this computed
observable might be something like this :
this.hasAddress = ko.computed(function () { return _this.address() != null; });
This all works great until I do the following:
1) set ko.options.deferUpdates = true
before creating the observables.
2) address()
will start off as null and everything is fine
3) set address()
to { street: '123 My Street' }
. Again everything works fine.
4) reset address()
to null. I get a null error because address().street
is
null :-(
Here is a fiddle to illustrate the problem : https://jsfiddle.net/g5gvfb7x/2/
It seems that unfortunately due to the order in which the micro-tasks runs it tries to recalculate the text
binding before the if
binding and so you still get a null error that normally wouldn't occur.
I'm a little scared about this since I use this pattern a lot :-(
When using deferUpdates
, Knockout internally uses a dirty
event to notify all computed observables of a change in their dependencies and to schedule updates, which happens in depth-first order. The problem here occurs because bindings ignore the dirty
event and wait for a change
event, which will happen in a breadth-first order.
The fix has to happen in Knockout to make bindings respond to the dirty
events. This has already been checked in for the upcoming version (3.5.0): https://github.com/knockout/knockout/issues/2226
Good news - if you have turned on ko.options.deferUpdates
and not even realized you have broken your app then the chances are your user won't see an error because errors from micro tasks are raised on ko.onError
or window.onerror
and the UI appears to recover. But if like me you're logging these errors then it's bad.
Good news - if you can identify which observables you use in an if
binding you can do the following and turn deferUpdates off and back on:
// turn deferUpdates off and back on again
var deferUpdatesState = ko.options.deferUpdates;
ko.options.deferUpdates = false;
this.hasAddress = ko.computed(function () { return _this.address() != null; });
ko.options.deferUpdates = deferUpdatesState;
This can be made into a helper function. There could be more complex issues if the hasAddress
computed has more complicated dependencies. But for the simpler case like this it's fine.
Bad news - you unfortunately can't do the following:
ko.computed(function () { return _this.address() != null; }).extend({ deferred: false });
This is just due to the way that deferUpdates works internally (from looking at source).
Still wondering if there is a better solution than this helper function, or a way to rewrite the if
binding to maybe do something clever in the binding to cover this case.
Edit: Thanks to @Tomalak for mentioning with
binding. I don't think it is the complete solution I'm looking for, but it can certainly be used in conjunction with any existing if
bindings (that have complex rules) like this. Might be the safest fix if trying to go through an existing app.
<div>
<div data-bind="if: hasAddress">
You live at:
<!-- ko with: address() -->
<p data-bind="text: street.toUpperCase()"></p>
<!-- /ko -->
</div>
<div data-bind="if: !hasAddress()">
Sorry! I don't know where you live!
</div>
</div>