miguelr
miguelr

Reputation: 1344

How can I enforce attribute types in a Backbone model?

I want to have a Backbone model with float attributes in it but without worrying too much about variable types.

I would like to encapsulate the value parsing right there in the model so I am thinking of overriding the set function:

var Place = Backbone.Model.extend({
  set: function(attributes, options) {
    if (!_.isEmpty(attributes.latitude)){
      attributes.latitude == parseFloat(attributes.latitude);
    }
    if (!_.isEmpty(attributes.longitude)){
      attributes.longitude == parseFloat(attributes.longitude);
    }
    Backbone.Model.prototype.set.call(this, attributes, options);
  }
});

However this seems cumbersome, since I would have a similar logic in the validate method and potentially repeated across multiple models. I don't think the View should take care of these conversions.

So what is the best way of doing it?

Upvotes: 3

Views: 3016

Answers (2)

Emil Lundberg
Emil Lundberg

Reputation: 7380

My solution was to replace Backbone.Model.prototype.set with a preprocessor proxy:

/**
 * Intercept calls to Backbone.Model.set and preprocess attribute values.
 * 
 * If the model has a <code>preprocess</code> property, that property will be
 * used for mapping attribute names to preprocessor functions. This is useful
 * for automatically converting strings to numbers, for instance.
 * 
 * @param Backbone
 *            the global Backbone object.
 */
(function(Backbone) {
    var originalSet = Backbone.Model.prototype.set;
    _.extend(Backbone.Model.prototype, {
        set: function(key, val, options) {
            if(!this.preprocess) {
                return originalSet.apply(this, arguments);
            }

            // If-else copied from Backbone source
            if (typeof key === 'object') {
                attrs = key;
                options = val;
            } else {
                (attrs = {})[key] = val;
            }

            for(attr in this.preprocess) {
                if(_.has(attrs, attr)) {
                    attrs[attr] = this.preprocess[attr](attrs[attr]);
                }
            }
            return originalSet.call(this, attrs, options);
        },
    });
})(Backbone);

After this, models with a preprocess property will use it to map attribute names to preprocessor functions. For instance, preprocess: { age: parseInt } means that whenever the age attribute is set, the value will be passed through parseInt before actually setting it. Attributes with no corresponding preprocess entry will not be affected.

Example usage:

var Thing = Backbone.Model.extend({
    preprocess: {
        mass: parseInt,
        created: function(s) { return new Date(s); },
    },
});
var t = new Thing({
    label: '42',
    mass: '42',
    created: '1971-02-03T12:13:14+02:00',
});
console.log(t.get('label')+3); // 423
console.log(t.get('mass')+3); // 45
console.log(t.get('created').toLocaleString('ja-JP', { weekday: 'short' })); // 水

Pros

  • The functionality is available in all models without needing to duplicate code
  • No need to send { validate: true } in every call to set
  • No need to duplicate preprocessing in validate, since this happens before validate is called (this might also be a con, se below)

Cons

  • Some duplication of Backbone code
  • Might break validation since preprocessing happens before validate is called. JavaScript parsing methods usually return invalid values instead of throwing exceptions, though (i.e. parseInt('foo') returns NaN), so you should be able to detect that instead.

Upvotes: 0

tkone
tkone

Reputation: 22738

Use a validation plugin for your model so that you can validate the input in a generic fashion.

There are several out there including one that I have written:

Then you don't worry about performing data validation anywhere else - your model does it and sends out and error message you can listen for and provide appropriate feedback.

Also, a lat/lng pair can, in rare circumstances, be an integer, such as Greenwich England: 0,0 or the north pole: 90,180. And since JavaScript only has "number" any valid input for parseFloat is also valid for parseInt.

But parseFloat will always return a float.

Upvotes: 2

Related Questions