0

I am combining an Options binding and a Computed, and am getting the error in the Firebug console:

TypeError: this.selectedCountryNeverNull is undefined

This is the relevant part of the ViewModel:

    // Constructor for an object with two properties
    var Country = function (name, population) {
        this.countryName = name;
        this.countryPopulation = population;
    };

    var viewModel = {
        availableCountries: ko.observableArray([
            new Country("UK", 65000000),
            new Country("USA", 320000000),
            new Country("Sweden", 29000000)
        ]),
        selectedCountry: ko.observable(), // Nothing selected by default
        selectedCountryNeverNull: ko.observable(), // has default
        selectedCountryDesc: ko.computed(function () { return '*' + this.selectedCountryNeverNull.countryName + '*'; }, this)
    };

and this is the select:

    <select data-bind="options: availableCountries,
                   optionsText: 'countryName',
                   value: selectedCountryNeverNull"></select>

I left out the optionsCaption: so that the first array element is the default, and the value is never null.

Firebug says the error is in the ko.computed line, and I tried to add () parentheses here and there, but with no avail.

I want to do much bigger ko.computed stuff, but isolated my problem in this extension of an example from the knockout site.

Thanks for any help in trying to understand the parentheses issues in general and mine particular.

Roland
  • 4,619
  • 7
  • 49
  • 81
  • This sounds like it may be a combination of `this` context which can wrap you in circles when using knockout, and parantheses.. In your viewmodel do a `var self = this;` line to start and use `self` in place of `this`. Also an observable, observablearray, and computed are all functions, so toss in the parantheses when referencing them `return '*' + this.selectedCountryNeverNull().countryName + '*';` – JNevill Feb 22 '17 at 15:11
  • @JNevill You mean I should convert from `var viewModel` to `function viewModel` ? – Roland Feb 22 '17 at 15:13
  • 1
    No. I mean creating a variable for `this` to be referenced down in your computed. Take a look at the answer [here](http://stackoverflow.com/questions/9589419/difference-between-knockout-view-models-declared-as-object-literals-vs-functions) – JNevill Feb 22 '17 at 15:16
  • 1
    [And the documentation for knockout here](http://knockoutjs.com/documentation/computedObservables.html) describing the convention of using `var self = this` to avoid mistakenly pointing to the wrong thing. – JNevill Feb 22 '17 at 15:17
  • @JNevill Good pointers, I will definitely study those! – Roland Feb 22 '17 at 15:29

1 Answers1

3

I see three problems with your code:

  1. this in your ko.computed refers to Window, not to viewModel,
  2. The select binding sets its value at the time of applyBindings, so for a brief period of time, selectedCountryNeverNull is actually undefined. This means you'll have to set it with a default manually or check for falsey values in your comptued
  3. In the computed, you'll need to use the () to get selectedCountryNeverNull's value and create a dependency.

An example on how to fix all three:

  1. Use a constructor and the new keyword to manage this,
  2. Default to an empty object in the computed ( || {} )
  3. Call the observable in the computed ( () )

// Constructor for an object with two properties
var Country = function(name, population) {
  this.countryName = name;
  this.countryPopulation = population;
};

var ViewModel = function() {
  this.availableCountries = ko.observableArray([
    new Country("UK", 65000000),
    new Country("USA", 320000000),
    new Country("Sweden", 29000000)
  ]);
  
  this.selectedCountry = ko.observable(); // Nothing selected by default
  
  this.selectedCountryNeverNull = ko.observable(); // has default
  
  this.selectedCountryDesc = ko.computed(function() {
    return '*' + (this.selectedCountryNeverNull() || {}).countryName + '*';
  }, this);
};

ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="options: availableCountries,
                   optionsText: 'countryName',
                   value: selectedCountryNeverNull"></select>
                   
Selection: <span data-bind="text: selectedCountryDesc"></span>
user3297291
  • 22,592
  • 4
  • 29
  • 45
  • I got your answer to work after converting `var viewModel = function` to `function ViewModel() {...` – Roland Feb 22 '17 at 15:25
  • That's essentially the same thing but the order of your code matters. Have a look at this explanation http://stackoverflow.com/questions/336859/javascript-function-declaration-syntax-var-fn-function-vs-function-fn – user3297291 Feb 22 '17 at 15:27
  • Also my "trick" of not using the "optionsCaption:" does not eliminate the danger of the selectedCountry being null ? Is that not possible? My mother taught me: always initialize your variables, but here that seems not so easy – Roland Feb 22 '17 at 15:29
  • I tried to explain this in point 2... When you define your `computed`, it runs once to find out its initial value. At this time, `selectedCountryNeverNull` is `undefined`, so it won't have a `countryName` property and throw an error. Once you call `applyBindings`, the binding sets the value. But this is too late for it to recover... – user3297291 Feb 22 '17 at 15:35
  • I see. And ` || {}` is the rescue, then `{}` is defined, but isn't this "nothing" or empty? Seems like I need to read more of my 1000 page JS book – Roland Feb 22 '17 at 15:37
  • 1
    When you have an object (`{}`) and try to retreive a property that doesn't exist, javascript returns `undefined`. So: `{}.countryName` => `undefined`. When you have `null` or `undefined`, and you try to retreive a property, javascript throws an error. So: `undefined.countryName` => `Error: Cannot read property 'countryName' of undefined` – user3297291 Feb 22 '17 at 15:40
  • Great, thanks. Basically, I was trying to avoid verbose stuff like `selectedCountry() ? selectedCountry().countryName : 'unknown'`, but your shorthand notation is a better solution than omitting `optionsCaption:` – Roland Feb 22 '17 at 15:44