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.
}
}
Related
I'm trying to render a partial view within a Backbone View with it's render method. I created a sort of helper to do this.
var DashboardPartial = (function(){
var _getPartialView = function() {
$.ajax({
url: _baseUrl + _url,
})
.done(function(response) {
_returnView(response);
})
.fail(function() {
console.log("error");
})
.always(function() {
console.log("complete");
});
};
var _returnView = function (response) {
return response;
};
return {
get: function (url) {
_url = url;
_baseUrl = '/dashboard/';
_getPartialView();
},
};
}());
So, what I want to do is call DashboardPartial.get('url') and use the response within the Backbones View render method. Something like the following:
render: function() {
partial = DashboardPartial.get('url');
this.$el.html(partial);
return this;
}
The problem is that the function does get the partial from the server, but I can't find a way to return the response. Doing console.log(response) inside the DashboardPartial function does show the partial, but I want to be able to return it and then pass it as a variable to "this.$el.html()".
You should return deferred ($.ajax returns it by default) from helper:
var DashboardPartial = (function(){
var _getPartialView = function() {
return $.ajax({
url: _baseUrl + _url,
});
};
return {
get: function (url) {
_url = url;
_baseUrl = '/dashboard/';
return _getPartialView();
},
};
}());
And then use it in your render:
render: function() {
var self = this,
dfd = $.Deferred();
DashboardPartial.get('url').done(function(partial){
self.$el.html(partial);
dfd.resolve();
});
return dfd; // use this outside, to know when view is rendered; view.render().done(function() { /* do stuff with rendered view */});
}
However, you could use requirejs for this, plus requirejs-text plugin to load templates, because your view has dependency on partial.
As I understood, you want render different partials using one backbone view.
You could create factory, smth like this:
var typeToTemplateMap = {};
typeToTemplateMap["backboneViewWithFirstPartial"] = firstPartial;
function(type) {
return typeToTemplateMap[type];
}
And then use it in your view:
initialize: function(options) {
this.partial = partialFactory(options.type);
},
render: function() {
this.$el.html(this.partial);
return this;
}
This is how it would look like using requirejs:
// factory.js
define(function(require) {
var partialOne = require("text!path/to/partialOne.htm"), // it's just html files
partialTwo = require("text!path/to/partialTwo.htm");
var typeToPartialMap = {};
typeToPartialMap["viewWithFirstPartial"] = partialOne;
typeToPartialMap["viewWithSecondartial"] = partialTwo;
return function(type) {
return typeToPartialMap[type];
}
});
// view.js
define(function(require){
var Backbone = require("backbone"), // this paths are configured via requirejs.config file.
partialFactory = require("path/to/factory");
return Backbone.View.extend({
initialize: function(options) {
this.partial = partialFactory(options.type);
},
render: function() {
this.$el.html(this.partial);
return this;
}
});
});
// Another_module.js, could be another backbone view.
define(function(require) {
var MyView = require("path/to/MyView"),
$ = require("jquery");
var myView = new MyView({type: "firstPartial"});
myView.render(); // or render to body $("body").append(myView.render().$el);
});
You should consider using requirejs, because you doing almost the same, but without dependencies handling.
Docs on requirejs can be found here: http://requirejs.org/docs/start.html
I'm trying to add a language support in my website and I need to add this code so it will run before marionette render in all the views no matter which type.
onBeforeRender: function(){
var helpers = this.templateHelpers();
this.templateHelpers = function(){
return $.extend( (helpers), {
lang : function () {
return function(val, render) {
return lang(val);
}
}
});
}
}
I don't want to extend all the views and put this code in each of them,
I wonder if there is a way to just put this code in some place and it will run before every render
You should be able to extend the prototype with something like
_.extend(Marionette.View.prototype, {
onBeforeRender: function(){
var helpers = this.templateHelpers();
this.templateHelpers = function(){
return $.extend( (helpers), {
lang : function () {
return function(val, render) {
return lang(val);
}
}
});
}
}
})
Naturally, that means that if one of your marionette views defines its own onBeforeRender, you'll need to call the implementation on the View prototype "by hand".
I think you should create a view mixin with your code and extend every view with this mixin
var LangMixin = {
onBeforeRender: function(){
var helpers = this.templateHelpers();
this.templateHelpers = function(){
return $.extend( (helpers), {
lang : function () {
return function(val, render) {
return lang(val);
}
}
});
}
}
}
var YourView= Backbone.View.extend({
// ...
});
_.extend(YourView.prototype, LangMixin);
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();
}
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")
});
I have just started with backbone.js. And I'm having a problem in fetching the data from the server. Here's the response I'm getting from server.
[{
"list_name":"list1",
"list_id":"4",
"created":"2011-07-07 21:21:16",
"user_id":"123456"
},
{
"list_name":"list2",
"list_id":"3",
"created":"2011-07-07 21:19:51",
"user_key":"678901"
}]
Here's my javascript code...
// Router
App.Routers.AppRouter = Backbone.Router.extend({
routes: {
'': 'index'
},
initialize: function() {
},
index: function() {
var listCollection = new App.Collections.ListCollection();
listCollection.fetch({
success: function() {
new App.Views.ListItemView({collection: listCollection});
},
error: function() {
alert("controller: error loading lists");
}
});
}
});
// Models
var List = Backbone.Model.extend({
defaults: {
name: '',
id: ''
}
});
App.Collections.ListStore = Backbone.Collection.extend({
model: List,
url: '/lists'
});
// Initiate Application
var App = {
Collections: {},
Routers: {},
Views: {},
init: function() {
var objAppRouter = new App.Routers.AppRouter();
Backbone.history.start();
}
};
I get the error "Can't add the same model to a set twice" on this line in Backbone.js
if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
I checked out the Backbone.js annotated and found out that the first model gets added to the collection but the second one gives this error. Why is this happening? Should I change something in the server side response?
Your List has id in its defaults property, which is making each instance have the same ID by default, and Backbone is using that to detect dupes. If your data uses list_id as the ID, you need to tell that to Backbone by putting idAttribute: 'list_id' inside your List class definition.
As an aside, I prefer to NOT duplicate type information in object attributes (and Backbone.js agrees on this point). Having consistent attribute names is what backbone expects and is easier to work with. So instead of having list_id and list_name, just use id, and name on all classes.
Use this fix to add models with same id.
When adding, use: collection.add(model,{unique: false})
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Backbone.Collection = (function(_super) {
__extends(Collection, _super);
function Collection() {
return Collection.__super__.constructor.apply(this, arguments);
}
Collection.prototype.add = function(models, options) {
var i, args, length, model, existing;
var at = options && options.at;
models = _.isArray(models) ? models.slice() : [models];
// Begin by turning bare objects into model references, and preventing
// invalid models from being added.
for (i = 0, length = models.length; i < length; i++) {
if (models[i] = this._prepareModel(models[i], options)) continue;
throw new Error("Can't add an invalid model to a collection");
}
for (i = models.length - 1; i >= 0; i--) {
model = models[i];
existing = model.id != null && this._byId[model.id];
// If a duplicate is found, splice it out and optionally merge it into
// the existing model.
if (options && options.unique) {
if (existing || this._byCid[model.cid]) {
if (options && options.merge && existing) {
existing.set(model, options);
}
models.splice(i, 1);
continue;
}
}
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
this._byCid[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
}
// Update `length` and splice in new models.
this.length += models.length;
args = [at != null ? at : this.models.length, 0];
Array.prototype.push.apply(args, models);
Array.prototype.splice.apply(this.models, args);
// Sort the collection if appropriate.
if (this.comparator && at == null) this.sort({silent: true});
if (options && options.silent) return this;
// Trigger `add` events.
while (model = models.shift()) {
model.trigger('add', model, this, options);
}
return this;
};
return Collection;
})(Backbone.Collection);
Backbone prevent us to insert the same model into one collection...
You can see it in backbone.js line 676 to line 700
if you really want to insert the same models into collection,just remove the code there
if(existing = this.get(model)){//here
...
}