semper fi
semper fi

Reputation: 747

knockout bind do not updating when observable has changed

I've a dropdown button which should be avaiable after ajax request will be finished.

<div class="form-input">
   <label class="">Sort by:</label>
   <select name="orderby" class="selectpicker" data-bind="value: sortBy, optionsCaption: 'Default', disable: waiting">
      <option value="some_value">some_option</option>
      <option value="some_value">some_option</option>
   </select>
</div>

On page requested, it initially load data

$(function() {
    //Initialization
    var vm = new ArticleViewModel();
    initialLoadArticles(vm);
    ko.applyBindings(vm, $("#article-plugin")[0]);
});

function ArticleViewModel() {
    var self = this;

    //options =>
    this.articles = ko.observableArray([]);
    this.pageSize = 12;

    this.sortBy = ko.observable('asc');

    this.currentPage = ko.observable(1);
    this.waiting = ko.observable(true);
    this.totalPages = 0;
    this.initMode = true;
    this.timerId = null;
    this.viewTemplate = ko.observable('listview-template');
    if (this.viewTemplate() === "listview-template") {
        this.pageSize = 4
    } else {
        this.pageSize = 12
    };


    this.sortBy.subscribe(function(event) {
        console.log(event);
        self.optionChanged();
        loadArticles(self);
    });

    this.optionChanged = function() {
        this.currentPage(1);
    }


    this.setCardView = function() {
        self.viewTemplate('cardview-template');
        loadArticles(self);
    }
    this.setListView = function() {
        self.viewTemplate('listview-template');
        loadArticles(self);
    }

}

function initialLoadArticles(vm) {
    vm.waiting(true);
    var params = {
        page: vm.currentPage(),
        size: vm.pageSize,
        sortby: vm.sortBy()
    };

    api.ajax.get(api.urls.article.getArticles, params, function(response) {
        console.log('waiting: ' + vm.waiting());
        if (response.success) {
            vm.articles(response.data.items);
            vm.waiting(false);
        }
    });

}

Well, on a page it display all articles, but dropdown button still blocked and I don't what exactly could be the problem of that.

Upvotes: 0

Views: 284

Answers (1)

Tomalak
Tomalak

Reputation: 338208

I'd suggest a few changes to your viewmodel, featuring automatic loading via a subscription.

I think you always want to set waiting to false after loading, independent of whether the request was a success or not. Also think about low-level request errors, you need to add a handler for those.

function ArticleViewModel() {
    var self = this;

    self.articles = ko.observableArray();

    self.pageSize = ko.observable();
    self.sortBy = ko.observable('asc');
    self.currentPage = ko.observable();
    self.waiting = ko.observable(true);
    self.viewTemplate = ko.observable();

    // API
    self.setCardView = function() {
        self.viewTemplate('cardview-template');
        self.pageSize(12);
        self.currentPage(1);
    };
    self.setListView = function() {
        self.viewTemplate('listview-template');
        self.pageSize(4);
        self.currentPage(1);
    };

    // compute Ajax-relevant parameters
    self.ajaxParams = ko.pureComputed(function () {
        return {
            page: self.currentPage(),
            size: self.pageSize(),
            sortby: self.sortBy()
        };
    }).extend({ rateLimit: { timeout: 10, method: 'notifyWhenChangesStop' } });

    // auto-load when params change
    self.ajaxParams.subscribe(function (params) {
        self.waiting(true);
        api.ajax.get(api.urls.article.getArticles, params, function (response) {
            if (response.success) {
                self.articles(response.data.items);
            }
            self.waiting(false);
        });
    });

    // set inital view (also triggers load)
    self.setListView();
}

$(function() {
    var vm = new ArticleViewModel();
    ko.applyBindings(vm, $('#article-plugin')[0]);
});

More strictly speaking, you I'd advice against true or false as the "loading" indicator. It's technically possible that more than one Ajax request is running and this would be a race condition. The first request that comes back resets the "loading" state, and the next one still overwrites the viewmodel data. Either use a counter, or prevent new requests while there is a pending one.

The rateLimit extender makes sure that a rapid succession of changes to the parameters, like what happens when setListView() is called, does not cause multiple Ajax requests.


If your Ajax requests are done by jQuery internally, I would suggest the following setup to be able to make use of the done, fail and always promise handlers:

function ApiWrapper() {
    var self = this;

    function unwrapApiResponse(response) {
        if (response.success) {
            return new $.Deferred().resolve(response.data).promise();
        } else {
            return new $.Deferred().reject(response.error).promise();
        }
    }

    self.getArticles = function (params) {
        return $.get('articleUrl', params).then(unwrapApiResponse);
    };
    // more functions like this
}

var api = new ApiWrapper();

and in your viewmodel:

self.ajaxParams.subscribe(function (params) {
    self.waiting(true);
    api.getArticles(params).done(function (data) {
        self.articles(data.items);
    }).fail(function (err) {
        // show error
    }).always(function () {
        self.waiting(false);
    });
});

Upvotes: 2

Related Questions