afeld / backbone-nested

A plugin to make Backbone.js keep track of nested attributes - looking for maintainers! https://github.com/afeld/backbone-nested/issues/157
https://afeld.github.com/backbone-nested/
MIT License
443 stars 83 forks source link

change event fires on nested parents while there is no actual change on the deep chiled #152

Open mohamedfarouk opened 8 years ago

mohamedfarouk commented 8 years ago

when trying to set value for deep nested attribute of a nested model with same old value, change events still fire on parents level, no change event fire on deep attributed level below test code, with proposed fix

(function () {
    var BugModel = Backbone.NestedModel.extend({
    });

    var FixModel = Backbone.NestedModel.extend({
        _setAttr: function (newAttrs, attrPath, newValue, opts) {
            opts = opts || {};

            var fullPathLength = attrPath.length;
            var model = this;
            var oldVal = Backbone.NestedModel.walkThenGet(newAttrs, attrPath);
            // See if this is a new value being set
            var isNewValue = !_.isEqual(oldVal, newValue);

            Backbone.NestedModel.walkPath(newAttrs, attrPath, function (val, path, next) {
                var attr = _.last(path);
                var attrStr = Backbone.NestedModel.createAttrStr(path);

                if (path.length === fullPathLength) {
                    // reached the attribute to be set

                    if (opts.unset) {
                        // unset the value
                        delete val[attr];

                        // Trigger Remove Event if array being set to null
                        if (_.isArray(val)) {
                            var parentPath = Backbone.NestedModel.createAttrStr(_.initial(attrPath));
                            model._delayedTrigger('remove:' + parentPath, model, val[attr]);
                        }
                    } else {
                        // Set the new value
                        val[attr] = newValue;
                    }

                    // Trigger Change Event if new values are being set
                    if (!opts.silent && _.isObject(newValue) && isNewValue) {
                        var visited = [];
                        var checkChanges = function (obj, prefix) {
                            // Don't choke on circular references
                            if (_.indexOf(visited, obj) > -1) {
                                return;
                            } else {
                                visited.push(obj);
                            }

                            var nestedAttr, nestedVal;
                            for (var a in obj) {
                                if (obj.hasOwnProperty(a)) {
                                    nestedAttr = prefix + '.' + a;
                                    nestedVal = obj[a];
                                    if (!_.isEqual(model.get(nestedAttr), nestedVal)) {
                                        model._delayedChange(nestedAttr, nestedVal, opts);
                                    }
                                    if (_.isObject(nestedVal)) {
                                        checkChanges(nestedVal, nestedAttr);
                                    }
                                }
                            }
                        };
                        checkChanges(newValue, attrStr);

                    }

                } else if (!val[attr]) {
                    if (_.isNumber(next)) {
                        val[attr] = [];
                    } else {
                        val[attr] = {};
                    }
                }

                if (!opts.silent) {
                    // let the superclass handle change events for top-level attributes
                    if (path.length > 1 && isNewValue) {
                        model._delayedChange(attrStr, val[attr], opts);
                    }

                    if (_.isArray(val[attr])) {
                        model._delayedTrigger('add:' + attrStr, model, val[attr]);
                    }
                }
            });
        }
    });

    $(document).ready(function () {

        var bugModel = new BugModel({
            Category: {
                categoryId: 1,
                categoryName: "Category 1",
                Types: [{
                    typeId: 1,
                    typeName: "type1"
                },
                {
                    typeId: 2,
                    typeName: "type2"
                }]
            }
        });
        bugModel.on("change:Category.Types", function () { console.log("no actual change but bug parent change event fired"); });
        bugModel.on("change:Category.Types[0].typeName", function () { console.log("fire only with correct change"); });

        var fixModel = new FixModel({
            Category: {
                categoryId: 1,
                categoryName: "Category 1",
                Types: [{
                    typeId: 1,
                    typeName: "type1"
                },
                {
                    typeId: 2,
                    typeName: "type2"
                }]
            }
        });
        fixModel.on("change:Category.Types", function () { console.log("fix model have changed"); });

        bugModel.set('Category.Types[0].typeName', "type1");
        fixModel.set('Category.Types[0].typeName', "type1");

    });
})();