1
votes

I'm using knockout.js 3.3.0 with jQuery mobile 1.4. The problem using knockout.js together with jQuery mobile is that the programmatically changes to the underlying viewmodel are not always reflected to the graphical user interface, due to the JQM refactoring of the html elements, or widgets. So, for example, the JQM selectmenu is synchronized from the user interface to the viewmodel, but not in the other way.

I'm trying to stick together the 3.3.0 KO 'options' binding with a custombinding to the current JQM actual version. There are already two possible solutions for the 'refresh' problem already proposed at SO for 2.x versions of KO: jqmSelect and jqmValue, as custombindings. I try this suggestions for a more recent KO+JQM combination, putting together all the answers/comments found at SO regarding this topic.

This is the js that i'm using to test:

$(document).ready(function () {
    var jsonResultData = [{
        "id": 6,
            "name": "First item",
            "pattern": "Cheetah"
    }, {
        "id": 2,
            "name": "Second item",
            "pattern": "Viper"
    }, {
        "id": 1,
            "name": "Third item",
            "pattern": "Scorpion"
    }];
    ko.applyBindings(new AdminViewModel(jsonResultData));
});

function Match(data) {
    this.id = ko.observable(data.id);
    this.pattern = ko.observable(data.pattern);
    this.name = ko.observable(data.name);
}

function AdminViewModel(allData) {
    var self = this;
    self.matches = ko.observableArray([]);
    self.matchesFromDb = $.map(allData, function (item) {
        return new Match(item);
    });
    self.matches = self.matchesFromDb;
    self.selectedMatchId = ko.observable(self.matches[0].id());
    self.selectedMatch = ko.observable(self.matches[0]);
    self.setSelectedMatchId = function (match) {
        if (match.id() != self.selectedMatchId()) {
            self.selectedMatchId(match.id());
            self.selectedMatch(match);
        }
    };
    self.patternValues = ko.observableArray(["Shark", "Viper", "Chameleon", "Cheetah", "Scorpion"]);
}

I made a fiddle to test the jqmValue custom binding, which is one of the latest soultions found at SO, but but i'm not able to get it to work:

ko.bindingHandlers.jqmValue = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        if (typeof ko.bindingHandlers.value.init !== 'undefined') {
            ko.bindingHandlers.value.init(element, valueAccessor, allBindingsAccessor, viewModel);
        }
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var instance;
        if (typeof ko.bindingHandlers.value.update !== 'undefined') {
            ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor, viewModel);
        }
        instance = $.data(element, 'mobile-selectmenu');
        if (instance) {
            $(element).selectmenu('refresh', true);
        }
    }
};

(the original code for AdminViewModel as reference for KO 2.x thanks to pablo is working with the suggested change to invert value/options in the markup thanks to JohnEarles at google groups)

Here is the fiddle with actual KO and JQM versions where i try to include the best of all suggestions found regarding this topic:

http://jsfiddle.net/nHNzL/42/

but i'm still streching my hairs without success. Why in my test fiddle the changes to the viewmodel are not reflected to the JQM selectmenu? What is my error?

UPDATE: two-page fiddle to test also initialization: http://jsfiddle.net/nHNzL/50/

FINAL VERSION: I made 2 small fixes and 1 change: 1) isInstance shall be checked every time 2) removed the if (currentValue == value) 3) inverted the disabled attribute Moreover: i tested this custombinding in a ko foreach-loop, each select element needs to be child of an own container div.

ko.bindingHandlers.jqmSelectMenu = {
    init: function (element, valueAccessor, allBindings) {
        var options = ko.toJS(valueAccessor()),
            valueObservable = allBindings.get("value"), valueSubscription,
            optionsObservable = allBindings.get("options"), optionsSubscription;
        var refresh = function () { 
            var $el = $(element);
            var isInstance = !!$.data(element, 'mobile-selectmenu');
            if (isInstance) {
                $el.selectmenu('refresh', true); 
            } else {
                /* instantiate the widget unless jqm has already done so */
                $(element).selectmenu(options);
            }
        };
        refresh();

        /* hook up to the observables that make up the underlying <select> */
        if (ko.isSubscribable(valueObservable)) {
            valueSubscription = valueObservable.subscribe(refresh);
        }
        if (ko.isSubscribable(optionsObservable)) {
            optionsSubscription = optionsObservable.subscribe(refresh);
        }

        /* properly dispose of widgets & subscriptions when element is removed */
        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            $(element).selectmenu("destroy");
            if (valueSubscription) valueSubscription.dispose();
            if (optionsSubscription) optionsSubscription.dispose();
        });
    },
    update: function (element, valueAccessor, allBindings) {
        var options = ko.toJS(valueAccessor()),
            $el = $(element);

        /* update any widget options if necessary */
        ko.utils.objectForEach(options, function (key, value) {
            if (key === "enabled") {
                $el.selectmenu(value ? "enable" : "disable");
            } else {
                $el.selectmenu("option", key, value);
            }
        });
    }
};
1

1 Answers

0
votes

In fact you are mixing two separate things.

  1. There is the underlying <select> element. It holds the available options as well as the selected one.
  2. Then there is the jQuery Mobile SelectMenu widget. It is concerned with the UX part. A look at the API options it provides reveals that it really has not a lot to do with the underlying select box, mostly using it for self-initialization.

Of course the widget communicates a value change to the underlying select box, but when you change the value programmatically you must communicate that to the widget yourself, by calling refresh on it. So even without knockout this is not a two-way communication.

We do have set of knockout built-in bindings that work very well with select boxes, but what we don't have is a binding that communicates changes in those values to the widget. What we also don't have is a way of initializing/updating any of the widget's API options.

So instead of re-inventing the value binding we need one that deals with the widget itself and otherwise simply complements the existing bindings:

ko.bindingHandlers.jqmSelectMenu = {
    init: function (element, valueAccessor, allBindings) {
        var options = ko.toJS(valueAccessor()),
            valueObservable = allBindings.get("value"), valueSubscription,
            optionsObservable = allBindings.get("options"), optionsSubscription,
            isInstance = !!$.data(element, 'mobile-selectmenu'),
            refresh = function () { $(element).selectmenu('refresh', true); };

        // instantiate the widget unless jqm has already done so
        if (!isInstance) $(element).selectmenu(options);
        refresh();

        // hook up to the observables that make up the underlying <select>
        if (ko.isSubscribable(valueObservable)) {
            valueSubscription = valueObservable.subscribe(refresh);
        }
        if (ko.isSubscribable(optionsObservable)) {
            optionsSubscription = optionsObservable.subscribe(refresh);
        }

        // properly dispose of widgets & subscriptions when element is removed
        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            $(element).selectmenu("destroy");
            if (valueSubscription) valueSubscription.dispose();
            if (optionsSubscription) optionsSubscription.dispose();
        });
    },
    update: function (element, valueAccessor, allBindings) {
        var options = ko.toJS(valueAccessor()),
            $elem = $(element);

        // update any widget options if necessary
        ko.utils.objectForEach(options, function (key, value) {
            var currentValue = $elem.selectmenu("option", key);
            if (currentValue !== value) {
                if (key === "disabled") {
                    $elem.selectmenu(value ? "disable" : "enable");
                } else {
                    $elem.selectmenu("option", key, value);
                }
            }
        });
    }
};

I have written this in the spirit of the knockout-jQueryUI. I recommend taking a look at that library.

Here's an updated fiddle, too. http://jsfiddle.net/nHNzL/46/