简体   繁体   中英

Backbone.js - How to create a collection that has bindable properties

This is somewhat of contrived example, but I believe it gets the point across.

Lets say I have a backbone cars collection. And for the collection I want a property called isValid. I want other objects to be able to bind to isValid and fire a function when isValid changes. The collection isValid property will be changed based on the models in a collection as a whole.

For example, if all the doors are locked on every car, then the isValid should change to true. When isValid changes, all functions that are bound to isValid change event should be fired.

My question is, how can I create a collection that has bindable properties that works similar to model properties?

This is the code that I would like get to work.

var Car = Backbone.Model.extend({});
var Cars = Backbone.Collection.extend({
    model: Car,
    url: "Cars",
    isValid: false,  //Here's the property that I want bindable
                     //but do not know how to accomplish this.
});

var cars = new Cars();

//Call a function when cars.isValid changes
cars.bind("change:isValid", carsIsValidChanged, cars)

//Not sure if this what the function would look like
//but would need access to cars.isValid or just the car collection
function carsIsValidChanged(isValid){
    console.log(isValid);
}


Update

See my complete solution as an answer below.

So there is no 'built-in' way to respond to property changes on a Collection, as there really isn't a supported way (that I know of) to have properties on a collection. However, its still totally possible like this, I reckon. (untested, but should be fairly close)

var Car = Backbone.Model.extend({});
var Cars = Backbone.Collection.extend({
    model: Car,
    url: "Cars",
    initialize: function() {
        var self = this;
        this.isValid = false;
        this.bind('add', function() {
          var curValid = true;
          self.each(function(car) {
            if(car.get('is_locked') != true) {
              curValid = false;
            }
          });
          if(curValid != self.isValid) {
            self.isValid = curValid;
            self.trigger('change:isValid', self.isValid, self);
          }
        });   
    }                 
});

// instantiate a new Cars collection
var cars = new Cars();


function carsIsValidChanged(isValid){
    console.log(isValid);
}

//Call a function when cars.isValid changes
cars.bind("change:isValid", carsIsValidChanged)

One note: you don't want isValid: listed as property like you would model or url. This seems to make backbone be weird, and your isValid might span all instances of the collection. Its better to define them as an initializer, then each time you instantiate that collection you will properly have an instance of isValid accessible by this.isValid.

You could use something like this;

Backbone.Collection.prototype.bindableProperty = function (key, val) {
    var self = this;
    Object.defineProperty(this, key, {
        configurable: false,
        get: function () { return val; },
        set: function (v) {
          val = v;
          self.trigger('change:'+key, val)
        }
    });
};

Just do this to add the property;

var Cars = Backbone.Collection.extend({
  model: Car,
  url: "Cars",
  initialize: function () {
    this.bindableProperty('isValid', false);
  }
});

Warning though; older versions of IE don't support Object.defineProperty.

Here is my complete solution to this question: jsFiddle Complete example

@spotmat put me in the correct direction, but I needed to add additional functionality. Here's a few items that needed to be solved:

  • Collection contructor needs to handle data pased to it
  • When a model IsLocked property is updated the collection needs to be re-validated

I'm not sure if adding binding properties to a collection is a great idea, but is was something fun to figure out, and I learned alot.

var log = {};//this is for debugging 
_.extend(log, Backbone.Events);

var Car = Backbone.Model.extend({});

var Cars = Backbone.Collection.extend({
    model: Car,
    url: "scripts/data/Cars.json",
initialize: function () {
    var _this = this;

    this.isValid = false;  //user should bind to "change:isValid"
    this._isPending = false;  //This is so that 

    this.bind("add", this.modelAdded, this);
    this.bind("reset", this.modelsAdded, this);

    if (this.length > 0) {
        //Model passed in though the constructor will not be binded
        //so call modelsAdded
        this.modelsAdded(this);
    }
},

modelsAdded: function (collection) {
    this._isPending = true
    log.trigger("log", ["modelsAdded: " + collection.length]);
    collection.each(this.modelAdded, this); //Do nut remove "this" param.  It need when modelAdded gets called
    this._isPending = false
    this._validate();
},

modelAdded: function (model) {
    log.trigger("log", ["modelAdded: " + model.get("Model") + "; IsLocked: " + model.get("IsLocked")]);
  //!!!for each model added to the colleciton bind
  //its IsLocked property to the collection modelIsLockedChange function
    model.bind("change:IsLocked", this.modelIsLockedChanged, this);
    if (this._isPending == false) {
        this._validate();
    }
},

modelIsLockedChanged: function (model) {
    log.trigger("log", ["modelIsLockedChanged:" + model.get("Model") + "; IsLocked: " + model.get("IsLocked")]);
    this._validate();
},

_validate: function () {
    var isValid = true;
    this.each(function (model) {
        if (model.get("IsLocked") == false) {
            isValid = false
        }
    });

    if (this.isValid != isValid) {
        this.isValid = isValid
        cars.trigger("change:isValid", [this.isValid])
    }
    },
});

This view is for debugging purposes only

var Logger = Backbone.View.extend({
    el: $("#output"),

    initialize: function(){
        log.bind("log", this.logMessage, this)
    },

    render: function(){
        return $(this.el).html("");
    },

    logMessage: function(message){
        $(this.el).find("ul").append("<li>" + message[0] + "</li>");
    }
})

The following code is used to test the collection

var logger = new Logger();
log.bind("log", function(message){
    console.log(message[0]);
});

var carsData = [
    { "Model": "Chevy Camero", "Year": 1982, "IsLocked": true },
    { "Model": "Ford F-150", "Year": 2011, "IsLocked": false }
]

var cars = new Cars(carsData);
cars.bind("change:isValid", function(isValid){
    log.trigger("log", ["change:isValid: " + isValid]);
});

cars.add({ "Model": "Toyota Tacoma", "Year": 2006, "IsLocked": true });
cars.at(1).set({ "IsLocked": true });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM