1

I am new to Knockout and I am building a Simple POC for using knockout to build SPA(Single Page Application).

What I want to do is to show "Business Units" when the app loads and on selection of a business unit show all "Front End Units" under that business unit and on selection of a front end unit, show all "Sales Segments" under that front end unit.

All this will happen in a single page using the same view and the viewmodel will bind the model based on selected business unit or front end unit.

The issue I am facing is that, I have 5 business units that get bound properly first on document ready, but on selection of business unit, the front end units get repeated 5 times each. In this case, I have 2 front end units and each is shown 5 times. Same issue on selection of front end unit.

You can see this issue mimicked in the following jsFiddle sample - jsFiddle Link

Let me know if you can't access the jsfiddle link. In this sample, I have used arrays, but in actual I will be getting the data through async call to the oData service.

This is the view HTML:

<div id="divbu">
    <h4 data-bind="text: Heading"></h4>
    <ul data-role="listview" data-inset="true" data-bind="foreach: Collection">
        <li data-role="list-divider" data-bind="text: EntityName"></li>
                <li>
                    <a href="#" data-bind="click: $root.fnNextLevel">
                        <table border="0">
                            <tr>
                                <td>
                                    <label style="font-size: 12px;">Bus. Plan: </label>
                                </td>
                                <td>
                                    <label style="font-size: 12px;" data-bind="text: BusinessPlan"></label>
                                </td>
                                <td>
                                    <label style="font-size: 12px;">Forecast: </label>
                                </td>
                                <td>
                                    <label style="font-size: 12px;" data-bind="text: Forecast"></label>
                                </td>
                            </tr>
                            <tr>
                                <td>
                                    <label style="font-size: 12px;">Gross Sales: </label>
                                </td>
                                <td colspan="3">
                                    <label style="font-size: 12px;" data-bind="text: GrossSales"></label>
                                </td>
                            </tr>
                        </table>
                    </a>
                </li>
            </ul>
</div>

This is the model and view model:

function CommonModel(model, viewType) {
 var self = this;
 if (viewType == 'BU') {
     self.EntityName = model[0];
     self.BusinessUnit = model[0];
     self.BusinessPlan = model[1];
     self.Forecast = model[2];
     self.GrossSales = model[3];
 } else if (viewType == 'FEU') {
     self.EntityName = model[1];
     self.BusinessUnit = model[0];
     self.FrontEndUnit = model[1];
     self.BusinessPlan = model[2];
     self.Forecast = model[3];
     self.GrossSales = model[4];
 } else if (viewType == 'SS') {
     self.EntityName = model[2];
     self.BusinessPlan = model[3];
     self.Forecast = model[4];
     self.GrossSales = model[5];
 }

}

function ShipmentReportsViewModel(results, viewType) {

 var self = this;
 self.Collection = ko.observableArray([]);

 for (var i = 0; i < results.length; i++) {
     self.Collection.push(new CommonModel(results[i], viewType));
 }
 if (viewType == 'BU') {
     self.Heading = "Business Units";
     self.fnNextLevel = function (businessUnit) {
         FetchFrontEndUnits(businessUnit);
     };
     self.Home = function () {
         FetchBusinessUnits();
     };

 } else if (viewType == 'FEU') {

     self.Heading = results[0][0];
     self.fnNextLevel = function (frontEndUnit) {
         FetchSalesSegments(frontEndUnit);
     };
     self.Home = function () {
         FetchBusinessUnits();
     };
 } else if (viewType == 'SS') {
     self.fnNextLevel = function () {
         alert('No activity zone');
     };
     self.Heading = results[0][0] + ' - ' + results[0][1];
     self.Home = function () {
         FetchBusinessUnits();
     };
 }

}

You can see the complete code in the jsFiddle link.

I have also tried this with multiple views and multiple view models, where I apply bindings by giving the element ID. In this case, one flow from business unit -> sales segment is fine, but when I click on home or back button and I do binding again to that element, I face the same issue. (home and back button features are not done in jsFiddle example).

Let me know if more details are required. I did look into lot of other links in stack overflow, but nothing addressing this particular problem.

Any help is deeply appreciated. Thanks in advance.

Shyam
  • 11
  • 3

2 Answers2

3

The problem here is that you call your ko.applybindings TWICE and there is a foreach binding that iterate within 5 items, therefore the data are duplicated five times.

you should not call a ko.applybindings more than once on the same model. Your model is always the same even if it's parametrized.

I had the same problem here: Data coming from an ObservableArray are displayed twice in my table

the fact that you have you business logic inside your viewModel is something that could be discussed, and it makes it not easy to fix this.

Make 3 classes, put them in a common model without logic inside. Then once you have applyed the ko.applyBindings once, you just have to modify the array like this:

viewModel.myArray(newValues)

Here is the fiddle with the amended code: http://jsfiddle.net/MaurizioPiccini/5B9Fd/17/ it does not do exaclty what you need but if remove the multiple bindings by moving the Collection object scope outside of your model. As you can see the problem IS that you are calling the ko.applybindings twice on the same model.

Community
  • 1
  • 1
Maurizio In denmark
  • 4,226
  • 2
  • 30
  • 64
  • 1
    Yes it is. He has a data-bind="click: $root.fnNextLevel" inside a data-bind="foreach: Collection". If he apply the binding for a second time, this will cause the foreach to reiterate and because one binding is already applyed to all the 5 Collection elements they will be repeated 5 times. – Maurizio In denmark Jun 10 '13 at 11:31
  • It doesn't mean that `fnNextLevel` function is indeed called for every loop iteration (it doesn't). it only sets the `click` event handler to that function that will eventually call `applyBindings` (only once per click). – haim770 Jun 10 '13 at 11:35
  • 1
    Be careful here because you are not applying a multiple bindign, but you are applying a binding multiple times on the same model. You do not want to call ko.applyBindings multiple times on the same elements. Check the accepted answer on this thread: http://stackoverflow.com/questions/14045872/knockout-js-multiple-viewmodel-bindings-on-a-page-or-a-part-of-a-page – Maurizio In denmark Jun 10 '13 at 11:36
  • It only run once. that's not the problem. – haim770 Jun 10 '13 at 11:38
  • Sorry maybe I said it wrong, it does not call the function 5 times, but if you call the ko.applybindings TWICE, it will duplicate the data not only twice but for each of the element in the data-bind="foreach: Collection" – Maurizio In denmark Jun 10 '13 at 11:40
  • It's called once, but apply the ko.applybindings for the second time. That cause s the issue – Maurizio In denmark Jun 10 '13 at 11:41
  • That's the essence of the problem: why calling `applyBindings` once again with different `viewModel` doesn't replace the data but appends to it? – haim770 Jun 10 '13 at 11:43
  • I actually see only one model: ShipmentReportsViewModel. the fact the it dinamically change his properties doesn't make it a different model in memory – Maurizio In denmark Jun 10 '13 at 11:55
  • Thanks Maurizio. I do see the fiddle. – Shyam Jun 11 '13 at 11:25
  • I am experimenting with your solution, will keep you posted. – Shyam Jun 11 '13 at 11:26
  • @MaurizioIndenmark Sorry for the dealyed response. I did try with multiple classes (models) and multiple views as shown in this [jsFiddle](http://jsfiddle.net/rniemeyer/PctJz/) Seems like this would work when each view is independent of each other and you can easily apply the bindings in the beginning. But in my example, the 2nd view is dependent on what is selected in first and 3rd is dependent on what is selected in 2nd. So, I dont have the data to bind initially. You can see this been tried in this [jsFiddle](http://jsfiddle.net/shyamayana/5B9Fd/19/). – Shyam Jun 13 '13 at 08:28
  • @MaurizioIndenmark I also tried making things global as suggested. This solution works for most part as I am able to generate all 3 views, but the only problem is the fnNextLevel method for the front end unit view still calls the same function as in the first view when we click the first time, the second time I click it generates the 3rd view. I have modified my initial [jsFiddle](http://jsfiddle.net/shyamayana/5B9Fd/21/) to show this issue. Either I am missing something really basic or this scenario not possible in knockout I am not sure. Any insight further into this issue will be helpful. – Shyam Jun 13 '13 at 08:35
  • @MaurizioIndenmark Did you get a chance to look further into this issue? Thanks. – Shyam Jun 17 '13 at 08:48
  • Now I went to see the jfiddle and it seems to work? or maybe I lost track of what the issue was. :P – Maurizio In denmark Jun 20 '13 at 06:46
  • Have a look at the way they do it here: http://learn.knockoutjs.com/#/?tutorial=webmail. I think you need to simplify it to understand where the problem is. I suggest you get rid of the dynamic viewModel and just make a model for each of the levels you need. In that way you minimize the number of parameters passed as well. – Maurizio In denmark Jun 20 '13 at 07:17
0

Finally, I got this working. Thanks to @MaurizioIndenmark.

Though I have removed multiple call for ko.applybindings, I was still calling the view model multiple times. This was causing the issue.

Now, I have cleaner view model and I have different function calls for different actions and modify all the data required to be modified within these functions(events). Now, everything is working as expected.

This is how the view model looks now -

function ShipmentReportsViewModel(results) {
var self = this;
self.Heading = ko.observable();
self.BusinessUnits = ko.observableArray();
self.FrontEndUnits = ko.observableArray();
self.SalesSegments = ko.observableArray();

self.Home = function () {
    var bu = FetchBusinessUnits();
    self.Heading("Business Units");
    self.BusinessUnits(bu);
    self.FrontEndUnits(null);
    self.SalesSegments(null);
};
self.fnFeu = function (businessUnit) {
    var feu = FetchFrontEndUnits(businessUnit);
    self.Heading(feu[0].BusinessUnit);
    self.FrontEndUnits(feu);
    self.BusinessUnits(null);
    self.SalesSegments(null);
};
self.fnSalesSeg = function (frontEndUnit) {
    var ss = FetchSalesSegments(frontEndUnit);
    self.Heading(ss[0].BusinessUnit + ' - ' + ss[0].FrontEndUnit);
    self.SalesSegments(ss);
    self.BusinessUnits(null);
    self.FrontEndUnits(null);
};
self.Home();
}

To see the entire working solution, please refer this jsFiddle

Thanks for all the valuable suggestions in getting this work.

Shyam
  • 11
  • 3