0

Hello and thanks for taking a look.

Screen shot I have a dynamic survey with a pretty deep hierarchy:

<assessment>
    <criterionTypes>
      <criterionType>
        <criteria>
          <criterion>
            <responses>
              <response>
                <assessmentResponses>
                  <assessmentResponse>

Occasionally, I would have some of the observable items in the array not bind to the form in the proper order.

enter image description here This despite my iterating over that collection, adjusting the sort before handing it over to the view and even seeing that list in the proper order in the Chrome debugger. I was able to resolve this by applying a sort routine into the foreach for those collections that weren't well behaved. Like so:

  <!-- ko foreach: criterionTypes -->
  <div data-bind="foreach: criteria.sort($root.context.sort.criteria),

or

<!-- ko foreach: assessmentResponses.sort(function (l, r) { return (l.id() == r.id()) ? (l.id() > r.id() ? -1 : 1) : (l.id() > r.id() ? -1 : 1) }) ... -->

So far so good. However, I have a function where a user can add a new row, and while the model gets updated in knockout, the UI will not reflect those changes. So if I drop the sort, the model binding works and the UI updates as expected.

In my update button I have tried to rebind in my click event:

var addResponse = function (response) {
        core.addResponse(assessment(), response); 
        ko.applyBindings(assessment);
    };

But I wind up with the same error regardless of what object I try to bind (assessment, response, etc)

Uncaught Error: Unable to parse bindings. Message: ReferenceError: router is not defined; Bindings value: compose: {model: router.activeItem, afterCompose: router.afterCompose, transition: 'entrance'}

I'm not sure how I might proceed with this. Perhaps a custom binding that would perform the sort on the foreach, but I couldn't get that sorted out (pun-intended).

    <div id="boolean.secondaryResponse" data-bind="if: isSecondaryResponse(), visible: $parent.showSecondaryResponse()>
<!-- ko foreach: assessmentResponses.sort(function (l, r) { return (l.id() == r.id()) ? (l.id() > r.id() ? -1 : 1) : (l.id() > r.id() ? -1 : 1) }) -->                      
                          <!-- ko if: customResponse().template() == 'ingredientSource'-->
                          <div class="col-md-2 col-lg-2" style="z-index: 10"><select data-bind="value: explanation, options: $root.controls.ingredientOrigins, event: { change: $root.context.selectionChanged }, attr: { class: 'form-control ' + criterionCode() + ' ordinal-' + customResponse().ordinal() } " class="remove"></select> &nbsp;</div>&nbsp;
                          <!-- /ko -->
                          <!-- ko if: customResponse().template().startsWith('span')-->
                          <div data-bind="attr: { class: customResponse().template() + ' col-sm-2  col-md-4' }" style="margin-left: -10px; z-index: 10; height: 69px"><input type="text" data-bind="value: textualResponse, attr: { class: 'form-control auto' + customResponse().name() + ' ordinal-' + customResponse().ordinal(), placeholder: customResponse().placeholder }" class="remove" /></div>
                          <!--/ko -->
                          <!-- /ko -->
                          <!-- /ko -->
</div>
1
  • Have you considered using a ko.computed to do the sort on the observable array in the viewmodel and binding the foreach the computed. That has work quite well for me in the past. Commented Jul 29, 2014 at 23:35

2 Answers 2

1

In addition to Michael's answer, I would like to clear up some understanding on ko's observableArray.sort().

Try in Console

> var arr = ko.observableArray([3,1,4,2]);
> var b = arr.sort();
> arr();  // arr.sort() mutate arr itself
< [1, 2, 3, 4]
> ko.isObservable(b); // result of arr.sort() is not a observable
< false
> b
< [1, 2, 3, 4]

In ko, any non-trivial expression in binding is automatically wrapped as a ko.computed( ko.dependentObservable ). For data-bind="foreach: arr.sort()", ko will build a ko.computed(function() { return arr.sort();})

The problem here is the arr.sort() doesn't trigger auto-dependency-tracking mechanism in ko.

Try this in Console

// k1 is not dependent on arr!
// (I'm not sure whether this is intended in knockout)
> var k1 = ko.computed(function() { return arr.sort();});
> k1.getDependenciesCount();
< 0
> arr.getSubscriptionsCount();
< 0

// k2 is dependent on arr!
> var k2 = ko.computed(function() { return arr().sort();});
> k2.getDependenciesCount();
< 1
> arr.getSubscriptionsCount();
< 1

Here is a demo showing k1 doesn't respond to arr change, but k2 does. http://jsfiddle.net/gfHz3/8/

So a simple fix is to use assessmentResponses().sort(...)

The native sort(...) on assessmentResponses() still mutates the content that assessmentResponses holds. In Michael's answer, he uses slice(0) to fix the dependency issue, it also has effect of copying the array before sort. Personally, I always use underscore sortBy function which is non-destructive.

BTW, one more thing, this line

return (l.id() == r.id()) ? (l.id() > r.id() ? -1 : 1) : (l.id() > r.id() ? -1 : 1);

is exactly same as

return l.id() > r.id() ? -1 : 1;

I think you try to write following?

return (l.id() == r.id()) ? 0 : (l.id() > r.id() ? -1 : 1);
Sign up to request clarification or add additional context in comments.

1 Comment

A most excellent and thoughtful post, not merely because you resolved my issue, because you explained in a clear fashion what I had overlooked. Another post I had read regarding sorting said to do collection.sort() to avoid a performance hit, but hadn't mentioned what one would give up to gain that efficiency. And thanks too for the BTW tip, I'd also caught that error after my post, but my laptop melted shortly there after (no really final temp reading was 212!)
1

How you approach sorting arrays depends on your application design. The most important question is usually "Is sorting important to the model or just to the view (UI)?"

Sort for the View

If it's only important to the view, then you don't need to worry about keeping the data in your view model sorted. You only need to the sort it as it's displayed in the view. For that, you can bind to a copy of the array that's sorted:

foreach: criteria.slice(0).sort(criteriaSortingFunction)

You could also do this using a computed observable in your view model to help keep your view clean:

this.sortedCriteria = ko.computed(function () {
    return criteria.slice(0).sort(criteriaSortingFunction);
}, this);

Sort for the Model

If it's important for the data to be sorted in your model, then one method is to make sure that the data is sorted before the observable is updated:

this.addCriteria(toAdd) {
    var rawArray = this.criteria();
    rawArray.push(toAdd);
    rawArray.sort(criteriaSortingFunction);
    this.criteria(rawArray);
}

If you update the array in a lot places, this can get repetitive. Alternatively, you can add an extender that will keep the array sorted:

ko.extenders.sorted = function (obs, sortFunction) {
    obs.sort(sortFunction);
    obs.subscribe(function (array) {
        array.sort(sortFunction);
    });
}

This can be applied in your view model constructor as follows:

this.criteria = ko.observableArray(initialCriteria).extend({sorted: criteriaSortingFunction});

1 Comment

Michael, thank you for a thoughtful and considered answer. I would like to have marked both the answer as they were equally instructive and correct; I awarded the 'point' to huocp only because your standing is that much higher, but my thanks are no less heartfelt.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.