2
votes

I've been going crazy trying to figure this one out. I've seen plenty of obfuscated and tough-to-implement examples of filling a computed observable in Knockout when an async call has finished, but I can't seem to make it work using the module-reveal pattern.

I'm trying to create a read-only observable, as that's all I need. I wouldn't even make an observable if I didn't need an anonymous function to run to make the request. Here is my view-model:

eh.vm.skills = function () {

    //#region Public Variables

    var skills = ko.computed({
            read: function () {
                $.get("http://horodyski.me/api/skills", function (data) {
                    return data;
                });
            },
            deferEvaluation: true
        }),
    //#endregion


    //#region Public Interface

    return {
        skills: skills
    }

    //#endregion
};

And this is the HTML binding:

<ul class="skills skills-top" id="skills" data-bind="foreach: skills">
    <li>
        <i data-bind="text: $index"> </i>
        <span data-bind="text: $data.Title"></span>
    </li>
</ul>
<script src="//cdn.horodyski.me/js/vm.js"></script>
<script>
    ko.applyBindings(eh.vm.skills, $("#skills")[0]);
</script>

What I'd like to do is on creation of the skills variable, fetch the data and return it. The data already comes back in an array (ex: [{Title: "ABC"}]) but it doesn't seem to bind. I tried using $.when().then() instead (as I prefer it), but even when the computed value is deferred it still won't update.

The really complex part for me is variable scope. The module-reveal pattern sucks when it comes to scope. I've been racking my brains over this for 3 hours...if anyone can guide me in the right direction it would be appreciated.

Edit Using Knockout 3.1 (if it helps)

1

1 Answers

6
votes

Your read callback is all wrong.

The callback:

function (data) {
    return data;
}

Is dead code, its returning the data to the jQuery function that called this callback, which is going to do nothing with it.

You need to return the deferred itself:

read: function () {
    return $.get("http://horodyski.me/api/skills");
}

You also need the async extender here: http://smellegantcode.wordpress.com/2012/12/10/asynchronous-computed-observables-in-knockout-js/

ko.extenders.async = function(computedDeferred, initialValue) {
    var plainObservable = ko.observable(initialValue), currentDeferred;
    plainObservable.inProgress = ko.observable(false);

    ko.computed(function() {
        if (currentDeferred) {
            currentDeferred.reject();
            currentDeferred = null;
        }

        var newDeferred = computedDeferred();
        if (newDeferred && (typeof newDeferred.done == "function")) {
            plainObservable.inProgress(true);
            currentDeferred = $.Deferred().done(function(data) {
                plainObservable.inProgress(false);
                plainObservable(data);
            });
            newDeferred.done(currentDeferred.resolve);
        } else {
            plainObservable(newDeferred);
        }
    });

    return plainObservable;
};

Used like:

var skills = ko.computed(...).extend({ async: null });

If you want to hook into the JSON before it is passed to your view:

var skillsJson = ko.computed(...).extend({ async: null });
var skills = ko.computed(function () {
    return transformData(skillsJson());
};

Or:

var skills = ko.computed(function () {
        return $.get("url").then(function (data) {
            return transform(data);
        });
    });