3

In my Knockout.js templates, it would be convenient if I could access properties of an object on the view model:

<span data-bind="text: account.shortName"></span>

This doesn't work. The element is blank. However, I can do something like this:

<div data-bind="with: account">
  <span data-bind="text: shortName"></span>
</div>

Is there any way around this? Must I use with everywhere, and the excessive elements as well?

Brad
  • 159,648
  • 54
  • 349
  • 530
  • Does the `account` object have to be observable? If it must be observable, then you _must_ use the `with` binding or access through a computed observable. – Jeff Mercado Jul 25 '15 at 01:48
  • @JeffMercado Interesting. Yes, in my case it has to be observable. Thanks for the info. Can you post that as an answer so I can accept it? – Brad Jul 25 '15 at 01:53
  • 1
    If `account` is an observable, try `text: account().shortName` – JohnnyHK Jul 25 '15 at 01:53
  • @JohnnyHK: Doing that can lead to all sorts of problems, especially if `shortName` is observable. – Jeff Mercado Jul 25 '15 at 01:54
  • @JohnnyHK That doesn't work. "Unable to process binding.... Cannot read property 'shortName' of undefined" I assume that's because at some point, account is undefined in my application. – Brad Jul 25 '15 at 01:55
  • @Brad Yep; that's the nice thing about using `with` (or a computed that checks for null) instead. – JohnnyHK Jul 25 '15 at 01:56
  • account() && account().shortName will do the trick – miellaby Aug 04 '15 at 13:29

2 Answers2

4

If the account is observable, then you really should use the with binding like you have it already or, use a computed observable to access the property. Sure, it is a bit verbose, but it must be done.

Using expressions like someObservable().someProperty will only lead to headaches and confusion and should be avoided. e.g., If you did use this and someProperty happened to be observable, you may notice that something's not right when someObservable changes. The binding will not be updated to use the someProperty of the new value and I hope you can see why.

You can make creating the computed observable in a safe manner easier by creating a function to do so.

ko.observable.fn.property = function (name) {
    return ko.computed({
        read: function () {
            var parentValue = this();
            if (parentValue)
                return ko.utils.unwrapObservable(parentValue[name]);
        },
        write: function (value) {
            var parentValue = this(), property;
            if (parentValue) {
                property = parentValue[name];
                if (ko.isWriteableObservable(property))
                    property(value);
            }
        },
        owner: this
    });
};

Then you could use this in your bindings:

<span data-bind="text: account.property('shortName')"></span>
Jeff Mercado
  • 129,526
  • 32
  • 251
  • 272
  • Are you sure? someObservable().someProperty works very well. If I remember well, I've done it a plenty of time. ko is clever enough to track both the change of the current object reference and the observable within the current reference. So if one completely changes someObservable to another object, the binding _will_ be updated to the sub-observable of the the new object. – miellaby Aug 04 '15 at 13:34
  • Nowadays, it works out well as I understand it. Either in previous versions of knockout or a very specific case I was looking at, I got burned because knockout didn't seem to rebind inputs. So for a long time, I tried to keep it safe and always recommended this approach. My stance on that has changed. – Jeff Mercado Aug 04 '15 at 22:31
  • I'm not an expert in stack overflow, but can't you edit the answer to correct (strike out) this false assessment? Is it impossible once the answer has been accepted? – miellaby Aug 05 '15 at 08:51
2

Jeff's answer is good, but you can also do this by just changing the binding to:

text: account() && account().shortName

I've found that contrary to what Jeff mentions in his answer, this works even if shortName is an observable as bindings are implemented inside of a computed observable. In other words, the text binding's value is implemented as a sort of anonymous computed.

Here's a snippet showing that it works, starting with an account observable without a value, and then updating things over time.

var vm = {
  account: ko.observable()
};

$(function() {
  ko.applyBindings(vm);

  // After a second, set account
  setTimeout(function() {
    var account = {
      shortName: ko.observable('Initial account')
    };
    vm.account(account);

    var newAccount = {
      shortName: ko.observable('New account')
    }

    // After another second, change account
    setTimeout(function() {
      vm.account(newAccount);

      // After another second, change shortName within the new account
      setTimeout(function() {
        newAccount.shortName('New shortName value in the new account');
      }, 1000);
    }, 1000);
  }, 1000);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<body>
  <h1>Account Binding Test</h1>
  <span data-bind="text: account() && account().shortName"></span>
</body>
Community
  • 1
  • 1
JohnnyHK
  • 305,182
  • 66
  • 621
  • 471
  • 1
    I think a lot of what I'm saying is due to being bit by it in earlier versions of knockout. It doesn't look like it's an issue with current versions now. From what I observed in the past, if an expression evaluated to an observable, the binding wouldn't change when a parent observable changed, the dom element would stay bound to that observable. It wasn't calculated as a computed. Maybe the example I was looking at was a lot more complicated and was caused by other issues. – Jeff Mercado Jul 25 '15 at 19:49