0
votes

I have a small, simple view & viewmodel that displays some numbers and a chart. I was asked to provide a "side by side" view whereby up to 6 instances of this would be shown side by side.

Somewhat naively, it would appear, I just did this in my view:

<div class="container acd">
    <div class="row">
        <div class="col-md-2">
        <div data-bind="compose: { model: acd1}"></div>
        </div>
        <div class="col-md-2">
        <div data-bind="compose: { model: acd2 }"></div>
        </div>
        <div class="col-md-2">
        <div data-bind="compose: { model: acd3 }"></div>
        </div>
        <div class="col-md-2">
        <div data-bind="compose: { model: acd4 }"></div>
        </div>
        <div class="col-md-2">
        <div data-bind="compose: { model: acd5 }"></div>
        </div>
        <div class="col-md-2">
        <div data-bind="compose: { model: acd6 }"></div>
        </div>
    </div>
 </div>

and this in the viewmodel:

var acd1 = new acd({ header: 'Article 1'});
var acd2 = new acd({ header: 'Article 2'});
var acd3 = new acd({ header: 'Article 3'});
var acd4 = new acd({ header: 'Article 4'});
var acd5 = new acd({ header: 'Article 5'});
var acd6 = new acd({ header: 'Article 6'});

...where "acd" is the reference "required" in my viewmodel that links to the standalone view & viewmodel.

This seemed to work at first and I had 6 instances side by side on the page with different headers as set above.

However when I came to actually fire off some loading events whereby a section is data-bound to an observable that controls whether it's visible or not, it seems that there is not enough separation of instances as changing a dropdown in the first instance actually reveals the section in the 6th section!

When I inspect the elements on the page through chrome dev tools, all the div IDs are the same, so I imagine that there's a fundamental problem with what I'm trying to achieve here? I thought that by creating "new" instances in the master page it would create standalone instances that were self-aware so that when passed parameters/updated, they would know to refer to their own instances of IDs but it would appear not.

Anyone got any pointers to try and help me out here please?

The master page viewmodel:

define(['services/datacontext', 'viewmodels/articleComparisonDetail'], function (datacontext, acd) {

    var acd1 = new acd({ header: 'Article 1'});
    var acd2 = new acd({ header: 'Article 2'});
    var acd3 = new acd({ header: 'Article 3'});
    var acd4 = new acd({ header: 'Article 4'});
    var acd5 = new acd({ header: 'Article 5'});
    var acd6 = new acd({ header: 'Article 6'});

    var acdMaster = {
        acd1: acd1,
        acd2: acd2,
        acd3: acd3,
        acd4: acd4,
        acd5: acd5,
        acd6: acd6
    };

    return acdMaster;

});

To add more info, I've put a debug stop point on the "var acdMaster =..." line and at that moment, acd1-6 appear to be separate and independent viewmodels with the "header" property set to the 6 separate values I used. Somehow though the input boxes in the individual details views don't seem to be linked up to the specific instance.

The acd "detail" view (simplified):

      <div id="acdWrapper">
        <section id="acd">
            <div class="acdArticleCode" data-bind="text: header"></div>
                <div class="formGrid">
                    <input autocomplete="off"  data-bind="typeahead: { name: 'sectionNames', highlight: true, source: articleList}, value: artCode""/>
                </div>

                <!-- ko if: articleLoaded() == true -->
                <div id="articleSelected" data-bind="visible: articleLoaded()">
                    <div class="formGrid resultsTable">
                        <div class="formRow">
                            <div class="formCell">
                                <label>Article Weight:</label>
                            </div>
                            <div class="formCell text-right">
                                <span data-bind="numericText: loadedArticle().totalArticleWeight, precision: 2"></span>
                            </div>
                        </div>


                 ...


         </div>
        <!-- /ko -->
         <div id="articleNotSelected" data-bind="visible: !articleLoaded()">

            <p>Type a minimum of 2 characters into text box to see list of articles</p>

        </div>
 </section>

...and viewmodel (also simplified)

define(['plugins/dialog', 'knockout', 'config', 'services/datacontext'], function (dialog, ko, config, datacontext) {

    var acdvm = function (params) {
        var self = this;
        self.content = ko.observable();
        self.header = ko.observable(params.header);
        self.loadedArticle = ko.observable(); //contains entity object for display
        self.articleLoaded = ko.observable(false); //is article loaded true/false
        self.articleList = ko.observableArray([]); //holds list of matching articles from search
        self.selectedRow = ko.observable('e1');
        self.chartID = ko.computed(function () {
            var str = self.header() + "_chart";
            return str.replace(" ","");
        }, this, { deferEvaluation: true });

        self.displayPrecision = ko.observable(6);
        self.artCode = ko.observable('');

    };

    return acdvm;
});

EDIT 2 So I investigated further. I added:

function externalActivate(fakeself) {
    if (fakeself.header() == "Article 2") { fakeself.blah("I blow") };
}

var acdvm = function (args) {
    var self = this;
    self.blah = ko.observable('blah');

    self.activate = function () {
        externalActivate(self);
        if(self.header()=="Article 5") {self.blah("Really Sucks")};
    };

... rest unchanged
}

return acdvm;

And altered the view to display blah on each of the 6 views I instantiate. In my master viewmodel I added:

    acd3.blah("sucks");

to the module's "activate" method and all of it worked exactly as expected.

On the 6 columns on screen I get "blah", "I blow", "sucks", "blah", "Really sucks", "blah" which is what I need. Yet if I use the input field on the first column/viewmodel to select something then as soon as the viewmodel loads up data and changes the observables, triggering the html in that view to be "filled in" then it appears in the 6th column, not the first!

This is insane... I truly have no idea how this can be happening.

2
I guess acd1-6 should be part of your main viewmodel? I don't see your viewmodel here, but I think you need to do rather this.acd1 = new acd({...});GôTô
Edited to add viewmodel. Still stumped by this. It appears that only the last created instance of my detail viewmodel is retained so that somehow anything that is referenced elsewhere seems linked to that alone.TheMook
You need to return a constructor function instead of an instance of the object (view model in this case) search the internet it is a fairly common practicePW Kad
I thought that was what I was doing :-( Each "detail" viewmodel should stand on its own. It doesn't need any parameters passing to it (not even the header really, I was doing that to see if it would work) and then it has its own functions and variables within that don't need to interact with anything else in the parent or other children. Surely my var acdvm = function(args) {... return acdvm code IS returning a constructor? I'm confused... (as usual)TheMook
Added further detail to first post under "Edit 2". I'm completely lost, everything appears to work perfectly until Knockout comes to fill in the html fields.TheMook

2 Answers

0
votes

More information rather than an answer.

According to the Composition docs, if you pass a module ID to the compose binding it will "Locate the module, locate its view, bind them and inject them into the DOM." . Whereas passing an object instance it will "Locate its view, bind it and inject them into the DOM.". Maybe there's subtle difference here? I'm not too familiar with the internals of Durandal.

An alternative idea is instead of binding to the properties of a master viewmodel, you could try this instead:

<div class="container acd">
    <div class="row">
        <div class="col-md-2">
            <div data-bind="compose: 'viewmodels/articleComparisonDetail', activationData: { header: 'Article1' }"></div>
        </div>
        <div class="col-md-2">
            <div data-bind="compose: 'viewmodels/articleComparisonDetail', activationData: { header: 'Article2' }"></div>
        </div>
        <div class="col-md-2">
            <div data-bind="compose: 'viewmodels/articleComparisonDetail', activationData: { header: 'Article3' }"></div>
        </div>
        <div class="col-md-2">
            <div data-bind="compose: 'viewmodels/articleComparisonDetail', activationData: { header: 'Article4' }"></div>
        </div>
        <div class="col-md-2">
            <div data-bind="compose: 'viewmodels/articleComparisonDetail', activationData: { header: 'Article5' }"></div>
        </div>
        <div class="col-md-2">
            <div data-bind="compose: 'viewmodels/articleComparisonDetail', activationData: { header: 'Article6' }"></div>
        </div>
    </div>
 </div>

Bear in mind you'll have to refactor your articleComparisonDetail viewmodel to this:

 var vm = {
    var self = this;

    self.header = ko.observable();

    self.activate = function (data) {
        self.header(data.header); // or whatever
    }

    // rest of code...
 }

 return vm;
0
votes

It turned out that all I had done was fine except for...

A custom binding handler that I had embedded in the "detail" viewmodel (I didn't include this in the code above as I didn't think it relevant - how wrong I was).

All I did to fix it was firstly move the bindinghandler outside of the viewmodel, then I added the 4th parameter on bindinghandler initialisation (which is the viewmodel) so that it knew which instance of the vm had called it.

Within the bindinghandler where I made calls to populate observables and display a chart I just referred to the "vm" param I now included on initialisation instead of "self.whatever" and bingo, everything was peachy again.

So it seems I had set up my views/models correctly (as I suspected from the tests) and that it was just an incorrect "self" within the custom binding handler that was the culprit.

Thanks for the input chaps, was useful reading.