Dealing with non-saveable values in Backbone

后端 未结 3 427
情书的邮戳
情书的邮戳 2021-01-18 07:23

Is there a standard way to deal with non-saveable values in Backbone.

e.g.

MyModel = Backbone.extend(Backbone.Model, {
    initialize: function () {         


        
相关标签:
3条回答
  • 2021-01-18 08:06

    I like Peter Lyon's idea. I've thought about that a few times, but never actually put it in place. For all the ways that I have handled this, though, here are my two favorites:

    • Non-"attribute" values
    • View Models

    Non-Attribute Values

    This one is simple: don't store the values you need in the model's standard attributes. Instead, attach it directly to the object:

    
    myModel.someValue = "some value";
    

    The big problem here is that you don't get all of the events associated with calling set on the model. So I tend to wrap this up in a method that does everything for me. For example, a common method I put on models is select to say that this model has been selected:

    
    MyModel = Backbone.Model.extend({
      select: function(){
        if (!this.selected){
          this.selected = true;
          this.trigger("change:selected", this, this.selected);
        }
      }
    });
    

    In your case, I'm not sure this would be a good approach. You have data that needs to be calculated based on the values that are in your attributes already.

    For that, I tend to use view models.

    View models.

    The basic idea is that you create a backbone model that is persist-able, as you normally would. But the you come along and create another model that inherits from your original one and adds all the data that you need.

    There are a very large number of ways that you can do this. Here's what might be a very simple version:

    
    MyModel = Backbone.Model.Extend({ ... });
    
    MyViewModel = function(model){
      var viewModel = Object.create(model);
    
      viewModel.toJSON = function(){
        var json = model.toJSON();
        json.inches = json.mm / 25;
        return json;
      };
    
      return viewModel;
    });
    

    The big benefit of wrapping this with Object.create is that you now have a prototypal inheritance situation, so all of your standard functionality from the model is still in place. We've just overridden the toJSON method on the view model, so that it returns the JSON object with the inches attribute.

    Then in a view that needs this, you would wrap your model in the initialize function:

    
    MyView = Backbone.View.extend({
      initialize: function(){
        this.model = MyViewModel(this.model);
      },

    render: function(){ var data = this.model.toJSON(); // returns with inches } });

    You could call new MyViewModel(this.model) if you want, but that's not going to do anything different, in the end, because we're explicitly returning an object instance from the MyViewModel function.

    When your view's render method calls toJSON, you'll get the inches attribute with it.

    Of course, there are some potential memory concerns and performance concerns with this implementation, but those can be solved easily with some better code for the view model. This quick and dirty example should get you down the path, though.

    0 讨论(0)
  • 2021-01-18 08:12

    I think this should do it. Define your Model defaults as your valid schema and then return only the subset of this.attributes that is valid during toJSON.

    var Model = Backbone.Model.extend({
      defaults: {
        foo: 42,
        bar: "bar"
      },
    
      toJSON: function () {
        var schemaKeys = _.keys(this.defaults);
        var allowedAttributes = {};
        _.each(this.attributes, function (value, key) {
        if (_.include(schemaKeys, key)) {
          allowedAttributes[key] = value;
        }
        return allowedAttributes;
      }
    });
    

    Note that _.pick would make the code a bit shorter once you have underscore 1.3.3 available. I haven't seen a "tried and tested" convention in my travels through the backbone community, and since backbone leaves so many options open, sometimes conventions don't emerge, but we'll see what this stackoverflow question yields.

    0 讨论(0)
  • 2021-01-18 08:17

    Dealing with non-persisted attributes in Backbone.js has been doing my head in for a while, particularly since I've started using ember/ember-data, which handles the various situations through computed properties, ember-data attributes, or controllers.

    Many solutions suggest customising the toJSON method. However, some popular Backbone plugins (particularly those that deal with nested models), implement their own toJSON method, and make a call to Backbone.Model.prototype.toJSON to obtain an object representation of a model's attributes. So by overwriting the toJSON method in a model definition, you'll lose some (potentially crucial) features of those plugins.

    The best I've come up with is to include an excludeFromJSON array of keys in the model definition, and overwrite the toJSON method on Backbone.Model.prototype itself:

    Backbone.Model.prototype.toJSON = function() {
      var json = _.clone(this.attributes),
          excludeFromJSON = this.excludeFromJSON;
      if(excludeFromJSON) {
        _.each(excludeFromJSON, function(key) {
          delete json[key];
        });
      }
      return json;
    };
    
    MyModel = Backbone.Model.extend({
      excludeFromJSON: [
        'inches'
      ]
    });
    

    In this way, you'll only have to define the non-persisted keys (if you forget to do so, you'll soon be reminded when your server throws an error!). toJSON will behave as normal if no excludeFromJSON property is present.


    In your case, inches is a computed property, derived from mm, so it makes sense to implement this as a method on your model (ensuring that the value for inches is correct when mm is changed):

    MyModel = Backbone.Model.extend({
      inches: function() {
        return this.get('mm') / 25;
      }
    });
    

    However, this has the downside of being accessed differently to everyother attribute. Ideally you'll want to keep it consistent with accessing other attributes. This can be achieved by extending the default get method:

    var getMixin = {
      get: function(attr) {
        if(typeof this[attr] == 'function') {
          return this[attr]();
        }
        return Backbone.Model.prototype.get.call(this, attr);
      }
    };
    
    MyModel = Backbone.Model.extend({
      inches: function() {
        return this.get('mm') / 25;
      }
    });
    _.extend(MyModel.prototype, getMixin);
    

    Which will let you do:

    new MyModel().get('inches');
    

    This approach doesn't touch the underlying attributes hash, meaning that inches will not appear in the toJSON representation, unless you set the value of inches later on, in which case you'll need something like the excludeFromJSON array.

    If you have the need to set the inches value, you may also want to listen for changes and adjust the value of mm:

    MyModel = Backbone.Model.extend({
    
      initialize: function() {
        this.on('change:inches', this.changeInches, this);
      },
    
      inches: function() {
        return this.get('mm') / 25;
      },
    
      changeInches: function() {
        this.set('mm', this.attributes.inches * 25);
      }
    });
    _.extend(MyModel.prototype, getMixin);
    

    See the complete example on JSBin.

    It's also worth noting that the (official?) purpose of the toJSON method has recently been redefined as preparing a model for syncing with a server. For this reason, calling toJSON should always return only the "persistable" (or "saveable") attributes.

    0 讨论(0)
提交回复
热议问题