stackh34p
stackh34p

Reputation: 9009

Issues with Knockout nested bindings updating each other, but not the dom

I have a composite view model, with other view models inside, that introduce nested bindings. It is thus possible one nested sub-model to alter another sub-model's observable, or one owned by its parent.

An example is available on the following JsFiddle

There, I am modifying an observable object (itemContainer) that itself exposes an observable array property (items). I want to be able to change the bindings each time I modify the items property, or the itemContainer itself. However, both seem not to have any effect on the dom, while in reality the bindings occur as expected - I am modifying a span with jQuery to assert the actual count of elements after my operations (see "Actual count" value).

So, I have made another JsFiddle with the main difference being that itemContainer is no longer an observable property. This time it works almost as I expect to.

However, I need to be able to have the dom automatically update whenever I alter either the itemContainer or its items property. How should I go about this?

Also, in both cases, the viewModel.actions.count computed member's binding is not re-evaluated. I'd appreciate any insights on why it does not pick the changes.

Upvotes: 1

Views: 673

Answers (2)

Matthew James Davis
Matthew James Davis

Reputation: 12295

Updated working fiddle here: http://jsfiddle.net/hh233r3q/1/

The problem was in your template. You bind contents.itemContainer.items.

<!--ko foreach: contents.itemContainer.items -->

However, itemContainer is initialized to null, and items is not a property of null.

itemContainer: ko.observable(null)

Instead, you can use the ko with binding.

<!-- ko with: contents.itemContainer -->
  <!-- ko foreach: items -->

The ko with binding automatically unwraps itemContainer and uses the template within if it is defined. See here: http://knockoutjs.com/documentation/with-binding.html

Also, note that even if itemContainer was defined, you would still need to invoke the observable to get the underlying object.

<!--ko foreach: contents.itemContainer().items -->

Upvotes: 3

TSV
TSV

Reputation: 7641

Does this help (jsfiddle) ?

markup:

  <p id="target">
  <!--ko if: contents.itemContainer() -->
  <!--ko foreach: contents.itemContainer().items -->
    <!--ko template: 'itemTemplate', data: $data -->
    <!--/ko -->
  <!--/ko -->
  <!--/ko -->
   </p>
   <p id="actions">
      <button data-bind="click: actions.add">Add</button>
      <button data-bind="click: actions.reset">Reset</button>
    </p>
    <p>Databound count:<span data-bind="text: actions.count"></span></p>
    <p>Actual count:<span id="actualCount"></span></p>
    <script type="text/html" id="itemTemplate">
      <span data-bind="text: $data"></span>
    </script>

code:

    var unwrap = ko.unwrap;
var viewModel = {
    contents: {
        itemContainer: ko.observable(null)
    },
    actions: {
        add: function () {
            if (!viewModel.contents.itemContainer()) {
                viewModel.contents.itemContainer({items: ko.observableArray([])});
            }
            if (viewModel.contents.itemContainer().items) {
                viewModel.contents.itemContainer().items.push("new item");
            }
            $("#actualCount").html(viewModel.contents.itemContainer().items().length);
        },
        reset: function () {
            viewModel.contents.itemContainer({items: ko.observableArray(["item 1", "item 2"])});
            $("#actualCount").html(viewModel.contents.itemContainer().items().length);
        },
        count: ko.computed(function() {
            if (!viewModel) {
                return -1;
            }
            return viewModel.contents.itemContainer().items().length;
        })
    }
};
ko.applyBindings(viewModel);

Upvotes: 1

Related Questions