Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

error on save ( while trying to parse server response ) #2

Open
nickperkinslondon opened this issue Mar 30, 2013 · 0 comments
Open

Comments

@nickperkinslondon
Copy link

I can not create a document and save it. It actually saves, but the "success" callback hits an error, and then I don't get my "success" ( or "error" ) result.

The problem is when it tries to call "model.parse" to get the server attributes after saving. The error is "model has no method 'parse'"

I have a very simple Kanso app which shows the problem.
This is the script on the page:

$ = window.$ = require 'jquery'
Backbone = window.Backbone = require 'backbone'
adapter = require 'backbone-adapter'

Backbone.db = '/field-agent'
Backbone.sync = adapter.sync

Quote = Backbone.Model.extend
    thing:'something'

window.onload = ->  
    q = new Quote()
    q.save {name:'Nick'},
        success:(a,b,c)->
            debugger
        error:(a,b,c)->
            debugger

Again, the document does actually save, but it hits an exception just after the comment:
// Ensure attributes are restored during synchronous saves.

Uncaught TypeError: Object # has no method 'parse'
The line "model.attributes = attributes" does not seem correct.
At the point "attributes" points to the same object as "model", and this creates a self-reference.
The "model", at this point, also has the given attribute (name=Nick) directly on the object, not under an "attributes" sub-object. I thought backbone kept its "data" in "attributes".

...debugging more....

the line:

options.success.apply(model,args)  // args[0] is the attributes, not the model

calls:

options.success = function(model,resp,options)

Which gets "attributes" instead of "model" in the first arg,...and "undefined" in the other 2 args. It then sets "model.attributes = attributes", which makes a complete mess of it...

Maybe the problem is the line

callback(null,model.attributes)

The callback itself only looks for (err), but it passes along its args to the next callback.

Maybe it should be something like:

callback(null,model,resp,options) 

( although "options" is not there at the time...but that is what the success callback later expects! )

@thaddeusalbers
Copy link

I had the same Uncaught TypeError: Object # has no method 'parse' error. I fixed it by modifying both backbone-adapter and backbone, so it isn't a patch per se.

Note: this is for the kanso packages:

  • db 0.1.0
  • backbone-adapter 0.0.1
  • backbone 0.9.10

Here's the changes and explanation of why.

Here's the calling stack when the options.success error appears...
ajax calls -> db onComplete -> backbone-adapter save -> backbone-adapter sync -> backbone.save -> error!

Let's skip the jquery ajax calls (which send the server request and get the server response) and start in db onComplete after a successful response.
db onComplete

/**
 * Returns a function for handling ajax responses from jquery and calls
 * the callback with the data or appropriate error.
 *
 * @param {Function} callback(err,response)
 * @api private
 */

function onComplete(options, callback) {
...
if (req.status === 200 || req.status === 201 || req.status === 202) {
            callback(null, resp);
        }
...

The callback is of the form callback(error, response)
with error = null, response = resp.
The server response, resp, will look like

{
  "ok":true,
  "id":"4d66bcc95c2e85abfc40efde7001658f",
  "rev":"1-37c08db5877e8470f1b1dd831e5d37fe"
}

backbone-adapter save
Next call is the save function in backbone-adapter. It uses the callback from onComplete above.

exports.save = function (db, model, callback) {

    db.saveDoc(model.attributes, function (err, resp) {
        if (err) {
            return callback(err);
        }
        model.attributes._id = resp.id;
        model.attributes._rev = resp.rev;
        callback(null, model.attributes);
    });
};

Here we put in the _id and _rev attributes from the server response eg. "id":"4d66bcc95c2e85abfc40efde7001658f","rev":"1-37c08db5877e8470f1b1dd831e5d37fe"
in the model.attributes.

The callback(error, response) is populated with error = null & response = model.attributes. Why is response set to model.attributes and not the server response (resp)? Because backbone expects a JSON representation of the model like

{ 
  _id: "4d66bcc95c2e85abfc40efde7001658f",
  _rev: "1-37c08db5877e8470f1b1dd831e5d37fe",
  bar: "24",
  createdAt: 1367955560200,
  foo: "something",
  name: "widget",
  updatedAt: 1367955560200
}

but the response from the server is

{
  "ok":true,
  "id":"4d66bcc95c2e85abfc40efde7001658f",
  "rev":"1-37c08db5877e8470f1b1dd831e5d37fe"
}

Using the true server response alone would overwrite our model with ok, id, & rev attributes only. It would cause our model data to disappear. And disappearing data is not cool.

backbone-adapter sync
Next call is the sync function in backbone-adapter. The arguments passed in are [null, model.attributes] for function (err). In javascript, you can pass in more arguments than it needs. The array arguments is the list of all arguments passed in.

    var callback = function (err) {
        if (err) {
            return options.error(model, err);
        }
        var args = Array.prototype.slice.call(arguments, 1);
        options.success.apply(model, args);
    };

This is where things start to go sideways...

The line

  var args = Array.prototype.slice.call(arguments, 1);

sets args equal to an "array" of size 1 with [model.attributes] as the contents. e.g. args.length = 1 and args[0] = model.attributes.
The line

   options.success.apply(model, args);

calls options.success with a "single" array as the argument. Why? .apply() takes an single array argument only. options.success.apply only uses the array "args" since it's the only array passed to it. model is not passed through because it's not an array and not part of the args array. (See https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/apply for apply vs call.) We will return to this later.

backbone.save
Lastly we end up at the options.success callback in backbone save.

      // After a successful server-side save, the client is (optionally)
      // updated with the server-side state.
      if (options.parse === void 0) options.parse = true;
      success = options.success;
      options.success = function(model, resp, options) {
        // Ensure attributes are restored during synchronous saves.
        model.attributes = attributes;
        var serverAttrs = model.parse(resp, options);
        if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
        if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
          return false;
        }
        if (success) success(model, resp, options);
      };

The issue is that options.success is passed only one argument (model.attributes) when it is expecting (model, resp, options). Model isn't passed in all (see "where things go sideways" above). When model.parse is called, it cannot find it since it's calling model.attributes.parse, which doesn't exist. Hence the parse error.

Fix
So how do we fix this?
First we add a line to backbone save

  var model = this;

like so

  if (options.parse === void 0) options.parse = true;
  var model = this;
  success = options.success;

Why can we do this? The model instance we are in extends backbone, so this is the model. In other words, the backbone save function is inside this (a model).
In backbone 1.0.0 this change is already made. (see http://documentcloud.github.io/backbone/docs/backbone.html )

Without needing to pass in the model, we can change in backbone

  options.success = function(model, resp, options) {

to

options.success = function(resp, options) {

and in backbone-adapter

  options.success.apply(model, args); 

to

options.success.apply(args);

options is already set in the model, so we don't need to pass this in either. Droping options in backbone...

options.success = function(resp, options) {

becomes

options.success = function(resp) {

In backbone 1.0.0 this change is also made.

Ta da!

So short version,
change in backbone.save

  if (options.parse === void 0) options.parse = true;
  success = options.success;
  options.success = function(model, resp, options) {

to

  if (options.parse === void 0) options.parse = true;
  var model = this;
  success = options.success;
  options.success = function(resp) {

and change in backbone-adapter

  options.success.apply(model, args);

to

  options.success.apply(args);

I haven't tested this in all the scenarios yet (only save & sync) and it may require other modifications in backbone and backbone-adapter to cover the other scenarios. But it should fix the "Uncaught TypeError: Object # has no method 'parse'" error.
In the future, when backbone moves to 1.0.0, only the change in backbone-adapter will be needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants