Search code examples
javascriptjqueryknockout.jsknockout-2.0

knockout unable to process binding "foreach"


I'm new to Knockout and I'm building an app that's effectively a large-scale calculator. So far I have two instances of knockout running on one page. One instance is working perfectly fine, however the other one is entirely broken and just won't seem to register at all?

Below is my Javascript, fetchYear is the function that works perfectly fine and fetchPopulation is the one that's completely broken. It doesn't seem to register "ageview" from the HTML at all and I can't figure out.

The error:

Uncaught ReferenceError: Unable to process binding "foreach: function (){return ageView }" Message: ageView is not defined

Thanks in advance.

JS:

var index = {

    fetchYear: function () {
        Item = function(year){
            var self = this;
            self.year = ko.observable(year || '');
            self.chosenYear = ko.observable('');
            self.horizon = ko.computed(function(){
                if(self.chosenYear() == '' || self.chosenYear().horizon == undefined)
                    return [];
                return self.chosenYear().horizon;
            });
        };
        YearViewModel = function(yeardata) {
            var self = this;
            self.yearSelect = yeardata;
            self.yearView = ko.observableArray([ new Item() ]);
            self.add = function(){
                self.yearView.push(new Item("New"));
            }; 
        };
        ko.applyBindings(new YearViewModel(yearData));
    },

    fetchPopulation: function () {
        popItem = function(age){
            var self = this;
            self.age = ko.observable(age || '');
            self.chosenAge = ko.observable('');
            self.population = ko.computed(function(){
                if(self.chosenAge() == '' || self.chosenAge().population == undefined)
                    return [];
                return self.chosenAge().population;
            });
        };
        PopulationViewModel = function(populationdata) {
            var self = this;
            self.ageSelect = populationdata;
            self.ageView = ko.observableArray([ new popItem() ]);
            self.add = function(){
                self.ageView.push(new popItem("New"));
            }; 
        };
        ko.applyBindings(new PopulationViewModel(populationData));
    }

}

index.fetchYear();
index.fetchPopulation();

HTML:

<div class="row" data-bind="foreach: yearView">
    <div class="grid_6">
        <img src="assets/img/index/calendar.png" width="120" height="120" />
        <select class="s-year input-setting" data-bind="options: $parent.yearSelect, optionsText: 'year', value: chosenYear"></select>
        <label for="s-year">Start year for the model analysis</label>
    </div>
    <div class="grid_6">
        <img src="assets/img/index/clock.png" width="120" height="120" />
        <select class="s-horizon input-setting" data-bind="options: horizon, value: horizon"></select>
        <label for="s-horizon">Analysis time horizon</label>
    </div>
</div>

<div class="row" data-bind="foreach: ageView">
    <div class="grid_6">
        <img src="assets/img/index/calendar.png" width="120" height="120" />
        <select class="s-year input-setting" data-bind="options: ageSelect, optionsText: 'age', value: chosenAge"></select>
        <label for="s-agegroup">Age group of <br> target population</label>
    </div>
    <div class="grid_6">
        <img src="assets/img/index/clock.png" width="120" height="120" />
        <input class="s-population input-setting"></input>
        <label for="s-population">Size of your patient <br> population <strong>National</strong> </label>
    </div>
</div>

Solution

  • When you do this (in fetchYear):

    ko.applyBindings(new YearViewModel(yearData));
    

    You are binding the entire page with the YearViewModel view model. But the YearViewModel doesn't have a property called ageView so you get the error and knockout stops trying to bind anything else.

    What you need to do is restrict your bindings to cover only part of the dom by passing the element you want to ko.applyBindings. For example:

    <div class="row" id="yearVM" data-bind="foreach: yearView">
    //....
    <div class="row" id="popVM" data-bind="foreach: ageView">
    

    And then:

    ko.applyBindings(new YearViewModel(yearData), document.getElementById("yearVM"));
    //...
    ko.applyBindings(new PopulationViewModel(populationData), document.getElementById("popVM"));
    

    Now your bindings are restricted just to the part of the DOM that actually displays stuff from that model.

    Another alternative is to just have your two view models as part of a parent view model and then you can apply the binding to the entire page. This makes it easier if you need to mix parts from both VMs and they are not conveniently separated in distinct sections of your page. Something like:

    var myParentVM = {
        yearVM : index.fetchYear(),          // note, make this return the VM instead of binding it
        popVM : index.fetchPopulation(),     // ditto
    }
    
    ko.applyBindings(myParentVM);
    

    And then you'd declare your bindings like so:

    <div class="row" data-bind="foreach: yearVM.yearView">