12
votes

I am spending hours trying to get a simple event call working correctly in my durandal/knockout app.

Context

I have a list of languages that that the user can select from a select-box:

    <select class="form-control select2"
        data-bind="event: { change: app.languageChanged }, options:languages,
        optionsText:'label',
        optionsValue:'code',
        value:app.selectedLanguage"></select>

The property app.selectedLanguage is a ko.observable. I know that this works because the correct item gets pre-selected.

    this.selectedLanguage = ko.observable(options.defaultLanguage);

I also have an event-handler which listens for changes on that select box, so that I can send a message to other parts of the application that need to be informed:

    languageChanged : function(data, event) {
        console.log(data);
        console.log(event);
        console.log(this.selectedLanguage());

        app.trigger('language:change', this.selectedLanguage());
    },

The problem

  1. the first parameter 'data' does not contain the selected item, but instead contains all items (actually, it seems to be the complete current view-model).
  2. If 1. does not work, then it would be an alternative to at least get hold of the new value from the observable 'selectedLanguage'. Unfortunately that always seems to have the old value. So whenever I change the selectbox option, I always get the previously selected value.

Question

So the question is: what could I be doing wrong? I am sure that this normally works correctly and I must be missing something somewhere.

I thought I had finally understood how knockout works, but now I have come across the next issue. I would be very grateful if someone could help me on this.

EDIT [SOLVED]

Thanks to xdumaine, here is the (nice and simple) solution:

In my html template, I removed the change-event:

    <select class="form-control select2"
        data-bind="options:languages,
        optionsText:'label',
        optionsValue:'code',
        value:app.selectedLanguage"></select>

In my App view-model (that I require everywhere), I now subscribe to the ko.observable instead of listening to the event-handler:

    define([ 'durandal/app', 'underscore', 'knockout', 'myapp/myapp' ], function(app, _, ko, myapp) {

        "use strict";

        function App(options) {

            if (!(this instanceof App)) {
                throw new TypeError("App constructor cannot be called as a function.");
            }

            this.options = options || {};

            // Set the initial language.
            this.selectedLanguage = ko.observable(options.defaultLanguage);
                    // *** Subscribes to the observable ***
            this.selectedLanguage.subscribe(function(newValue) {
                console.log(newValue);
                app.trigger('language:change', newValue);
            });

            _.bindAll(this, 'getSelectedLanguage');
        }

        App.prototype = {
            constructor : App,
            getSelectedLanguage : function() {
                return this.selectedLanguage();
            }
        }

        return App;
    });

This code has therefore been removed and is no longer needed:

languageChanged : function(data, event) {
    console.log(data);
    console.log(event);
    console.log(this.selectedLanguage());

    app.trigger('language:change', this.selectedLanguage());
},

Best regards, Michael

1
It's unclear what app is in your code. Is that a durandal thing or is that your viewModel?xdumaine
Hi, it is a global ViewModel that I pass via require into all my pages: define([ 'knockout', 'myapp/myapp' ], function(ko, myapp) {. I then make it available to the view like this: return { app: myapp.app, ...}.michaeldd
@michaeldd Michael, that goes back to one of the recommendations I made: knockout-postbox. Ryan Niemeyer has built some great functionality on top of subscribing directly to observables that's just as easy to use. While we use postal.js for our client-side message bus, we do you direct subscriptions for internal notifications, and when we need to get that close to the metal.user3174746
Yes, thanks. I suppose in the question that you are referring to (stackoverflow.com/questions/22542051/…) I was more concerned in getting the language-change-event being "noticed" in the right places. It did not occur to me at that point that I was having this issue. I thought the actual handling of the event was working fine and from there it was just a matter of passing the message on. This morning I realised that that was unfortunately not the case and I had not quite understood the concept of the knockout-subscribe yet.michaeldd

1 Answers

19
votes

Why bind to the select change event instead of just subscribing to the selectedLanguage?

var self = this;
self.selectedLanguage = ko.observable();
self.selectedLangauge.subscribe(function(newValue) {
    console.log(newValue);
    app.trigger('language:change', newValue);
});

If you want to do it like you have it, know this: event bindings in knockout always get a reference to the viewModel as the first parameter, and the event data as the second, so you could would have to inspect the event to get the target and extract the value if you're doing it that way. The reason 2 is not working is that your change event is firing before the knockout observable is notified, so you get timing issues. This could have different behavior in different browsers.

I'd recommend sticking to observable subscriptions, instead of using DOM events, whenever possible.