Trying to get my head around backbone.js. This example is using Backbone Boilerplate and Backbone.localStorage and I'm coming up against a confusing problem; When quizes.create(...) is called I get this error:
backbone.js:570 - Uncaught TypeError: object is not a function
model = new this.model(attrs, {collection: this});
Quiz module code:
(function(Quiz) {
Quiz.Model = Backbone.Model.extend({ /* ... */ });
Quiz.Collection = Backbone.Collection.extend({
model: Quiz,
localStorage: new Store("quizes")
});
quizes = new Quiz.Collection;
Quiz.Router = Backbone.Router.extend({ /* ... */ });
Quiz.Views.Question = Backbone.View.extend({
template: "app/templates/quiz.html",
events: {
'click #save': 'saveForm'
},
initialize: function(){
_.bindAll(this);
this.counter = 0;
},
render: function(done) {
var view = this;
namespace.fetchTemplate(this.template, function(tmpl) {
view.el.innerHTML = tmpl();
done(view.el);
});
},
saveForm: function(data){
if (this.counter <= 0) {
$('#saved ul').html('');
}
this.counter++;
var titleField = $('#title').val();
console.log(quizes);
quizes.create({title: titleField});
}
});
})(namespace.module("quiz"));
In your Collection, you're naming model as your Quiz object, not the actual Quiz.Model. So, when you call new this.model(), you're actually calling Quiz() - which is an object, not a function. You need to change the code to:
Quiz.Collection = Backbone.Collection.extend({
model: Quiz.Model, // Change this to the actual model instance
localStorage: new Store("quizes")
});
Related
In the following code, I have set up a view where I am filtering a collection based on a date input. The code works correctly the first time, but obviously the collection resets and then if I filter by date again it doesn't filter from the original collection. What is the best practice to filter collections in backbone and maintain a copy of the original?
window.DataIndexView = Backbone.View.extend({
tagName: 'section',
className: 'data',
events: {
"click #changedate" : "filterDate"
},
initialize: function(){
_.bindAll(this, 'render', 'filterDate');
.template = _.template($("#data_template").html());
this.collection.bind('reset', this.render());
},
render: function(){
var data = this.collection;
$(this.el).html(this.template({}));
data.each(function(point){
});
return this;
},
filterDate: function() {
var startdate = this.$("#startdate").val();
var filtered = _.filter(this.collection, function(item) {
return moment(item.get("date")).valueOf() > moment(startdate, 'MM-DD-YYYY').valueOf();
});
this.collection.reset(filtered);
}
});
_.filter DOES NOT touch your collection. it returns a brand new array. You can use it to instantiate a new collection, if you wish.
I believe what you want can be achieved with something like
filterDate: function() {
var startdate = this.$("#startdate").val();
// Basically a collection clone. It'll keep the same references,
// instead of copying each object
this.original_collection = new YourCollectionType(this.collection.models);
this.collection.reset(_.filter(this.collection, function(item) {
return moment(item.get("date")).valueOf() > moment(startdate, 'MM-DD-YYYY').valueOf();
}));
// Render triggered automagically.
}
You should not filter your collection inside the view. A better way is to create a FilteredCollection from your original collection.
DateFilteredCollection
function DateFilteredCollection(original){
var filtered = new original.constructor();
filtered.filter = function(startdate){
var filteredItems = _.filter(this.collection, function(item) {
return moment(item.get("date")).valueOf() > moment(startdate, 'MM-DD-YYYY').valueOf();
});
filtered.reset(filteredItems);
};
original.on('reset', function(){
filtered.reset(original.models);
});
return filtered;
}
DataIndexView
window.DataIndexView = Backbone.View.extend({
tagName: 'section',
className: 'data',
events: {
"click #changedate" : "filterDate"
},
initialize: function(){
_.bindAll(this, 'render', 'filterDate');
.template = _.template($("#data_template").html());
this.collection.bind('reset', this.render());
},
render: function(){
var data = this.collection;
$(this.el).html(this.template({}));
data.each(function(point){
});
return this;
},
filterDate: function() {
var startdate = this.$("#startdate").val();
this.collection.filter(startdate); // use filter function here
}
});
Creating your view
var original = new OriginalCollection(); // your original collection
var filteredCollection = DateFilteredCollection( original );
var dataIndexView = new window.DataIndexView({
collection: filteredCollection
});
original.fetch(); // fetch the data -> filteredCollection will automatically filter it.
I am trying to get into backbone and have the following which is an attempt at doing an image gallery. I am trying to use render with a model in a collection. I will show the first element of the collection but I would like to add support for simply rerendering with the next element but I don't know how to do this .
I have implemented next and previous on my model like the following:
arc.Item = Backbone.Model.extend({
next: function () {
if (this.collection) {
return this.collection.at(this.collection.indexOf(this) + 1);
}
},previous: function () {
if (this.collection) {
return this.collection.at(this.collection.indexOf(this) - 1);
}
}
});
The problem here (there could be more than the one I am asking about though) is in the loadNext method. How would I get the current location in this collection and to increment it?
arc.ItemsGalleryView = Backbone.View.extend({
el: $('#mig-container'),
events: {'click .next-btn' : 'loadNext',
'click .previous-btn':'loadPrevious' },
template:_.template($('#mig-image-tmp').text()),
initialize: function() {
_.bindAll( this, 'render' );
// render the initial state
var thisModel=this.collection.first();
this.render(thisModel);
},
render: function(xModel) { // <- is it ok to do it this way?
var compiled=this.template(xModel.toJSON());
this.$el.html(compiled);
return this;
},
loadNext: function(){
console.log('i want to load Next');
this.render(this.collection.next()); // <- how would I do this
this.render(this.collection.first().next()); // <- this works by always giving me the second.
// I need to replace first with current
},
loadPrevious: function(){
console.log('i want to load Previous');
}
Or is there a better way to implement this?
thx in advance
edit #1
arc.ItemsGalleryView = Backbone.View.extend({
el: $('#mig-container'),
events: {'click .next-btn' : 'loadNext', 'click .previous-btn':'loadPrevious' },
template:_.template($('#mig-image-tmp').text()),
initialize: function() {
_.bindAll( this, 'render' );
this.render(this.collection.first()); // <- this works correct
},
render: function(xModel) {
console.log(xModel.toJSON());
var compiled=this.template(xModel.toJSON());
this.$el.html(compiled);
return this;
},
loadNext: function(){
console.log('i want to load next');
this.render(this.collection.next()); // <- this doesn't seem to do anything, event is called correctly but doesn't seem to move to next element
},
However if I adjust to this, it will load the 3rd element of the array
loadNext: function(){
console.log('i want to load Previous');
this.render(this.collection.at(2));
},
How would I use this.collection.next() to get this behavior?
thx
What it looks like you're looking for is a way to use the Collection to manipulate the next/prev stuff. What you currently have only puts it on the model. Here's a base Collection I use in my projects:
App.Collection = Backbone.Collection.extend({
constructor: function(models, options) {
var self = this;
var oldInitialize = this.initialize;
this.initialize = function() {
self.on('reset', self.onReset);
oldInitialize.apply(this, arguments);
};
Backbone.Collection.call(this, models, options);
},
onReset: function() {
this.setActive(this.first());
},
setActive: function(active, options) {
var cid = active;
if ( active instanceof Backbone.Model ) {
cid = active.cid;
}
this.each(function(model) {
model.set('current', model.cid === cid, options);
});
},
getActive: function() {
return this.find(function(model) {
return model.get('current');
});
},
next: function() {
return this.at(this.indexOf(this.getActive()) + 1);
},
prev: function() {
return this.at(this.indexOf(this.getActive()) - 1);
}
});
It's probably not perfect, but it works for me. Hopefully it can at least put you on the right track. Here is how I use it:
var someOtherCollection = App.Collection.extend({
model: MyModel
});
kalley's answer is right on, but I will throw in another example.
I would entertain the idea of keeping the current model inside of state model, and work from there. You can store other application information within the state model as well.
Your model declaration would look like the following. Notice I renamed previous to prev. Backbone.Model already has a previous method and we don't want to over-ride it.
var Model = Backbone.Model.extend({
index:function() {
return this.collection.indexOf(this);
},
next:function() {
return this.collection.at(this.index()+1) || this;
},
prev:function() {
return this.collection.at(this.index()-1) || this;
}
});
Have a generic Backbone.Model that holds your selected model:
var state = new Backbone.Model();
In the view you will listen for changes to the state model and render accordingly:
var View = Backbone.View.extend({
el: '#mig-container'
template:_.template($('#mig-image-tmp').html()),
events: {
'click .prev' : 'prev',
'click .next' : 'next'
},
initialize:function() {
this.listenTo(state,'change:selected',this.render);
state.set({selected:this.collection.at(0)});
},
render:function() {
var model = state.get('selected');
this.$el.html(this.template(model.toJSON()));
return this;
},
next:function() {
// get the current model
var model = state.get('selected');
/* Set state with the next model, fires a change event and triggers render.
Unless you are at the last model, then no event will fire.
*/
state.set({selected:model.next()});
},
prev:function() {
var model = state.get('selected');
state.set({selected:model.prev()});
}
});
Here is a demo. I like the state model approach because I'm not storing application-state information within my models.
If you don't like the state model approach, you can always just throw it on the floor:
/ .. code above ../
initialize:function() {
this.model = this.collection.at(0);
this.render();
},
render:function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
next:function() {
this.model = this.model.nxt();
this.render();
},
prev:function() {
this.model = this.model.prev();
this.render();
}
How can I modify a single model from different views?
Pseudo code:
var myModel = Backbone.Model.extend({url: 'rest'});
var myCollection = Backbone.Model.extend({});
var myView1 = Backbone.Views.extend({
initialize: function() {
// sets a data in model so it can be use by second and third view
myModel().fetch();
},
events: {
'event': 'callSecondView',
'event': 'callThirdView'
},
callSecondView: function() {
// second view will use the current records in myModel that
// was created during initialization or from the modification
// of 1st or third view
new myView2({model: myModel})
},
callThirdView: function() {
// third view will use the current records in myModel that
// was created during initialization
// of 1st or third view
new myView3({model: myModel})
}
});
var myView2 = Backbone.Views.extend({
events: {
'event': 'modifyMyModel'
},
modifyMyModel: function() {
this.model.set(etc)
// Modifying model here should affect the 1st and 3rd view's model
}
});
var myView3 = Backbone.Views.extend({
events: {
'event': 'modifyMyModel'
},
modifyMyModel: function() {
this.model.set(etc)
// Modifying model here should affect the 1st and 2nd view's model
}
});
Is this possible? or should I just combine all the three views in a single view?(this will result into a monster view)
It looks very ugly why don't you fetch the model first then create how many views with it as you want:
var MyModel = Backbone.Model.extend({url: '/api/data'});
var View1 = Backbone.View.extend({});
var View2 = Backbone.View.extend({});
var View3 = Backbone.View.extend({});
var myModel = new MyModel();
myModel.fetch({
success: function(){
window.myView1 = new View1({model:myModel});
window.myView2 = new View1({model:myModel});
window.myView3 = new View1({model:myModel});
}
});
// in console
myView1.model == myModel
true
I am trying to trigger behaviour on a collection by triggering an event elsewhere in my app. I am pretty new to backbone, so probably have all my syntax wrong, but this seems like it should work
fiddle
var Test = Backbone.Model.extend({
});
var Tests = Backbone.Collection.extend({
model: Test,
events: {
"testEvent":"testResponce"
},
testResponce: function(){
alert('hello world');
}
});
var myTest = new Test();
myTest.trigger('testEvent');
Can this be done? Where am I going wrong?
If you want to call a particular method of your collection using testEvent event then you can take this path also. Working Demo
var Test = Backbone.Model.extend({
initialize: function() {
this.on('testEvent', function() {
console.log(this.collection);
this.collection.testResponce();
});
}
});
var Tests = Backbone.Collection.extend({
model: Test,
testResponce: function(){
alert('hello world');
}
});
var myTest = new Test();
var testList = new Tests([myTest]);
myTest.trigger('testEvent');
The event that you are triggering will have to be caught inside the model.
If you want to catch an event inside collection you can use Backbone.on and Backbone.trigger OR collection.on and collection.trigger
Please check the fiddle below for an example
fiddle
var Test = Backbone.Model.extend({});
var Tests = Backbone.Collection.extend({
model: Test,
initialize: function () {
Backbone.on('testEvent', function () {
alert('event handled in collection');
});
}
});
var myCollection = new Tests();
var myTest = new Test();
Backbone.trigger('testEvent');
UPDATE
Collection has an initialize method which you can use to register events on. Later you can trigger these events from their instances.
Other way as NULL suggested is to do it like below.
var Test = Backbone.Model.extend({});
var Tests = Backbone.Collection.extend({
model: Test,
initialize: function () {
this.on('testEvent', function () {
alert('event handled in collection');
});
}
});
var myCollection = new Tests();
var myTest = new Test();
myCollection.trigger('testEvent');
I've spent the past two weeks trying to learn Backbone.js and then also modularizing the app with Require.js. But seems there are something that I'm not getting in the whole process of initialization and fetching.
I have two routes, one shows an entire collection while the other shows just an individual model. And I want to be able to start the app with any of both routes.
If I start loading the collections url and later on a single model url, everything works as expected. If I start the app with the url route to a single model I got the error: TypeError: this.model is undefined this.$el.html(tmpl(this.model.toJSON()));on the view.
If I set defaults for the model, it renders the view but doesn't fetch it with the real data. I've tried also to handle the success event in the fetch function of the model without any luck.
router.js
define(['jquery','underscore','backbone','models/offer','collections/offers','views/header','views/event','views/offer/list',
], function($, _, Backbone, OfferModel, OffersCollection, HeaderView, EventView, OfferListView){
var AppRouter = Backbone.Router.extend({
routes: {
'event/:id' : 'showEvent',
'*path': 'showOffers'
},
initialize : function() {
this.offersCollection = new OffersCollection();
this.offersCollection.fetch();
var headerView = new HeaderView();
$('#header').html(headerView.render().el);
},
showEvent : function(id) {
if (this.offersCollection) {
this.offerModel = this.offersCollection.get(id);
} else {
this.offerModel = new OfferModel({id: id});
this.offerModel.fetch();
}
var eventView = new EventView({model: this.offerModel});
$('#main').html(eventView.render().el);
},
showOffers : function(path) {
if (path === 'betting' || path === 'score') {
var offerListView = new OfferListView({collection: this.offersCollection, mainTemplate: path});
$('#main').html(offerListView.render().el) ;
}
},
});
var initialize = function(){
window.router = new AppRouter;
Backbone.history.start();
};
return {
initialize: initialize
};
});
views/event.js
define(['jquery','underscore','backbone','text!templates/event/main.html',
], function($, _, Backbone, eventMainTemplate){
var EventView = Backbone.View.extend({
initalize : function(options) {
this.model = options.model;
this.model.on("change", this.render);
},
render : function() {
var tmpl = _.template(eventMainTemplate);
this.$el.html(tmpl(this.model.toJSON()));
return this;
}
});
return EventView;
});
You are creating and fetching the OffersCollection in initialize method of the router, so the else block in showEvent will never be hit since this.offersCollection is always truthy.
After the comments, I think you need to do this:
showEvent : function(id) {
var that = this;
var show = function(){
var eventView = new EventView({model: that.offerModel});
$('#main').html(eventView.render().el);
};
// offersCollection is always defined, so check if it has the model
if (this.offersCollection && this.offersCollection.get(id)) {
this.offerModel = this.offersCollection.get(id);
show();
} else {
this.offerModel = new OfferModel({id: id});
this.offerModel.fetch().done(function(){
// model is fetched, show now to avoid your render problems.
show();
});
// alternatively, set the defaults in the model,
// so you don't need to wait for the fetch to complete.
}
}