Search code examples
javascriptjqueryknockout.jsknockout-sortable

Cross-updating issue with knockout


I have an "cross-update" issue with knockout. The following function ('self.applyTagsAllocation') causes some strange effect. Line 3: 'scenetag.sceneID = scene.sceneID;' is updating 'tag.sceneID' as well as the local scenetag-variable. I really cannot understand why as sceneID never is an observable.

self.applyTagsAllocation = function (tag) {
        var  scenetag = tag;
        var scene = self.selectedScene();
        scenetag.sceneID = scene.sceneID;  
        scene.sceneTags.push(scenetag);
};

Here´s a more complete listing of my viewmodel:

var Tag = function (data) {
    var self = this;
    self.ID = data.ID || -1;      
    self.sceneID = data.sceneID;
    self.text = ko.observable((data.text || 'new').trim()); 
    self.tagType = ko.observable(data.tagType || -1 );
}

var Scene = function(data){
    var self = this;
    self.sceneID = data.sceneID;
    self.sceneTags = ko.observableArray();

    self.tags = ko.computed({
        read: function () {
            var tags = [];
            tags.push.apply(tags, self.sceneTags());
            return tags;
        },
        write: function (tag) {
            this.sceneTags.push(tag);
        },
        owner: self
    });
};

var ViewModel = function (model){
    var self = this;

    self.selectedScene = ko.observable();
    self.sceneTags = ko.observableArray();

    self.Scenes = ko.observableArray(
        ko.utils.arrayMap(model, function (item) {
            return new Scene(item);
        }));

    self.sceneTags = ko.computed(function () { 
        var tags = [];
        ko.utils.arrayForEach(self.Scenes(), function (scene) {
            tags.push.apply(tags, scene.tags());   
        });
        return tags;
    });


    //Tag is first created with:
    self.addSceneTag = function (name, type) {   
        if (!type) type = -1;
        var newtag = new Tag({
            ID: -1,
            sceneID: self.selectedScene().sceneID,
            text: name,    
            tagType: type
        });

        // already in use?
        var abort = false;
        ko.utils.arrayForEach(self.selectedScene().sceneTags(), function (tag) {
            if (tag.text() === newtag.text()) abort = true;
        });
        if (!abort) self.selectedScene().sceneTags.push(newtag);
    };

    self.applyTagsAllocation = function (tag) {
        var  scenetag = tag;
        var scene = self.selectedScene();
        scenetag.sceneID = scene.sceneID;  
        scene.sceneTags.push(scenetag);
    };
};

(You might wonder why 'Scene' has both a sceneTags-array and a tags-array. This is because I have done a simplification in this example. In my project I have another tag-type and the two types are merged together in the viewmodel-scope.)

HTML

Tag is first set up with:

<input class="taginput"  placeholder="&lt;add a tag&gt;" data-bind="textInput: tagname, event: {keyup: $root.inputTag}" />

Then when tag exists I want to add it onto another ‘scene’ from the scope of «foreach: tag» with:

 <span data-bind="visible: sceneID  !== null &&  sceneID !== $root.selectedScene().sceneID, click: function(){$root.applyTagsAllocation($data)};">Add Tag</span>

The actual foreach here is a loop within the knockout-sortable (from Ryan Niemeyer):

<div class="tag-list" data-bind="sortable:{template:'tagsTmpl',data:$root.sceneTags"></div>

What can be causing this? Any hints and clues highly appreciated!

Thanx in advance!


Solution

  • Have another look at the function; in your view you bind the $data for a tag to the self.applyTagsAllocation function, which essentially does this =>

    self.applyTagsAllocation = function (tag) {
       var scenetag = tag; // scenetag == instance of Tag you passed ( $data )
       var scene = self.selectedScene(); // scene == the selected instance of Tag
       scenetag.sceneID = scene.sceneID;  //!!!! This line says:
       // 'Make sceneID of this instance = sceneID of the selected instance'
       scene.sceneTags.push(scenetag); // will fail, you can't write to a simple computed
    };
    

    So yes, it updates the local variable, but the local variable points to your Tag viewModel, and so it updates the object.. Unlike you might have (seemed to have) thought, assigning an object to another variable doesn't clone it; it is merely a reference.

    Rather do:

    self.applyTagsAllocation = function (tag) {
       var scenetag = {};
       // clone an object, for whatever strange reason one could have:
       for (var prop in tag) {
          scenetag[prop] = tag[prop];
       } // now you have a copy of the tag instance you passed [..] rest of function
    };
    

    Or if you wanted to set the tag as selected, which I would deem more logical:
    self.selectedScene(tag) in your function.

    Important: you have assigned self.sceneTags twice, and the second time it was a computed observable. You can't write to computed observables (while in applyTagsAllocation you push to it), then you need pureComputed..