I have the following View:
var PageView = Backbone.View.extend({
model: Models.MyModel,
initialize: function () {
this.state = window.state;
this.state.on("change", this.render, this);
},
render: function () {
}
});
The state is an another Model that will contain different global settings like: pageNumber, pageSize and etc.
So my question is: is it possible to change this.state.on("change", this.render, this); to something like:
this.state.on("change:pageNumber=2", this.render, this);
i.e. this.render will be executed after state is changed and only if pageNumber property will be equal to 2.
I know that I can just place if statement into render method but if there is way to do that like above it will be greater.
Thanks.
Backbone does not offer a filtering mechanism on events, but you could alter your state model to trigger custom events with the signature you wish.
For example, let's say state is an instance of this class
var EqualBindModel = Backbone.Model.extend({
arm: function(attribute, watchvalue) {
this.on('change:'+attribute, function(model, val, options) {
if (watchvalue === val)
model.trigger('change:'+attribute+'='+val, model, val, options);
});
}
});
you could then setup your custom event with
var state = new EqualBindModel();
state.arm('pageNumber', 2);
and listen to with
state.on("change:pageNumber=2", function(model, value, options) {
console.log('event change:pageNumber=2');
});
And a demo http://jsfiddle.net/ZCab8/1/
Related
I'm writing basic to-do list using Backbone.js. Every input adding as a model to collection. Listening for 'add' on collection and rendering newly added model (appending li with 'task' to ul). Then by double-clicking on item I'm retrieving html() of it and in a loop comparing it to corresponding attribute in a model. When it catch the right model - destroying the model (should be deleted from a collection accordingly). But some issue occuring in console, it says
Uncaught TypeError: Cannot read property 'toJSON' of undefined
and adding some buggy effect (not everytime can delete item by the first dblckick). If anyone can point the problem out it would be greatly appreciated!
Here's code
var Model = Backbone.Model.extend({
default: {
task: '',
completed: false
}
});
var Collection = Backbone.Collection.extend({
model: Model
});
var ItemView = Backbone.View.extend({
tagName: 'li',
render: function () {
this.$el.html(this.model.toJSON().task);
return this;
}
});
var TodoView = Backbone.View.extend({
el: '#todo',
initialize: function () {
this.collection = new Collection();
this.collection.on('add', this.render, this);
},
events: {
'click .add': 'add',
'dblclick li': 'destroy',
'keydown': 'keyEvent'
},
add: function () {
this.collection.add(new Model({ //adding input as an model to collection
task: this.$el.find('#todo').val(),
completed: false
}));
this.$el.find('#todo').val(''); //clearing input field
this.$el.find('#todo').focus(); //focusing input after adding task
},
keyEvent: function (e) {
if (e.which === 13) {
this.add();
}
},
destroy: function (e) {
// console.log(this.collection.toJSON());
this.collection.each(function (model) {
if ($(e.target).html() === model.toJSON().task) {
model.destroy();
}
});
e.target.remove();
// console.log(this.collection.toJSON());
},
render: function (newModel) {
var self = this,
todoView;
todoView = new ItemView({
model: newModel
});
self.$el.find('.list').append(todoView.render().el);
return this;
}
});
var trigger = new TodoView();
And here's http://jsbin.com/ciwunizuyi/edit?html,js,output
The problem is that in your destroy method, you find the model to destroy by comparing the task property of the models. If you have multiple models with the same task property, you'll get the error. The actual error occurs because you're removing items from the collection while iterating over it.
Instead of comparing the task property, you could use the cid (client id) property that Backbone gives all models. One way to do this would be this:
When rendering an ItemView, use jQuery's data method to store the cid with the view element (alternatively, use a custom data attribute)
this.$el.data('cid', this.model.cid);
In the destroy function, get the cid property from the view element, and use it to find the right model in the collection (you can use the collection's get method here):
destroy: function (e) {
var id = $(e.target).data('cid');
var model = this.collection.get(id);
model.destroy();
e.target.remove();
},
Adding a unique attribute to the DOM element is only one way to solve this problem. One, much better, alternative would be to listen for the double-click event from the ItemView class itself. That way, you would always have a reference to this.model.
EDIT: This shows the code above in action: http://jsbin.com/nijikidewe/edit?js,output
I am working on a Backbone Project with Backbone.Layoutmanager.js
Ive got a ListView with nested ReceiverViews.
My collection is updated unordered - i want to sort these views BUT i dont want to re-render the whole collection. ( because i loose old data / event handler / graph instance inside old views. )
How to fix ?
ReceiverListView = Backbone.View.extend({
manage:true,
initialize: function(options){
_.bindAll(this, "renderReceiver","renderMe");
this.vent = _.extend({}, Backbone.Events);
this.collection.on('add', this.renderMe, this);
},
renderMe: function(model1){
this.collection.sort(this.collection.comparator);
this.insertView(new ReceiverView({model: model1})).render();
}
You don't need to call sort method manually. Learn about it: http://backbonejs.org/#Collection-sort
initialize: function () {
this.listenTo(this.collection, 'sort', _.bind(this.onSortCollection, this));
},
onSortCollection: function (collection) {
var views = {};
_.each(this.getViews(), function (view) {
if (view.model) views[view.model.cid] = view;
});
collection.each(function (model) {
var view = views[model.cid];
if (view) this.el.appendChild(view.el);
}, this);
}
Hope this helps
Ok, so I am working on a method to override the fetch method on a model. I want to be able to pass it a list of URL's and have it do a fetch on each one, apply some processing to the results, then update its own attributes when they have all completed. Here's the basic design:
A Parent "wrapper" Model called AllVenues has a custom fetch function which reads a list of URL's it is given when it is instantiated
For each URL, it creates a Child Model and calls fetch on it specifying that URL as well as a success callback.
The AllVenues instance also has a property progress which it needs to update inside the success callback, so that it will know when all Child fetch's are complete.
And that's the part I'm having problems with. When the Child Model fetch completes, the success callback has no context of the Parent Model which originally called it. I've kind of hacked it because I have access to the Module and have stored the Parent Model in a variable, but this doesn't seem right to me. The Parent Model executed the Child's fetch so it should be able to pass the context along somehow. I don't want to hardcode the reference in there.
TL;DR
Here's my jsFiddle illustrating the problem. The interesting part starts on line 13. http://jsfiddle.net/tonicboy/64XpZ/5/
The full code:
// Define the app and a region to show content
// -------------------------------------------
var App = new Marionette.Application();
App.addRegions({
"mainRegion": "#main"
});
App.module("SampleModule", function (Mod, App, Backbone, Marionette, $, _) {
var MainView = Marionette.ItemView.extend({
template: "#sample-template"
});
var AllVenues = Backbone.Model.extend({
progress: 0,
join: function (model) {
this.progress++;
// do some processing of each model
if (this.progress === this.urls.length) this.finish();
},
finish: function() {
// do something when all models have completed
this.progress = 0;
console.log("FINISHED!");
},
fetch: function() {
successCallback = function(model) {
console.log("Returning from the fetch for a model");
Mod.controller.model.join(model);
};
_.bind(successCallback, this);
$.each(this.urls, function(key, val) {
var venue = new Backbone.Model();
venue.url = val;
venue.fetch({
success: successCallback
});
});
}
});
var Venue = Backbone.Model.extend({
toJSON: function () {
return _.clone(this.attributes.response);
}
});
var Controller = Marionette.Controller.extend({
initialize: function (options) {
this.region = options.region;
this.model = options.model;
this.listenTo(this.model, 'change', this.renderRegion);
},
show: function () {
this.model.fetch();
},
renderRegion: function () {
var view = new MainView({
model: this.model
});
this.region.show(view);
}
});
Mod.addInitializer(function () {
var allVenues = new AllVenues();
allVenues.urls = [
'https://api.foursquare.com/v2/venues/4a27485af964a52071911fe3?oauth_token=EWTYUCTSZDBOVTYZQ3Z01E54HMDYEPZMWOC0AKLVFRBIEXV4&v=20130811',
'https://api.foursquare.com/v2/venues/4afc4d3bf964a520512122e3?oauth_token=EWTYUCTSZDBOVTYZQ3Z01E54HMDYEPZMWOC0AKLVFRBIEXV4&v=20130811',
'https://api.foursquare.com/v2/venues/49cfde17f964a520d85a1fe3?oauth_token=EWTYUCTSZDBOVTYZQ3Z01E54HMDYEPZMWOC0AKLVFRBIEXV4&v=20130811'
];
Mod.controller = new Controller({
region: App.mainRegion,
model: allVenues
});
Mod.controller.show();
});
});
App.start();
I think you're misunderstanding how _.bind works. _.bind returns the bound function, it doesn't modify it in place. In truth, the documentation could be a bit clearer on this.
So this:
_.bind(successCallback, this);
is pointless as you're ignoring the bound function that _.bind is returning. I think you want to say this:
var successCallback = _.bind(function(model) {
console.log("Returning from the fetch for a model");
Mod.controller.model.join(model);
}, this);
Also note that I added a missing var, presumably you don't want successCallback to be global.
I had a strange issue working with backbone and binding events. I'll see if I can explain it in a clear way (it's a cropped example...)
In a view, I had the following code in the initialize method
var MyView = Backbone.View.extend({
initialize: function(options) {
//[...]
this.items = [];
this.collection.on('reset', this.updateItems, this);
this.fetched = false;
},
render: function() {
if (!this.fetched) {
this.collection.fetch(); // fetch the collection and fire updateItems
return this;
}
this.$el = $('#my-element');
this.$el.html(this.template(this.items));
},
updateItems: function() {
this.fetched = true;
this.loadItems();
this.render(); // call render with the items array ready to be displayed
}
}
The idea is that I have to fetch the collection, process the items (this.loadItems), and then I set this.$el.
The problem I was facing, is that inside updateItems, I couldn't see any property added after the binding (this.collection.on...)
It seemed like the binding was done against a frozen version of the view. I tried adding properties to test it, but inside updateItems (and inside render if being fired by the collection reset event) I could not see the added properties.
I solved it binding the collection just before fetching it, like this:
render: function() {
if (!this.fetched) {
this.collection.on('reset', this.updateItems, this);
this.collection.fetch();
return this;
}
But it's a strange behavior. Seems like when binding, a copy of 'this' is made, instead of a reference.
Am I right? or there's anything wrong I'm doing?
You should perform your binding in the initialization phase of your collection view:
// View of collection
initialize: function() {
this.model.bind('reset', this.updateItems);
}
now when fetch is finished on the collection updateItems method will be invoked.
Of course you need to bind the model and view before doing this:
var list = new ListModel();
var listView = new ListView({model: list});
list.fetch();
What is the correct way to persist an inherited variable, on action to the parent in Backbone.js?
I can see some logical ways to do this but they seem inefficient and thought it might be worth asking for another opinion.
The two classes are both views which construct a new model to be saved to a collection, the parent passing a variable through to a popup window where this variable can be set.
I'm not sure there's enough detail in your question to answer, but there are a few ways to to do this:
Share a common model. As you describe it, you're using two views to construct a model, so the easiest way is probably to pass the model itself to the child view and have the child view modify the model, rather than passing any variables between views:
var MyModel = Backbone.Model.extend({});
var ParentView = Backbone.View.extend({
// initialize the new model
initialize: function() {
this.model = new MyModel();
},
// open the pop-up on click
events: {
'click #open_popup': 'openPopUp'
},
openPopUp: function() {
// pass the model
new PopUpView({ model: this.model })
}
});
var PopUpView = Backbone.View.extend({
events: {
'change input#someProperty': 'changeProperty'
},
changeProperty: function() {
var value = $('input#someProperty').val();
this.model.set({ someProperty : value });
}
});
Trigger an event on the parent. If for some reason you can't just pass the value via the model, you can just pass a reference to the parent and trigger an event:
var ParentView = Backbone.View.extend({
initialize: function() {
// bind callback to event
this.on('updateProperty', this.updateProperty, this);
},
updateProperty: function(value) {
// do whatever you need to do with the value here
},
// open the pop-up on click
events: {
'click #open_popup': 'openPopUp'
},
openPopUp: function() {
// pass the model
new PopUpView({ parent: this })
}
});
var PopUpView = Backbone.View.extend({
events: {
'change input#someProperty': 'changeProperty'
},
changeProperty: function() {
var value = $('input#someProperty').val();
this.options.parent.trigger('updateProperty', value);
}
});