knockout unable to process binding "foreach"
Asked Answered
S

2

9

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>
Septic answered 15/4, 2014 at 13:38 Comment(0)
M
18

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">
Marna answered 15/4, 2014 at 13:54 Comment(1)
This is great, thanks! I had wondered if it was because I was calling the binding function twice & I wasn't aware that I could target specific DOM elements. Where would I place the alternative (option B above) in my JS in order to get that to work as it should?Septic
A
3

The main reason why this is not working is because you call ko.applyBindings() more than once on a page (that is not really forbidden but is a bad practice in my opinion).

If you need to call it twice, you must call it with a container for which region this bind is meant to.

Something like this:

ko.applyBindings(new YearViewModel(yearData), document.getElementById('YourYearViewElementId'));

The error you get is from the first binding, which tries to process the whole page and does not find the 'ageView' in its ViewModel.

Better would be if you build a single ViewModel for a single Page where you have sub-models for sections if needed.

Some pseudo code for such a scenario:

var Section1ViewModel = function() {
    var self = this;

    self.property1 = ko.observable();
    self.myComputed = ko.computed(function () {
        // do some fancy stuff
    });
    self.myFunc = function() {
        // do some more fancy stuff
    };
}

var Section2ViewModel = function() {
    var self = this;

    self.property1 = ko.observable();
    self.myComputed = ko.computed(function () {
        // do some fancy stuff
    });
    self.myFunc = function() {
        // do some more fancy stuff
    };
}

var PageViewModel = function() {
    var self = this;

    self.section1 = ko.observable(new Section1ViewModel());
    self.section2 = ko.observable(new Section2ViewModel());

    self.myGlobalFunc = function() {
        // do some even more fancy stuff
    }
}

ko.applyBindings(new PageViewModel());

Hope that helps.

Best regards, Chris

Angelika answered 15/4, 2014 at 13:56 Comment(2)
I've gone with option A but appear to have run into another hurdle. I now want to use one of the values inside fetchPopulation which now gets binded to #populationVM, outside of #populationVM - is this possible?Septic
It's a bit difficult without an example. But if you go with seperated ViewModels, then you should not refer to each other (for this, I would suggest to use the second option, use one VM per page). But what you could do (it's very hacky!): define the constructed ViewModels as Window Variables, so you can access them over window.xxx (example: window.yearModel = new YearViewModel(yeardata); and then: window.yearModel.yearView() ...Angelika

© 2022 - 2024 — McMap. All rights reserved.