Coder Social home page Coder Social logo

afeld / backbone-nested Goto Github PK

View Code? Open in Web Editor NEW
445.0 18.0 83.0 9.88 MB

A plugin to make Backbone.js keep track of nested attributes - looking for maintainers! https://github.com/afeld/backbone-nested/issues/157

Home Page: https://afeld.github.com/backbone-nested/

License: MIT License

JavaScript 95.23% HTML 4.77%
backbone nested-attributes backbone-models

backbone-nested's Introduction

Backbone-Nested Build Status

A plugin to make Backbone.js keep track of nested attributes. Download the latest version and see the changelog/history/release notes on the Releases page. Supports Backbone 0.9.x and 1.x.

The Need

Suppose you have a Backbone Model with nested attributes, perhaps to remain consistent with your document-oriented database. Updating the nested attribute won't cause the model's "change" event to fire, which is confusing.

var user = new Backbone.Model({
  name: {
    first: 'Aidan',
    last: 'Feldman'
  }
});

user.bind('change', function(){
  // this is never reached!
});

user.get('name').first = 'Bob';
user.save();

Wouldn't it be awesome if you could do this?

user.bind('change:name.first', function(){ ... });

Installation

Recommended.

  1. Install the latest version:

    bower install backbone backbone-nested-model jquery underscore --save
  2. Add backbone-nested.js to your HTML <head>:

    <!-- must loaded in this order -->
    <script type="text/javascript" src="/bower_components/jquery/jquery.js"></script>
    <script type="text/javascript" src="/bower_components/underscore/underscore.js"></script>
    <script type="text/javascript" src="/bower_components/backbone/backbone.js"></script>
    <script type="text/javascript" src="/bower_components/backbone-nested-model/backbone-nested.js"></script>

Manual

Download the latest release and the dependencies listed above, then include with script tags in your HTML.

Usage

  1. Change your models to extend from Backbone.NestedModel, e.g.

    var Person = Backbone.Model.extend({ ... });
    
    // becomes
    
    var Person = Backbone.NestedModel.extend({ ... });
  2. Change your getters and setters to not access nested attributes directly, e.g.

    user.get('name').first = 'Bob';
    
    // becomes
    
    user.set({'name.first': 'Bob'});

Best of all, Backbone.NestedModel is designed to be a backwards-compatible, drop-in replacement of Backbone.Model, so the switch can be made painlessly.

Nested Attributes

get() and set() will work as before, but nested attributes should be accessed using the Backbone-Nested string syntax:

1-1

// dot syntax
user.set({
  'name.first': 'Bob',
  'name.middle.initial': 'H'
});
user.get('name.first') // returns 'Bob'
user.get('name.middle.initial') // returns 'H'

// object syntax
user.set({
  'name': {
    first: 'Barack',
    last: 'Obama'
  }
});

1-N

// object syntax
user.set({
  'addresses': [
    {city: 'Brooklyn', state: 'NY'},
    {city: 'Oak Park', state: 'IL'}
  ]
});
user.get('addresses[0].state') // returns 'NY'

// square bracket syntax
user.set({
  'addresses[1].state': 'MI'
});

Events

"change"

"change" events can be bound to nested attributes in the same way, and changing nested attributes will fire up the chain:

// all of these will fire when 'name.middle.initial' is set or changed
user.bind('change', function(model, newVal){ ... });
user.bind('change:name', function(model, newName){ ... });
user.bind('change:name.middle', function(model, newMiddleName){ ... });
user.bind('change:name.middle.initial', function(model, newInitial){ ... });

// all of these will fire when the first address is added or changed
user.bind('change', function(model, newVal){ ... });
user.bind('change:addresses', function(model, addrs){ ... });
user.bind('change:addresses[0]', function(model, newAddr){ ... });
user.bind('change:addresses[0].city', function(model, newCity){ ... });

"add" and "remove"

Additionally, nested arrays fire "add" and "remove" events:

user.bind('add:addresses', function(model, newAddr){ ... });
user.bind('remove:addresses', function(model, oldAddr){ ... });

Special Methods

add()

Acts like set(), but appends the item to the nested array. For example:

user.get('addresses').length; //=> 2
user.add('addresses', {
  city: 'Seattle',
  state: 'WA'
});
user.get('addresses').length; //=> 3

remove()

Acts like unset(), but if the unset item is an element in a nested array, the array will be compacted. For example:

user.get('addresses').length; //=> 2
user.remove('addresses[0]');
user.get('addresses').length; //=> 1

See also

Note, this plugin does not handle non-embedded relations (a.k.a. relations), which keeps it relatively simple. If you support for more complex relationships between models, see the Backbone plugin wiki page. There is also an open discussion about merging this project with backbone-deep-model.

Contributing

Pull requests are more than welcome - please add tests, which can be run by opening test/index.html. They can also be run from the command-line (requires PhantomJS):

$ npm install
$ grunt

See also: live tests for latest release.

backbone-nested's People

Contributors

afeld avatar bartek avatar eddieantonio avatar fcamblor avatar gkatsev avatar isakb avatar karlwestin avatar lovetheidea avatar michaelcox avatar p3drosola avatar trevorquinn avatar yepitschunked avatar zincli avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

backbone-nested's Issues

don't force full property path when setting a nested property

Hi, is there a way to avoid specifying the full path to the nested property?

For example, I would like something like this to work:

var model = new Backbone.NestedModel();
var nested = new Backbone.NestedModel();
nested.set('foo', 'bar');
model.set('nested', nested);

model.get('nested').get('foo'); // returns 'bar'

model.get('nested.foo') // returns undefined :(

I prefer not to set the full path every time, like model.set('nested.foo', 'bar'), because often times I will have independent modules that are adding their own models without knowing the full path of where they're being added.

Uncaught TypeError: Object function (){e.apply(this,arguments)} has no method 'walkPath'

If i create a NestedModel within my models.js, anytime I attempt to update the model through code, I receive the above error. My model is instantiated like so:

var Special = Backbone.NestedModel.extend({
    initialize : function () {
        // Log the changed properties
        this.on('change', function (model, options) {
            for ( var i in options.changes)
                this.display();
            updateSession(model);
        });
        // Update session var
        this.updateSession(this);
    },
    //Attributes
    defaults : function () {
        return {
            "Product" : null,
            "ShortDescription" : null,
            "Category" : "food",
            "Price" : {
                "Regular" : null,
                "Special" : null,
                "PercentOff" : null
            },
            "Date" : {
                "StartTime" : null,
                "EndTime" : null,
                "HumanTimeRange" : null
            },
            "Uses" : 0,
            "Tags" : [],
            "Contributor" : null
        }
    },

    //Functions
    display : function (property) {
        console.log(property + ': ' + this.get(property));
    },
    displayAll : function () {
        console.log('Product: ' + this.get('Product'));
        console.log('ShortDescription: ' + this.get('ShortDescription'));
        console.log('Category: ' + this.get('Category'));
        console.log('Price: {');
        console.log('  Regular: ' + this.get('Price.Regular'));
        console.log('  Special: ' + this.get('Price.Special'));
        console.log('  PercentOff: ' + this.get('Price.PercentOff') + '\n}');
        console.log('Date: {');
        console.log('  StartTime: ' + this.get('Date.StartTime'));
        console.log('  EndTime: ' + this.get('Date.EndTime'));
        console.log('  HumanTimeRange: ' + this.get('Date.HumanTimeRange') + '\n}');
        console.log('Uses: ' + this.get('Uses'));
        console.log('Tags: ' + this.get('Tags'));
        console.log('Contributor: ' + this.get('Contributor'));
    },
    updateSession : function (model) {
        Session.set('NewSpecial', model);
    }
});

// Empty special to hold user entered data pertaining to new special creation
NewSpecial = new Special();

If i attempt to modify the contents with NewSpecial.set('Category', 'travel') I get the error. If i copy the same exact code but instantiate the model in the console, everything works fine. I'm using the backbone-nested.js as a smart package in Meteor (I packaged it myself and will upload it to Atmosphere for community use shortly).

default values as functions

It would be nice to have the possibility to allow default: {} to contain functions (also in nested properties).

For example:

var myModel = Backbone.NestedModel.extended({
    id: '',
    properties: {
        weidth: function(){
            return <some_code_here>;
        }
    }
});

Nested sets that fail validation still trigger change events and are added to changedAttributes

Set operations on nested model attributes that fail validation are triggering change events in backbone-nested, and I don't think that's how it's intended to work in Backbone. If you try invalid set operations on regular attributes, they do not trigger change events, and I think it should be the same for nested attributes.

Also, it looks like invalid sets on nested attributes are updating the changedAttributes for the model and these are not cleared out when future change events are fired. This is affecting integration with other libraries like Backbone.ModelBinding.

I think there is something that runs in model sets before the this.changed is altered (in the normal Backbone Model class), which Backbone-Nested, in handling nested properties, does not do:

if (!this._validate(attrs, options)) return false;

The unit test below should demonstrate this:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta name="generator" content="HTML Tidy for Windows (vers 14 February 2006), see www.w3.org">
        <script src="http://code.jquery.com/jquery-latest.js" type="text/javascript"></script>
        <link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen">
        <script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
        <script type="text/javascript" src="http://underscorejs.org/underscore.js"></script>
        <script type="text/javascript" src="http://documentcloud.github.com/backbone/backbone.js"></script>
        <script type="text/javascript" src="https://raw.github.com/afeld/backbone-nested/master/backbone-nested.js"></script>

 <script type="text/javascript">
$(document).ready(function(){


    module("Backbone-Nested testing", {

        setup: function() {

            // Top level parent model with nested stuff
            this.ParentBBModel = Backbone.NestedModel.extend({
                validate : function(attributes) {
                    if (attributes.firstLevelChild.firstLevelChildAttrA.length < 25) {
                        return "firstLevelChild.firstLevelChildAttrA must be at least 25 characters";
                    } else if (attributes.parentAttrA.length < 16) {
                        return "parentAttrA must be at least 16 characters";
                    }
                }
            });
            this.parentBBModel = new this.ParentBBModel({
                parentAttrA : "parentAttrAValue",
                firstLevelChild : {
                    firstLevelChildAttrA : "firstLevelChildAttrAValue",
                    firstLevelChildAttrB : "firstLevelChildAttrBValue",
                    secondLevelArray : [
                        {
                            secondLevelArrObjAttrA : "secondLevelArrObj0AttrAValue",
                            secondLevelArrObjAttrB : "secondLevelArrObj0AttrBValue"
                        },
                        {
                            secondLevelArrObjAttrA : "secondLevelArrObj1AttrAValue",
                            secondLevelArrObjAttrB : "secondLevelArrObj1AttrBValue"
                        },
                        {
                            secondLevelArrObjAttrA : "secondLevelArrObj2AttrAValue",
                            secondLevelArrObjAttrB : "secondLevelArrObj2AttrBValue"
                        }
                    ]
                },
                firstLevelArray : [
                    {
                        firstLevelArrObjAttrA : "firstLevelArrObj0AttrAValue",
                        firstLevelArrObjAttrB : "firstLevelArrObj0AttrBValue"
                    },
                    {
                        firstLevelArrObjAttrA : "firstLevelArrObj1AttrAValue",
                        firstLevelArrObjAttrB : "firstLevelArrObj1AttrBValue"
                    },
                    {
                        firstLevelArrObjAttrA : "firstLevelArrObj2AttrAValue",
                        firstLevelArrObjAttrB : "firstLevelArrObj2AttrBValue"
                    }
                ]
            });
        },

        teardown: function() {

        }

    });

    test("change should not be triggered for invalid updates to regular attributes", function() {
        var changeCallbackCalled = false;

        this.parentBBModel.bind('change:parentAttrA', function(model, errors) {
            changeCallbackCalled = true; 
        });

        // Change a nested attribute to invalid value
        this.parentBBModel.set({"parentAttrA" : "tooShort"});   

        // Verify there was no change made
        equal( this.parentBBModel.get("parentAttrA"), "parentAttrAValue");

        // The change event should not be triggered because the attribute was never changed (it failed validation)
        ok(!(changeCallbackCalled));
    });

    test("change should not be triggered for invalid updates to nested attributes", function() {
        var changeCallbackCalled = false;

        this.parentBBModel.bind('change:firstLevelChild.firstLevelChildAttrA', function(model, errors) {
            changeCallbackCalled = true; 
        });

        // Change a nested attribute to invalid value
        this.parentBBModel.set({"firstLevelChild.firstLevelChildAttrA" : "tooShort"});  

        // Verify there was no change made
        equal( this.parentBBModel.get("firstLevelChild.firstLevelChildAttrA"), "firstLevelChildAttrAValue");

        // The change event should not be triggered because the attribute was never changed (it failed validation)
        ok(!(changeCallbackCalled));
    });

    test("changedAttributes should not include changes from previous change events, especially those that failed validation", function() {
        var changeCallbackCalled = false;

        this.parentBBModel.bind('change:parentAttrA', function(model, errors) {
            changeCallbackCalled = true;
            // Changed attributes should only include parentAttr because firstLevelChild.firstLevelChildAttrA 
            // failed validation and in any case was part of a previous event
            equal( model.changedAttributes(), {"parentAttrA" : "parentAttrAValueChanged"}  ); 
        });

        // Change a nested attribute to invalid value
        this.parentBBModel.set({"firstLevelChild.firstLevelChildAttrA" : "tooShort"});  

        // Verify there was no change made
        equal( this.parentBBModel.get("firstLevelChild.firstLevelChildAttrA"), "firstLevelChildAttrAValue");

        // Change another attribute to a valid value
        this.parentBBModel.set({"parentAttrA" : "parentAttrAValueChanged"});
        ok(changeCallbackCalled);
    });
});
 </script>

    <title></title>
    </head>
    <body>
        <h1 id="qunit-header">
            Backbone-Nested Test
        </h1>
        <h2 id="qunit-banner"></h2>
        <div id="qunit-testrunner-toolbar"></div>
        <h2 id="qunit-userAgent"></h2>
        <ol id="qunit-tests"></ol>
        <!-- THE FIXTURE -->
        <div id="qunit-fixture"></div>
    </body>
</html>

Stackoverflow Error With Large Objects

When using the set method I keep getting a stackoverflow error. The object contains multi-dimensional arrays. It'll be nice either skip the change events or optimize it.

Check for type missmatch in values

An error will be raised in the given situation:

Model:

defaults: {
    foo: null,
}

Code:

this.model.add('foo', {bar: 'crash'});

Issue: Uncaught Error: current value is not an array

easy adding to nested arrays

Would be nice to be able to add items to nested arrays, without having to know destination index. Possible syntax:

user.set({
  'addresses[]': {
    city: 'Seattle',
    state: 'WA'
  }
});

Escape Method Not Implemented

From Backbone Docs:

escapemodel.escape(attribute)
Similar to get, but returns the HTML-escaped version of a model's attribute. If you're interpolating data from the model into HTML, using escape to retrieve attributes will prevent XSS attacks.

Remove doesn't seem to work

I might not be doing this correctly but I have a test case here that seems to fail removing the item

Campaigner = Backbone.NestedModel.extend({
  defaults: function(){
    return {
      campaign_notes: [{body: 'test'}, {body: 'test2'}, {body: 'test3'}, {body: 'test4'}]
    };
  }
});

campaign = new Campaigner();
console.log(campaign.get('campaign_notes'));
campaign.remove('campaign_notes[1]');
console.log(campaign.get('campaign_notes'));

And here is the output:

[Object { body="test"}, Object { body="test2"}, Object { body="test3"}, Object { body="test4"}]
[Object { body="test"}, [Object { body="test"}, Object { body="test3"}, Object { body="test4"}], Object { body="test4"}]

Seems like it is pushing the spliced array back into the element that I wanted to delete. Other than that all my tests seem to have worked well.

change events for attributes in nested arrays

Would be nice for a special change event to be fired when a specific attribute is changed in any item in a nested array, e.g.

user.bind('change:addresses[].city', function(){
  // user changed one (or more) of their cities
});
user.set({'addresses[1].city': 'New York'});

Events from model.add are triggered before the value is added to the model

When an 'add' event is triggered after a model.add, the model does not yet contain the new element. Example code:

$(function() {
    MyModel = Backbone.NestedModel.extend({
        defaults : function() {
            return {
                myArray:[]
            };
        }
    });


    MyView = Backbone.View.extend({
        initialize: function() {
            this.model.on("add:myArray", this.onAdd, this);

            this.model.add("myArray", "aNewElement");

            // this alert will show the array containing a 'aNewElement'
            alert("array after add: " + this.model.get("myArray"));

        },

        onAdd: function() {
            // this alert will show the model before the 'aNewElement' was added. 
                        // This behaviour is different from remove events and Backbone.Model's change events
            alert("array in change event: " + this.model.get("myArray"));
        }
    });

    new MyView({model:new MyModel()});

});

check for window before calling window.console

If you are trying to use this library in node.js there is not a global window. I fixed the error by simply adding:

if(typeof window === 'undefined')var window = {};

above the code and under my custom AMD wrapper. But I'm sure there is a standard or better way of making this work in node.js.

Re-setting items on an array within a nested object doesn't work correctly

If you have a nested model with an array, you are unable to change that array once set. I'm taking a look into it - posting the issue for others.

Passing Test:

test("#set() 1-N with an object containing an array", function() {
  doc.set({
    'addresses[0]': {
      city: 'Seattle',
      state: 'WA',
      areaCodes: ['001', '002', '003']
    }
  });
  doc.set({
    'addresses[1]': {
      city: 'Minneapolis',
      state: 'MN',
      areaCodes: ['101', '102', '103']
    }
  });

  deepEqual(doc.get('addresses[0].areaCodes'), ['001', '002', '003']);
  deepEqual(doc.get('addresses[1].areaCodes'), ['101', '102', '103']);
});

Failing Test:

test("#set() 1-N with an object containing an array where array values are being removed", function() {
  doc.set({
    'addresses[0]': {
      city: 'Seattle',
      state: 'WA',
      areaCodes: ['001', '002', '003']
    }
  });
  doc.set({
    'addresses[0]': {
      city: 'Minneapolis',
      state: 'MN',
      areaCodes: ['101']
    }
  });

  deepEqual(doc.get('addresses[0].areaCodes'), ['101']);
  // addresses[0].areaCodes = ['101', '002', '003']
});

changedAttributes() is not consistent when setting using nested string syntax vs. passing full JSON object

When calling model.set using the Backbone-Nested string syntax (method A), model.changedAttributes() returns a complete list, including both the nested JSON and the nested string syntax of changes:

name: {
  first: 'Aidan',
  middle: {
    initial: 'L',
    full: 'Limburger'
  },
  last: 'Feldman'
},
'name.middle': {
  initial: 'L',
  full: 'Limburger'
},
'name.middle.full': 'Limburger'

When calling model.set using a JSON object like so (method B)...

doc.set({
  gender: 'M',
  name: {
    first: 'Aidan',
    middle: {
      initial: 'L',
      full: 'Limburger'
    },
    last: 'Feldman'
  },
  addresses: [
    {
      city: 'Brooklyn',
      state: 'NY'
    },
    {
      city: 'Oak Park',
      state: 'IL'
    }
  ]
});

... changedAttributes() only returns the top level changed attribute, not the nested string hash:

name: {
  first: 'Aidan',
  middle: {
    initial: 'L',
    full: 'Limburger'
  },
  last: 'Feldman'
}

This might not seem like a big deal, but some of the set operations, for example like the one I believe happens as a result of model.fetch, are not developer-controlled and use method B. This creates problems for code that needs the nested strings in changedAttributes (like Backbone.ModelBinder). It's also a consistency issue.

A test demonstrating this is available on my fork: https://github.com/trevorquinn/backbone-nested/blob/master/test/nested-model.js

run tests in Node

ensure that the plugin works when a document/window isn't present

_delayedTriggers is a singleton, but should be private per model instance

The _delayedTriggers storage is a singleton (shared across all instances). As a result, if during instance A event dispatch an event listener performs some action resulting in events being triggered on a different instance (instance B), instance B will drain the remaining delayed triggers from instance A. Thus, events can trigger on the wrong instance :(

remove function does not trigger event when removing last element in array

When the remove function is used to remove the last element in an array, the remove event is not triggered.
Example code:

$(function() {
    MyModel = Backbone.NestedModel.extend({
        defaults : function() {
            return {
                myArray:["first"]
            };
        }
    });


    MyView = Backbone.View.extend({
        initialize: function() {
            this.model.on("remove:myArray", this.onRemove, this);

            this.model.remove("myArray[0]");

            // the element is removed, but onRemove function is not called
            alert("myArray value is now: " + this.model.get("myArray"));

        },

        onRemove: function() {
            alert("myArray element removed, array is now: " + this.model.get("myArray"));
        }
    });

    new MyView({model:new MyModel()});

});

I've traced this bug to the function remove: function(attrStr, opts). In this function an evaluation is performed to see if an element is actually removed:

(from version 1.1.2, line 88)

// only trigger if an element is actually being removed
      var trigger = !opts.silent && (val.length > i + 1),
        oldEl = val[i];

The variable 'trigger' will be false when the array (in this case, the variable 'val') only has one element and that one element is to be removed. When changing the evaluation to accomodate for array lengths of 1, this issue is fixed.

// only trigger if an element is actually being removed
      var trigger = !opts.silent && (val.length >= i + 1),
        oldEl = val[i];

unset removes all attributes

unset removes all attributes on a model. It does this because it builds a new attributes object with the appropriate attribute removed, and then passes it to the super class "set" function with option.unset == true. This causes Backbone.Model to remove all attributes from the Model.

Improve set performance (IE8)

It seems that set is very slow operation in IE8 this leads to poor performance for example if you need to iterate over collection of models and set bunch of attributes. Consider ways to improve it's performance.

Should change event trigger before data is set on the model?

When a change event fires on a nested property, should a listener be able to access the new value of that property on the model during the change event by doing a model.get(...)?

For example:

//shortened representation of model
model = {
  arr: [
       {
           country: "GB"
       }
  ]
}
//Set nested property
model.set("arr[0].country", "IE");
//change event listener for nested property
model.on("change:arr[0].country", function( model, value, options ) {
    //value is correct
    //value = IE
    //model value still has old value
    //model.get("arr[0].country") = GB
});

On a top level property, the model.get(...) also gets the same result as value.

change event options parameter not detailed enough

The change event handler is passed the model and an options object that contains the attributes that have changed. Because you're calling the super's set(), its just specifying the top level attribute that changed. Ideally, this would specify the actual attributes that changed, similar to Backbone.DeepModel. Basically, there's a loss of information with what actually changed, and binding to the all event to get the change:* events has the problem that its not possible to dedupe changes.

Using model.changedAttributes() doesn't handle this use case because it accumulates all changes to the model

The use case I'm trying to support to syncing a model to a plain old javascript object for use with JsViews, and I would like to do this as efficiently as possible.

I'm guessing this isn't something you'd be able to support, but it would be nice.

Add AMD support

This helps when using requirejs and the included optimizer or you get always the message that Backbone can not be found.

cannot empty an array

When using NestedModel's set implementation to assign an empty array to an existing array value, the array value is not changed. See example below:

$(function() {
    MyModel = Backbone.NestedModel.extend({
    //MyModel = Backbone.Model.extend({
        defaults : function() {
            return {
                myArray:["first", "second"]
            };
        }
    });


    MyView = Backbone.View.extend({
        initialize: function() {
            this.model.on("change:myArray", this.render, this);

            this.model.set("myArray", []);

            // when using Backbone.NestedModel, myArray stays unmodified and change event is not triggered
            // when using Backbone.Model, myArray is set to [] and change event is triggered
            alert("myArray value is now: " + this.model.get("myArray"));

        },

        render: function() {
            alert("myArray changed: " + this.model.get("myArray"));
        }       
    });

    new MyView({model:new MyModel()});

});

Looking at the NestedModel code, it seems that the _mergeAttrs: function(dest, source, opts, stack) function causes the problem: it should merge attributes from source to dest but only does this when source contains array elements. A quick fix might be something like this:

(backbone-nested-v1.1.2, from line 117)

  _mergeAttrs: function(dest, source, opts, stack){
      opts = opts || {};
      stack = stack || [];

      // fix: empty dest when source is an empty array
      if (source.length == 0) {
          dest = [];
      }

      _.each(source, function(sourceVal, prop){
         [.. rest of code..]

Use Sinon to Better Assert Triggers

Hey Aidan,

I was thinking it'd be nice if we could assert the triggers a bit better. One of the things I'd like to confirm is that each trigger is only being fired once, and that they're being sent in the correct order (which I would say is bottom-up). To do that, I'm thinking of importing Sinon to the test suite so that we can do some additional checking.

So instead of this:

  test("change event on deeply nested attribute", function() {
    var callbacksFired = [false, false, false, false];

    doc.bind('change', function(){ callbacksFired[0] = true; });
    doc.bind('change:name', function(){ callbacksFired[1] = true; });
    doc.bind('change:name.middle', function(){ callbacksFired[2] = true; });
    doc.bind('change:name.middle.full', function(){ callbacksFired[3] = true; });

    doc.bind('change:name.middle.initial', function(){ ok(false, "'change:name.middle.initial' should not fire"); });
    doc.bind('change:name.first', function(){ ok(false, "'change:name.first' should not fire"); });

    doc.set({'name.middle.full': 'Leonard'});

    ok(callbacksFired[0], "'change' should fire");
    ok(callbacksFired[1], "'change:name' should fire");
    ok(callbacksFired[2], "'change:name.middle' should fire");
    ok(callbacksFired[3], "'change:name.middle.full' should fire");
  });

Doing something like this:

  test("change event on deeply nested attribute", function() {
    var change = sinon.spy();
    var changeName = sinon.spy();
    var changeNameMiddle = sinon.spy();
    var changeNameMiddleFull = sinon.spy();
    var changeNameMiddleInitial = sinon.spy();
    var changeNameFirst = sinon.spy();

    doc.bind('change', change);
    doc.bind('change:name', changeName);
    doc.bind('change:name.middle', changeNameMiddle);
    doc.bind('change:name.middle.full', changeNameMiddleFull);

    doc.bind('change:name.middle.initial', changeNameMiddleInitial);
    doc.bind('change:name.first', changeNameFirst);

    doc.set({'name.middle.full': 'Leonard'});

    ok(change.calledOnce);
    ok(change.calledAfter(changeName));

    ok(changeName.calledOnce);
    ok(changeName.calledAfter(changeNameMiddle));

    ok(changeNameMiddle.calledOnce);
    ok(changeNameMiddle.calledAfter(changeNameMiddleFull));

    ok(changeNameMiddleFull.calledOnce);

  });

Thoughts?

Cannot read property 0 of undefined

This is an extension of the issue I already opened, #70. If I copy the Backbone.NestedModel.extend({...}) in the linked issue, and paste it after
var other = (pasted code), a new model object is created with no issues. If i do

>other.set({'Date.StartTime': 'never'})

I get:

  TypeError: Cannot read property '0' of undefined
  arguments: Array[2]
    0: 0
    1: undefined
    length: 2
    __proto__: Array[0]
get message: function () { [native code] }
get stack: function () { [native code] }
set message: function () { [native code] }
set stack: function () { [native code] }
type: "non_object_property_load"
__proto__: Error

If i do the same exact >other.set({'Date.StartTime': 'never'}) command immediately after, it works fine. This is obviously creating issues where I set attributes in code where I don't have the option to run the block again.

having issues getting this to work with meteor...

I import jQuery, underscore, and backbone as smart packages in meteor. I have a
<script src="/javascripts/backbone-nested.js"></script>
import in my head.html file, but when my models are loaded i get a "cannot call method 'extend' of undefined" error

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.