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

Add a "replace" option to Backbone.Model.set #3253

Open
thesmart opened this issue Jul 31, 2014 · 22 comments · May be fixed by #4101
Open

Add a "replace" option to Backbone.Model.set #3253

thesmart opened this issue Jul 31, 2014 · 22 comments · May be fixed by #4101
Labels

Comments

@thesmart
Copy link

I needed to update some of a Model's data that came in from the server. Currently, there are two options: call Model.set, or set Model.attributes directly. I didn't want changes to be recorded, but I also couldn't use silent because I needed the respective views to update. So, I wrote a monkey patch:

Backbone.Model.prototype.reset = (attributes, options) ->
    attrs = attributes || {};
    if options.parse
      attrs = this.parse(attrs, options) || {}

    @set(attrs, options);
    @changed = {};

Wondered why Backbone.Model doesn't have a reset method like Backbone.Collection?

@jashkenas
Copy link
Owner

Reset is an escape hatch, that allows you to easily do efficient rendering in bulk when you know that you need it.

For your case, just use set.

@thesmart
Copy link
Author

thesmart commented Aug 1, 2014

Can you set without making attribute changes dirty?

On Aug 1, 2014, at 7:28 AM, Jeremy Ashkenas [email protected] wrote:

Reset is an escape hatch, that allows you to easily do efficient rendering in bulk when you know that you need it.

For your case, just use set.


Reply to this email directly or view it on GitHub.

@gabriellupu
Copy link

Calling Model.clear() before Model.set({}) will work in your case? This way you won't extend the current attributes, instead they will be replaced. Model.clear() also supports silent option if you don't want to trigger two "change" events on the model.

@lennerd
Copy link

lennerd commented Oct 12, 2014

I agree that this method is missing. I also thought about using model.clear() and model.set() in conjunction. Then I ran across the problem, that I trigger the change event twice now.
Using the silent option when calling model.clear() is not an option, because I also want to have a change event fired, when a property gets unset.

A model.reset() method would take a new attributes hash and fill this hash with undefined values for old attributes keys not being present in the new attribute hash.

Model.prototype.reset = function(attrs, options) {
    for (var key in this.attributes) {
        if (key in attrs) continue;
        attrs[key] = void 0;
    }

    return this.set(attrs, options);
};

@TheIronDev
Copy link

@lennerd

What about:

Model.prototype.reset = function(attrs, options) {
    for (var key in this.attributes) {
        this.unset(key, {silent:true});
    }
    return this.set(attrs, options);
};

@lennerd
Copy link

lennerd commented Oct 12, 2014

No, this does not help. Imagine you a have a key foo in the current attributes hash of the model, which is not present in the new attrs hash you pass to model.reset(). When I listen to the change:foo event, it won't be triggered with the new value undefined, because we used model.unset() in silent mode.

Backbone.Model.prototype.reset = function(attrs, options) {
    for (var key in this.attributes) {
        this.unset(key, {silent:true});
    }
    return this.set(attrs, options);
};

var bar = new Backbone.Model();

bar.on('change', function(model) {
    console.log('The model bar has been changed.');
});

bar.on('change:foo', function(model, foo) {
    console.log('Foo has been changed to: ' + foo);
});

bar.set(foo, 'test');
// => The model bar has been changed.
// => Foo has been changed to: test

bar.reset({ foo2: 'test2' });
// => The model bar has been changed.
// Foo was resetted but no change event has been triggered.

http://jsfiddle.net/lennerd/3s1Ltwgu/

@TheIronDev
Copy link

Cool, I get what you mean. I'd probably opt for using this.unset(key, options) over overriding this.attributes explicitly, but that's just a matter of swapping out attrs[key] = void 0; 🐼

@tyler-eon
Copy link

Pardon any ignorance I show, as I'm still relatively new to backbone.js, but the behavior being discussed sounds like Model.fetch. The description says:

Resets the model's state from the server

It looks as though change events are still triggered but do not make the model "dirty". Is there a reason this approach cannot be taken when you are looking to reset attributes based on a server response? I would imagine the only such situation this might occur is when a model changes as a side-effect of another operation, but generally such side-effects are considered poor programming and should be avoided if possible. If the side-effect cannot be avoided, perhaps it makes more sense to send down an "update model XYZ" response flag instead of the new model attributes and trigger a fetch whenever you see such a response.

Again, pardon any ignorance I am exhibiting in this comment.

@akre54 akre54 closed this as completed Feb 17, 2015
@jponc
Copy link

jponc commented May 20, 2015

@kolorahl ,

What the OP wants to achieve is to clear the current model attributes and pass a new JSON which becomes the new attributes of the model. In this manner, we don't really want to hit the backend if already have the JSON.

@jponc
Copy link

jponc commented May 20, 2015

I somehow agree with @lupugabriel1 with his clear + set method. But I think this is one functionality that needs to be considered. Something like Backbone.Collection#reset

jridgewell added a commit to jridgewell/backbone that referenced this issue May 24, 2015
- Adds `{ reset: true }` flag to `#set` that deletes any attributes
that are not specified
- Adds ‘reset’ event mirroring `Collection`’s.

Fixes jashkenas#3253, jashkenas#3395.

I could really use help naming the `validateCombined` option. I need to
validate only the passed in `attrs` (both `#clear` and `#reset` specify
_exactly_ what the attributes will be), not the merged `attrs`
`attributes`.
@thesmart
Copy link
Author

The reason I needed this is because the world is changing. Backbone assumes Model#fetch() XHR is the primary method for loading data from the server, but we're doing a lot more stuff using websockets. When data gets pushed to the client, it's redundant to call .fetch and we need a decent way to side-load the data and still get an event hook to trigger.

@jridgewell
Copy link
Collaborator

Why aren't you using #set?

@thesmart
Copy link
Author

@jridgewell because #set will make the attributes dirty. Let's try using set and see what happens:

function callback(data_from_server) {
  console.info(data_from_server);
  m = new Backbone.Model(data_from_server);
  m.set('foo', 'what?', {silent: true});
  console.info(m.changedAttributes())
}

Actual outcome:

{foo: 'bar'}
{foo: "what?"}

Desired outcome:

{foo: 'bar'}
false

Set is fine for when the state has changed out-of-sync with the server, but Model#reset is needed because there is no way to mark state as synchronize w/ the server.

@thesmart
Copy link
Author

I ended up writing a different monkey patch for this feature:

/**
 * Allow for side-loading data from the server, calling the sync event afterwards.
 * @param attributes
 * @param options
 */
Backbone.Model.prototype.sync_set = function(attributes, options) {
  var attrs = attributes || {};
  if (options.parse) {
    attrs = this.parse(attrs, options) || {}
  }

  this.set(attrs, options);
  this.changed = {};
  this.trigger('sync', this, attributes, options);
  return this;
}
function callback(data_from_server) {
  console.info(data_from_server);
  m = new Backbone.Model(data_from_server);
  m.set('foo', 'what?', {silent: true});
  console.info(m.changedAttributes())
}
{foo: 'bar'}
false

This probably needs to unset missing items also. Not sure if Model#set(attributes) does that.

@brettjonesdev
Copy link

As @lennerd pointed out, calling clear() followed by set() is not a good option, because it

  1. Fires two change events and
  2. If you use silent:true on the clear call, you do not get change events on the attributes which were unset.

collection.reset() is very intuitive and I think that Model could really benefit from an equivalent method. I find myself trying to use model.reset(attrs) all the time, always disappointed when it's not there. :(

@brettjonesdev
Copy link

I went ahead and created a little extension to add a working reset method to Backbone.Model: Backbone-Model-Reset

@moos
Copy link

moos commented Sep 16, 2015

Agree - would be useful to have natively. I needed to just reset attributes without mangling the 'id' attribute, as clear() was doing. Here's the gist of it.

@pgifford
Copy link

pgifford commented Jul 5, 2016

@thesmart, Model#set does not unset missing attributes which is why I think Model#reset is necessary.

It should be a requirement to give a reason when closing an issue. I'd like to know why @akre54 closed this. Backbone models are deliberately primitive and unopinionated, however, the lack of Model#reset expresses an opinion about how models are used. From http://backbonejs.org/#Getting-started

Philosophically, Backbone is an attempt to discover the minimal set of data-structuring (models and collections) and user interface (views and URLs) primitives that are generally useful when building web applications with JavaScript.

@thesmart
Copy link
Author

thesmart commented Jul 6, 2016

As far as I can tell, Backbone's data model is incompatible with REST
because there is no way to safely model sever state on the client after a
model has been constructed. Constructing a new model is the only way to get
a fresh state without reset.

On Tuesday, July 5, 2016, pgifford [email protected] wrote:

@thesmart https://github.com/thesmart, Model#set does not unset missing
attributes which is why I think Model#reset is necessary.

It should be a requirement to give a reason when closing an issue. I'd
like to know why @akre54 https://github.com/akre54 closed this.
Backbone models are deliberately primitive and unopinionated, however, the
lack of Model#reset expresses an opinion about how models are used. From
http://backbonejs.org/#Getting-started

Philosophically, Backbone is an attempt to discover the minimal set of
data-structuring (models and collections) and user interface (views and
URLs) primitives that are generally useful when building web applications
with JavaScript.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#3253 (comment),
or mute the thread
https://github.com/notifications/unsubscribe/AARJLQ8-BGeV0X_owHVpyPQAdeiMweNMks5qSriDgaJpZM4CTBHH
.

@blikblum blikblum linked a pull request Nov 20, 2016 that will close this issue
@emileber
Copy link
Contributor

emileber commented Jan 4, 2017

I needed a model reset and went ahead and made it in the spirit of Backbone. I use this in my personal extension of Backbone.

It's better than a simple .clear followed by a .set because it merges the defaults back into the model, letting any passed attributes to override them like on initialization.

/**
 * Clears the model's attributes and sets the default attributes.
 * @param {Object} attributes to overwrite defaults
 * @param {Object} options  to pass with the "set" call.
 * @return {Backbone.Model}  this object, to chain function calls.
 */
reset: function(attributes, options) {
    options = _.extend({ reset: true }, options);

    // ensure default params
    var defaults = _.result(this, 'defaults'),
        attrs = _.defaults(_.extend({}, defaults, attributes || {}), defaults);

    // apply
    this._reset(attrs, options);

    // triggers a custom event, namespaced to model in order
    // to avoid collision with collection's native reset event
    // when listening to a collection.
    if (!options.silent) this.trigger('model:reset', this, options);

    return this;
},

/**
 * Private method to help wrap reset with a custom behavior in child
 * classes.
 * @param  {Object} attributes to overwrite defaults
 * @param  {Object} options  to pass with the "set" call.
 */
_reset: function(attrs, options) {
    this.clear({ silent: true }).set(attrs, options);
},

It triggers change events (change and change:attribute) in addition to the custom model:reset event if not silent: true. It could easily be customized to trigger only the model:reset event but I think that the change events should always trigger when resetting a model.

@blikblum
Copy link

blikblum commented Jan 4, 2017

See also #4101

@solygen
Copy link

solygen commented Aug 11, 2017

i've tweaked the solution of lennerd #3253 (comment) a little bit.

Backbone.Model.prototype.reset = function(attrs, options) {
    var missing = {};
    for (var key in this.attributes) {
        if (key in attrs) continue;
        attrs[key] = undefined;
        missing[key] = true;
    }
    // trigger all change events at the same time
    this.set(attrs, options);
    // remove missing attributes completely
    for (var key in missing) {
        // no silent option here in case  attributes changed again meanwhile (edge case)
        this.unset(key)
    }
    return this;
};

@jgonggrijp jgonggrijp reopened this Dec 22, 2021
@jgonggrijp jgonggrijp changed the title Add a "reset" method to Backbone.Model Add a "replace" option to Backbone.Model.set Dec 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.