backbone extension file loads, some helpers work, one doesn't - backbone.js

I have a backbone-extend.js file that I load in the require define in app.js. It has a Backbone.View extender class defining a couple helper methods. Two of the methods work just fine in my views, one always errors with Uncaught TypeError: Object [object global] has no method 'gotoUrl'. Why would just this one method be not defined but the other two are working fine? Do you see any issue in this code...
// Filename: backbone-extend.js
define([
'jquery',
'underscore',
'backbone'
], function($, _, Backbone) {
var helpers = {
eventSyncError: function(model,response,options) {
console.log('Sync error='+response.statusText);
$('#server-message').css({'color':'red', 'font-weight':'bold'}).text(response.statusText);
},
gotoUrl: function(url,delay) {
var to = setTimeout(function() { Backbone.history.navigate(url, true); }, delay);
},
getFormData: function(form) {
var unindexed_array = form.serializeArray();
var indexed_array = {};
$.map(unindexed_array, function(n, i) {
indexed_array[n['name']] = n['value'];
});
return indexed_array;
}
}
_.extend(Backbone.View.prototype, helpers);
});
Here is the code in view that calls it...
eventSyncMemberSaved: function(model,response,options) {
console.log("Member saved!");
$('#server-message').css({'color':'green', 'font-weight':'bold'}).text("Member saved!");
this.gotoUrl('members',2000);
//setTimeout(function() { Backbone.history.navigate('members', true); }, 2000);
},
saveMember: function() {
var data = this.getFormData($('#member-form'));
this.member.save(data, { success: this.eventSyncMemberSaved });
},
Thanks in advance for your help. I'm stuck.

The context of this is different in the success callback.
It no longer points to the view as it points to the xhr object
So it throws an error as that method is not available on the xhr object
To resolve it you need to bind the context of this to the success handler so that it points to the right object.
So in the initialize of the view add this code
initialize: function() {
_.bindAll(this, 'eventSyncMemberSaved');
}

Related

How to call a Layout from itself to make an instance

I have a Layout(SubMenu100) that have an event to call other Layout(SubMenu200), but it also make a onClick trigger that will create an instance if layout(SubMenu100) and call itself, but I don't know how to get an instance like the next code.
this.SubMenu100 = new SubMenu100();
I try to put it in Define vars but get me an error.
define([
'backbone.marionette',
'underscore',
'logger',
'tpl!apps/templates/SubMenu100.html',
'i18n!apps/nls/Messages',
'apps/views/SubMenu200',
'apps/views/SubMenu100'
], function (Marionette, _, Logger, Template, i18n, SubMenu200, SubMenu100) {
launch: function (e) {
$("#title_wrapper_div").click(this.callMe);
},
callMe: function () {
if (this.subMenu100 === undefined) {
this.subMenu100 = new SubMenu100(); // <- here
}
window.App.vent.trigger("dashboard:showView",this.subMenu100, "", "", "", "");
}
}
So, this is the question, How can I create an instance of SubMenu100 inside within itself?
If launch is called with context of SubMenu100, you can do this.callMe.bind(this) then you should be able to do new this() inside callMe.
But for simplicity you could do this :
define([
'backbone.marionette',
'underscore',
'logger',
'tpl!apps/templates/SubMenu100.html',
'i18n!apps/nls/Messages',
'apps/views/SubMenu200'
], function(Marionette, _, Logger, Template, i18n, SubMenu200) {
var SubMenu100 = Class SubMenu100 {
launch: function(e) {
$("#title_wrapper_div").click(this.callMe);
},
callMe: function() {
if (this.subMenu100 === undefined) {
this.subMenu100 = new SubMenu100();
}
window.App.vent.trigger("dashboard:showView", this.subMenu100, "", "", "", "");
}
}
return SubMenu100;
});
Note that every launch will create new event handler potentially on the same element (unless something else is doing proper cleanup) and this can lead to bugs. I don't recommend using global jQuery selectors in backbone components.
If you want to create instances of SubMenu100 and SubMenu200 I'd create a higher level component in charge of that:
require([
// ^ ------ this is NOT SubMenu100 definition
'apps/views/SubMenu100',
'apps/views/SubMenu200',
], function (SubMenu100, SubMenu200) {
// this is NOT SubMenu100 definition
launch: function (e) {
$("#title_wrapper_div").click(this.callMe);
},
callMe: function () {
if (this.subMenu100 === undefined) {
this.subMenu100 = new SubMenu100();
}
}
});

Backbone error: Model is not a constructor

Afternoon all, I'm relatively new to backbone and have been stumped for 3 days with this error which I have not seen before.
I have a collection 'TestCollection' which defines it's model as a function. When the collection is loaded I get an error the first time it attempts to make a model with class 'TestModel'.
The error I get is:
Uncaught TypeError: TestModel is not a constructor
at new model (testCollection.js:14)
at child._prepareModel (backbone.js:913)
at child.set (backbone.js:700)
at child.add (backbone.js:632)
at child.reset (backbone.js:764)
at Object.options.success (backbone.js:860)
at fire (jquery.js:3143)
at Object.fireWith [as resolveWith] (jquery.js:3255)
at done (jquery.js:9309)
at XMLHttpRequest.callback (jquery.js:9713)
I believe I have given both the collection and the model all of the code they should need to work. It feels like something has gone wrong with the loading, but when I put a console.log at the top of the model file I could see that it is definitely being loaded before the collection attempts to use it.
Any help would be massively appreciated.
TestCollection:
define([
'backbone',
'models/testModel'
], function(Backbone, TestModel) {
var TestCollection = Backbone.Collection.extend({
model: function(attrs) {
switch (attrs._type) {
case 'test':
console.log('making a test model')
return new TestModel();
}
},
initialize : function(models, options){
this.url = options.url;
this._type = options._type;
this.fetch({reset:true});
}
});
return TestCollection;
});
TestModel:
require([
'./testParentModel'
], function(TestParentModel) {
var TestModel = TestParentModel.extend({
urlRoot: 'root/url',
initialize: function() {
console.log('making test model')
}
});
return TestModel;
});
File where TestCollection is made:
define(function(require) {
var MyProjectCollection = require('collections/myProjectCollection');
var TestCollection = require('collections/testCollection');
Origin.on('router:dashboard', function(location, subLocation, action) {
Origin.on('dashboard:loaded', function (options) {
switch (options.type) {
case 'all':
var myProjectCollection = new MyProjectCollection;
myProjectCollection.fetch({
success: function() {
myProjectCollection.each(function(project) {
this.project[project.id] = {};
this.project[project.id].testObjects = new TestCollection([], {
url: 'url/' + project.id,
_type: 'test'
});
});
}
});
}
});
});
I've had a look around stack overflow, it does not appear to be the issue below (which seems to be the most common issue).
Model is not a constructor-Backbone
I also do not think I have any circular dependencies.
Any help would be massively appreciated as I am completely stumped. I've tried to include only the relevant code, please let me know if additional code would be useful.
Thanks
I can't say for other parts of the code but an obvious problem you have is misunderstanding what data is passed to the model creator function.
var TestCollection = Backbone.Collection.extend({
model: function(attrs) {
switch (attrs._type) { // attrs._type does exist here
console.log( attrs ); // Will print { foo: 'bar' }
case 'test': // This will always be false since attrs._type does not exist
console.log('making a test model')
return new TestModel();
default:
return new Backbone.Model(); // Or return some other model instance,
// you MUST have this function return
// some kind of a Backbone.Model
}
},
initialize : function(models, options){
this.url = options.url;
this._type = options._type;
this.fetch({reset:true});
}
});
new TestCollection([ { foo: 'bar' }], {
url: 'url/' + project.id,
_type: 'test' // This will NOT be passed to the model attrs, these are
// options used for creating the Collection instance.
})
To re-iterate. When you instantiate a Collection you pass an array of plain objects [{ foo: 'bar'}, { foo: 'baz'}] ( or you get them via fetch like you're doing ). That object will be passed as the attrs parameter in the model function, and the model function MUST return at least some kind of a Backbone.Model instance so you need a fallback for your switch statement.

Small Jasmine test for Javascript file: Best approach to test it

First of all I want to say that I am new in Jasmine, so I beg for your kind comprehension if the question is very basic. I am writing a test for this file:
define([
'q',
'backbone',
'marionette',
'education/eet/views/destinationview',
'education/eet/views/editdestinationview',
'education/eet/models/destination',
'common/ajaxerrorhandler',
'common/alertdialog'
], function (Q, Backbone, Marionette, DestinationView, EditDestinationView, Destination, AjaxErrorHandler, AlertDialog) {
'use strict';
var ReferenceDataController = Marionette.Controller.extend({
initialize: function (options) {
this._subjectCompositeId = options.subjectCompositeId;
},
getView: function (destinationTypes, editMode) {
var self = this,
deferred = Q.defer(),
destination = new Destination();
destination.fetch({
data: {subjectCompositeId: self._subjectCompositeId}
}).done(function () {
var view;
if (editMode) {
view = new EditDestinationView({
model: destination,
'destinationTypes': destinationTypes
});
view.on('click:saveDestination', self._handleSaveDestination, view);
} else {
view = new DestinationView({
model: destination
});
}
deferred.resolve(view);
}).fail(function (jqXHR) {
deferred.reject(jqXHR);
});
return deferred.promise;
},
_handleSaveDestination: function () {
if (this.model.isValid(true)) {
this.model.save(null, {
success: function () {
Backbone.Wreqr.radio.vent.trigger('education', 'show:destination');
},
error: function (jqXHR) {
var userFriendlyErrorString = AjaxErrorHandler.buildDefaultErrorMessage(jqXHR);
return new AlertDialog(userFriendlyErrorString);
}
});
}
}
});
return ReferenceDataController;
});
The problem is that I am not very sure about how can I access the variables inside it to test it. I am a Java Tester but never test Javascript even when I wrote, so I am very confused with it.
Any hint or code will be actually appreciated.
Thanks.
Think of Jasmine suite/spec as your application that is dependent on this module.
We do our specs as RequireJS modules that require the appropriate module, instantiate it - sometimes on module level, sometimes on suite (describe) level, sometimes on spec (it) level.
At this point, due to you (in it) having an access to an actual instance of the class, you invoke its various methods and test for the results using various asserts in the form of
expect(something).toBeTruthy();
or similar.

Backbone.js model trigger change only if model.on called again

It's my first post here so please be nice ;) I'm trying to create a Backbone+requireJS sample app and I have stumbled upon a strange problem. In my views initialize function I bind to models change event by
this.model.on("change", this.change());
Event is triggered correctly when data is loaded and all is fine. In my view I also bind to a click event. On click I try to change one of the properties and I was hoping for a change event, but it never came.
I was trying some stuff recommended here on stackoverflow (here, here, and there) but with no luck.
In my guts I feel it has to do with event binding. When I tried to bind again to models on change event inside click callback it started to work. So now I sit here and I'm, hm, confused. Could someone shed some light on this issue?
My View:
define(
[
'jquery',
'underscore',
'backbone',
'text!templates/news/newsListItem.html'
],
function($, _, Backbone, newsListItemTemplate)
{
var NewsListItemViewModel = Backbone.View.extend({
tagName: 'li',
events: {
"click a" : "onLinkClick"
},
initialize: function(){
this.model.on("change", this.changed());
},
render: function()
{
this.$el.html(_.template(newsListItemTemplate, this.model.toJSON()));
},
changed: function()
{
this.render();
console.log("changed");
},
//GUI functions
onLinkClick: function(e)
{
console.log("click!");
this.model.toggle();
this.model.on("change", this.changed());
}
});
var init = function(config){
return new NewsListItemViewModel(config);
}
return {
init : init
}
}
);
My Model:
define(
['jquery', 'underscore', 'backbone'],
function($, _, Backbone){
var NewsListItemModel = Backbone.Model.extend({
toggle: function() {
this.set('done', !this.get('done'));
this.trigger('change', this);
}
});
var init = function(data)
{
return new NewsListItemModel(data);
}
return {
init: init,
getClass: NewsListItemModel
};
}
);
Thanks for any input :)
First, you should use a function as event handler - not its result.
Hence, change the line into this:
this.model.on("change", this.changed.bind(this));
As it stands now, you actually fire this.changed() function just once - and assign its result (which is undefined, as the function doesn't have return statement) to be the model's change event handler.
Second, you shouldn't rebind this handler in onLinkClick: once bound, it'll stay here. It looks like it's more appropriate to trigger this event instead:
onLinkClick: function(e)
{
console.log("click!");
this.$el.toggle();
this.model.trigger('change');
}

When is the right time to fetch a collection in Backbone js?

So I'm working on a backbone app, and trying to modularize things as much as I can using require.js. This has been my guide.
I'm having trouble getting my view to always fetch my collection. If I access my app from the base url (myapp.com/), and go to the route of my view, the collection is fetched. If I do not go to the view, and instead access it from myapp.com/#/campaigns, then the collection is not fetched.
Here is some relevant code.
router.js
define([
'jQuery',
'Underscore',
'Backbone',
'views/home/main',
'views/campaigns/list'
], function($, _, Backbone, mainHomeView, campaignListView ){
var AppRouter = Backbone.Router.extend({
routes: {
// Define some URL routes
'campaigns': 'showCampaigns',
// Default
'*actions': 'defaultAction'
},
showCampaigns: function(){
campaignListView.render();
},
defaultAction: function(actions){
// We have no matching route, lets display the home page
//mainHomeView.render();
}
});
var initialize = function(){
var app_router = new AppRouter;
Backbone.history.start();
};
return {
initialize: initialize
};
});
collections/campaigns.js
define([
'jQuery',
'Underscore',
'Backbone',
'models/campaigns'
], function($, _, Backbone, campaignsModel){
var campaignsCollection = Backbone.Collection.extend({
model: campaignsModel,
url: '/campaigns',
initialize: function(){
}
});
return new campaignsCollection;
});
views/campaigns/list.js
define([
'jQuery',
'Underscore',
'Backbone',
'collections/campaigns'
], function($, _, Backbone, campaignsCollection){
var campaignListView = Backbone.View.extend({
el:$('#container'),
initialize:function(){
this.collection = campaignsCollection;
this.collection.fetch();
},
render: function(){
var data = {
campaigns: this.collection,
_: _
};
$('#container').html('Campaigns length: '+data.campaigns.models.length);
}
});
return new campaignListView;
});
Any ideas on what I'm doing wrong? I believe it has something to do with calling this.collection.fetch() in the initalize function of the view. If that is the issue, where should I put fetch()?
The problem in your code is that your campaignListView fetch the collection when it is initialized and it is initialized only once. Your render method which is actually called from the router doesn't call your initialize method of campaignListView, when you change theurl your second time.
You have two options here :
1. return the class not the instance of your campaignListView and initialize it in the router :
// in your router code
showCampaigns: function(){
var campaignListViewInstance = new campaignListView(); // don't forget the brackets after your initialize function
campaignListViewInstance.render();
},
// in your campaignListView code :
return campaignListView; // without "new"
This will initialize the code everytime the router is hit.
2. place your fetch in the render method
// in your campaignListView code :
initialize:function(){
this.collection = campaignsCollection;
},
render: function(){
this.collection.fetch({success: function(collection) {
var data = {
campaigns: collection,
_: _
};
$('#container').html('Campaigns length: '+data.campaigns.models.length);
}); // this will be fetched everytime you render and also it has success callback
}
Also be aware that you should replace all your instance creating functions with brackets at the end
return new campaignListView; --> return new campaignListView();
return new campaignsCollection; --> return new campaignsCollection();
Another problem you will face is the async work of fetch. You should use success or event driven rendering of your collection, because fetch works in the background and you will have no data when you immediately call render after calling fetch.
+1 for your AMD approach.
I should really update the tutorial but it's better to not return instantiated modules. Maybe try checking out http://backboneboilerplate.com

Resources