1
votes

Consider the following ko bindings on a select element:

<select data-bind="value: valueObservable, options: optionsObservableArray, 
    optionsCaption: '[None] - this is an optional field'">

... given a viewmodel like the following:

function MyViewModel()
{
    var self = this;
    self.valueObservable = ko.observable();
    self.optionsObservableArray = ko.observableArray();

    // ajax call to load options
    ko.computed(function() {
        $.ajax(...)
        .success(function(optionsResponse) {
            self.optionsObservableArray(optionsResponse)
        });
    });

    // ajax call to load data value
    ko.computed(function() {
        $.ajax(...)
        .success(function(valueResponse) {
            self.valueObservable(valueResponse)
        });
    });
}

What's weird about this is when the second (data) ajax call returns before the first (options) ajax call. Since the select element has an optionsCaption binding, here is what I believe is happening:

  1. The data ajax call completes, setting the valueObservable to some value (like 6, abc, or some other non-falsy value).
  2. Because there is only 1 option in the select element (due to the optionsCaption), and because the valueObservable is bound to it (via the value binding), this causes the valueObservable to be changed to undefined.
  3. Finally, the optionsObservableArray completes and adds new option elements to the select, but by this time it is too late: The valueObservable is now wrapping an undefined value rather than the real data value returned from the first ajax call.

Question: What is the best way to work around this? Here is what I can think of:

  1. Make the first ajax call run with async: false. This may slow page rendering.
  2. Create a separate observable for binding to the select value (such as value: selectedValueObservable). Then subscribe to the optionsObservableArray and use the subscription to do something like self.selectedValueObservable(self.valueObservable()). This seems like a bandaid fix.
  3. Render the select & options to the page before any javascript executes by sending the options data from the server (using MVC viewmodel). This makes it a little more difficult to deal with the options as an observableArray.

Update

There is another concern which I omitted from this question to simplify the example code. In actuality, this viewmodel is used to create an editable list of data items. So there is actually more than 1 dropdown list that gets rendered to the page. The data ajax call really returns an array, and its success function really sets an observableArray of complex items. Since the dropdown list options are reused in every inline form, it is placed on the $parent viewmodel and only loaded once. It is also difficult to pass the select options in a single ajax call because the data items are retrieved via WebAPI (which returns an IEnumerable, no room to send additional dropdown options).

2
What is the reason to wrap ajax call into computed ? - vittore
@vittore, wrapping the ajax call in a ko.computed causes it to be executed immediately. Basically it initializes the viewmodel by sending the ajax calls off when the viewmodel is constructed. - danludwig
there are many other way to execute function immediately, and computed doesnt seem to be designed for that. - vittore

2 Answers

1
votes

I would suggest you to have one ajax call if it is possible. Make your controller to return complex object with array of objects and selected value:

// ajax call to load options and data value
ko.computed(function() {
    $.ajax(...)
    .success(function(response) {
        self.optionsObservableArray(response.options);
        self.valueObservable(response.value);
    });
});

If it is impossible to merge two ajax call you can put calling of the second ajax to success callback of the first ajax:

// ajax call to load options
ko.computed(function() {
    $.ajax(...)
    .success(function(optionsResponse) {
        self.optionsObservableArray(optionsResponse)

        // ajax call to load data value
        $.ajax(...)
        .success(function(valueResponse) {
             self.valueObservable(valueResponse)
        });
    });
});
1
votes

Is there any reason not to make ajax call first and applyBinding on done ?

$.when(getOptions(), getData()).done(bind) 


function getOptions() { 
    return $.ajax(...).success(vm.optionsObservableArray)
}

function getData() {
    return $.ajax(...).success(vm.valueObservable)
}

function bind() {
    ko.applyBindings(vm, document.getElementById('elem')) 
}