Search code examples
javascriptknockout.jsknockout-mapping-plugin

Why knockout.mapping fails on some data-structures?


Using knockout.mapping I try to map from JSON to ko-object. When trying to use more advanced solutions with mapping options I often got Uncaught TypeError: g is not a function. I tried to figure out what is problem, but I am still confused.

Lets say I have pretty simple model:

var data1 = { // works
  a: "a",
  b: [ 
    { b1: "v1" }, 
    { b2: "v2" } 
  ] 
};

Now consuming this model works fine like this:

function viewModel ( data ) {
  var self = this;
  var a    = ko.mapping.fromJS( data, { observe: "a"} );

  return a;
}

var vm1 = viewModel( data1 ); // works

I have two properties (a and b) in my model, but I need only a to be observable, while b should just copied to the viewmodel.

Sadly it does not work so well, if data-structure gets deeper:

var data1 = { // works
  a: "a",
  b: [ 
    { b1: "v1" }, 
    { b2: "v2" } 
  ] 
};

var data2 = { // Uncaught TypeError: g is not a function
  a: "a",
  b: [ 
    { b1: { name: "v1" } }, 
    { b2: { name: "v2" } } 
  ]
};

var data3 = { // Uncaught TypeError: g is not a function
  a: "a",
  b1: { name: "v1" }, 
  b2: { name: "v2" } 
};

var data4 = { // works
  a: "a",
  b1: "v1", 
  b2: "v2"  
};

var data5 = { // works
  a: "a",
  b1: ["1::v1", "2::v1"], 
  b2: ["1::v2", "2::v2"]  
};

function viewModel ( data ) {
  var self = this;
  var a    = ko.mapping.fromJS( data, { observe: "a"} );

  return a;
}

var vm1 = viewModel( data1 ); // works
var vm2 = viewModel( data2 ); // Uncaught TypeError: g is not a function
var vm3 = viewModel( data3 ); // Uncaught TypeError: g is not a function
var vm4 = viewModel( data4 ); // works
var vm5 = viewModel( data5 ); // works

It seems baffling that copying some structures works and other not. I understand, that problem jumps in, when there is multilevel hash somewhere in the tree.

Is this desired behavior or do I something wrong here? Why copying some structure works while other not?


Solution

  • I've succesfully reproduced the issue you mentioned:

    var data1 = { // works
      a: "a",
      b: [ 
        { b1: "v1" }, 
        { b2: "v2" } 
      ] 
    };
    
    var data2 = { // Uncaught TypeError: g is not a function
      a: "a",
      b: [ 
        { b1: { name: "v1" } }, 
        { b2: { name: "v2" } } 
      ]
    };
    
    function viewModel ( data ) {
      var self = this;
      var a = ko.mapping.fromJS( data, { observe: ["a"]} );
      return a;
    }
    
    var vm1 = viewModel( data1 ); // works
    var vm2 = viewModel( data2 ); // Uncaught TypeError: g is not a function
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-debug.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>

    As you can see, in my repro I include the non-minified versions of both libs. I stepped into it on the error, and it's here, in this code:

    if(options.observe.length > 0 && ko.utils.arrayIndexOf(options.observe, fullPropertyName) == -1)
    {
        mappedRootObject[indexer] = value(); // <---- THIS LINE
        options.copiedProperties[fullPropertyName] = true;
        return;
    }
    

    That line struck me as odd, because typically with KO you need to explicitly need to "unwrap" values if you're unsure if they're a plain value or an observable.

    So I went to the relevant source code on Github, but to my surprise, the above code from the CDNJS version is not there, it's different:

    if(options.observe.length > 0 && ko.utils.arrayIndexOf(options.observe, fullPropertyName) == -1)
    {
        mappedRootObject[indexer] = ko.utils.unwrapObservable(value); // <-- DIFFERENT
        options.copiedProperties[fullPropertyName] = true;
        return;
    }
    

    That line is the way I'd expect it to be.

    Then again, the most recent debug build has the buggy/first version again.

    Then some more digging, I see there's issue #137 with exactly this problem. That issue ends with the same conclusion I reached: the fix is not included in the 2.4.1 build. There will not likely soon be new builds: as you can see in the project's readme this project is currently not actively being maintained.

    Bottom line: you'll need to either work around this situation, or create your own custom build of mapping with this problem fixed.