KnockoutJS - Frustrations with ko.computed and an AJAX request

14,883

Solution 1

As SLaks pointed out, you cannot do it this way due to the call being made asynchronously, i.e., the "read" function returns before the response got retrieved. I would recommend something like this:

function viewModel() {
    var self = this;
    this.Id = ko.observable("@Model.Identity()");
    this.Points = ko.observable(0);

    var refreshPoints = function() {
        $.ajax({
            url: '/skills/points',
            data: { id: self.Id() }, // <-- you need () here!
            type: 'get',
            success: function (data) {
                self.Points(data);
            }
        });
    };

    // Now subscribe to the ID observable to update the points whenever the 
    // ID gets changed:
    this.Id.subscribe(refreshPoints);
}

Solution 2

Just bind an observable variable to the html variable and update that field in the ko.computed field. Do not directly bind the ko.computed field to the html variable.

self.htmlUserName = ko.observable();

self.computedUserName = ko.computed(function () {
    $.ajax(
    ....
    success: function (result) {
    self.htmlUserName(result);
    }
}

Solution 3

Knockout binding does not support asynchronous computations.

Instead, you should use a regular property and set it to the result of the AJAX request.
You can make it observable by re-sending the AJAX request in the change handler for the dependent property.

You can also use a separate placeholder value to add a loading indicator to your bound UI.

Share:
14,883

Related videos on Youtube

Ciel
Author by

Ciel

Updated on September 16, 2022

Comments

  • Ciel
    Ciel almost 2 years

    I am simply trying to pull some data from an ajax request. The ajax call works - I know the data is retrieved. But it just isn't setting the value of the ko.computed...

            function viewModel() {
                this.Id = ko.observable("@Model.Identity()");
                this.Points = ko.computed({ 
                    read: function () {
                        $.ajax({
                            url: '/skills/points',
                            data: { id: this.Id },
                            type: 'get',
                            success: function (data) {
                                return data;
                            }
                        });
                    },
                    owner: this
                });
            }
    

    So a call like ...

    <span data-bind="text: Points"></span>
    

    Just isn't working. Can anyone help me figure out what I am doing so wrong?

    Update

    I'm using the following code, following RPN's suggestion - and I just can't get it to function at all. It won't look at the controller, it won't return the data... it just does nothing. I've tried all three examples given with no success.

    <script type="text/javascript">
        //an observable that retrieves its value when first bound
        ko.onDemandObservable = function (callback, target) {
            var _value = ko.observable();  //private observable
    
            var result = ko.computed({
                read: function () {
                    //if it has not been loaded, execute the supplied function
                    if (!result.loaded()) {
                        callback.call(target);
                    }
                    //always return the current value
                    return _value();
                },
                write: function (newValue) {
                    //indicate that the value is now loaded and set it
                    result.loaded(true);
                    _value(newValue);
                },
                deferEvaluation: true  //do not evaluate immediately when created
            });
    
            //expose the current state, which can be bound against
            result.loaded = ko.observable();
            //load it again
            result.refresh = function () {
                result.loaded(false);
            };
    
            return result;
        };
    
        $(document).ready(function () {
            function User(id, name) {
                this.Id = ko.observable(id);
                this.Name = ko.observable(name);
            }
            function viewModel() {
                var self = this;
    
                this.User = ko.onDemandObservable(this.Update, this);
    
                this.Update = function () {
                    return $.ajax({
                        url: '/home/user/',
                        data: { id: 1 },
                        dataType: 'json'
                    }).pipe(function (data) {
                        return new User(data.Id, data.Name);
                    });
                };
            };
            var model = new viewModel();
            ko.applyBindings(model);
            model.User();
        });
    </script>
    
    <span data-bind="text: User.Name"></span>
    

    Update (2)

    Following more instructions, I have narrowed down some of the problem. Defining the callback as a function on the viewModel doesn't seem to work (I am not sure why) but declaring an inline function does yield ... something different. Still isn't working though. Here is an update.

    <script type="text/javascript">
        //an observable that retrieves its value when first bound
        ko.onDemandObservable = function (callback, target) {
            var _value = ko.observable();  //private observable
    
            var result = ko.computed({
                read: function () {
                    //if it has not been loaded, execute the supplied function
                    if (!result.loaded()) {
                        callback.call(target);
                    }
                    //always return the current value
                    return _value();
                },
                write: function (newValue) {
                    //indicate that the value is now loaded and set it
                    result.loaded(true);
                    _value(newValue);
                },
                deferEvaluation: true  //do not evaluate immediately when created
            });
    
            //expose the current state, which can be bound against
            result.loaded = ko.observable();
            //load it again
            result.refresh = function () {
                result.loaded(false);
            };
    
            return result;
        };
    
        $(document).ready(function () {
            function User(id, name) {
                this.Id = ko.observable(id);
                this.Name = ko.observable(name);
            }
            function viewModel() {
                var self = this;
    
                this.User = ko.onDemandObservable(function(){
                    $.ajax({
                        url: '/home/user/',
                        data: { id: 1 },
                        dataType: 'json',
                        success: function (data) {
                            self.User(new User(data.Id, data.Name));
                        }
                    });
                }, this);
    
                //this.Update = function () {
                //  $.ajax({
                //      url: '/home/user/',
                //      data: { id: 1 },
                //      dataType: 'json',
                //      success: function (data) {
                //          self.User(new User(data.Id, data.Name));
                //      }
                //  });
                //};
            };
            var model = new viewModel();
            ko.applyBindings(model);
            model.User();
        });
    </script>
    

    And then trying to display any of the data retrieved still fails.

    <span data-bind="text: User.Name"></span>
    

    Update (3)

    A bit of a breakthrough! If I change the declarative binding to look like this ..

    <span data-bind="with: User">
        <span data-bind="text: Id"></span>
        <span data-bind="text: Name"></span>
    </span>
    

    Then I am starting to see results. I think I am almost getting there...

    • SLaks
      SLaks over 11 years
      Welcome to the wonderful world of async! You can't do that.