We use RequireJS to add modularity to our Backbone.js site. I found myself with the need to override the Backbone.Collection class to add an advance filtering routine.
My questions is, say I have the following 'override',
Backbone.Collection.prototype.advanceFilter = function() {
/* Filtering code here */
};
and our site structure looks like the following:
where, main.js sits at the top level and beneath it is app.js; Where would I add this override, such that I don't have to add a new module to our RequireJS definition for every class? More generally, where are overrides to Backbone usually recommended?
Create a file (say Overrides.js in modules folder)
define(function(require){
var app = require('app');
Backbone.Collection.prototype.advanceFilter = function() {
/* Filtering code here */
};
// other overrides can also be added here in this file like
_.extend(Backbone.View.prototype,{}, {
// adding functions or overriding something
})
});
Now, require this file in main.js like
require([
'backbone',
'App',
'modules/Overrides',
'globalize',
.
.
.
],
function ( Backbone, App, ..... ) {
});
There you go!
Say, I want to add some function to the view or override some function such as render, initialize, remove,... universally in the application. You could do something like this:
_.extend(Backbone.View.prototype,{}, {
remove: function() {
alert("View removed");
this.$el.remove();
this.stopListening();
return this;
}
});
One easy option if using requirejs, in your require config add an init statement. eg,
require.config({
shim: {
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone',
init: function (_) {
Backbone.Collection.prototype.advanceFilter = function() {
/* Filtering code here */
};
}
}
}
});
Alternatively you can use a map config call to amp all backbone calls to your overridden backbone,
require.config({
map: {
'*': { 'backbone': 'backbone-custom' },
'backbone-custom': { 'backbone': 'backbone' }
}
});
// backbone-custom.js file:
define(['backbone'], function (Backbone) {
Backbone.Collection.prototype.advanceFilter = function() {
/* Filtering code here */
};
return Backbone;
});
Either case will load the override into the backbone object before it is used anywhere.
Related
I'm using an authentication example from here https://github.com/alexanderscott/backbone-login and instead of using underscore templates I would like to use doT.js templates.
I've added the doT.js source to the lib directory and my config.js looks like this
if (typeof DEBUG === 'undefined') DEBUG = true;
require.config({
baseUrl: '/assets',
paths: {
//'jquery' : 'http://code.jquery.com/jquery-1.10.2.js',
'jquery' : 'assets/lib/jquery',
'underscore' : 'assets/lib/underscore', // load lodash instead of underscore (faster + bugfixes)
'backbone' : 'assets/lib/backbone',
'bootstrap' : 'assets/vendor/bootstrap/js/bootstrap',
'doT' : 'assets/lib/doT',
'text' : 'assets/lib/text',
'parsley' : 'assets/lib/parsley'
},
// non-AMD lib
shim: {
//'jquery' : { exports : '$' },
'underscore' : { exports : '_' },
'backbone' : { deps : ['underscore', 'jquery'], exports : 'Backbone' },
'bootstrap' : { deps : ['jquery'], exports : 'Bootstrap' },
'parsley' : { deps: ['jquery'] },
'doT' : { exports : 'doT'}
}
});
require(['main']); // Initialize the application with the main application file.
my app.js looks like this
define([
"jquery",
"underscore",
"backbone",
"doT",
"utils"
],
function($, _, Backbone, doT) {
var app = {
root : "/", // The root path to run the application through.
URL : "/", // Base application URL
API : "/api", // Base API URL (used by models & collections)
// Show alert classes and hide after specified timeout
showAlert: function(title, text, klass) {
$("#header-alert").removeClass("alert-error alert-warning alert-success alert-info");
$("#header-alert").addClass(klass);
$("#header-alert").html('<button class="close" data-dismiss="alert">×</button><strong>' + title + '</strong> ' + text);
$("#header-alert").show('fast');
setTimeout(function() {
$("#header-alert").hide();
}, 7000 );
}
};
$.ajaxSetup({ cache: false }); // force ajax call on all browsers
//alert(doT.template("what up {{=it.name}}"),{'name': 'John'});
// Global event aggregator
app.eventAggregator = _.extend({}, Backbone.Events);
return app;
});
and HeaderView.js looks like this
define([
"app",
"text!templates/header.html",
"utils",
"bootstrap"
], function(app, HeaderTpl){
var HeaderView = Backbone.View.extend({
template: doT.template(HeaderTpl), //_.template(HeaderTpl),
initialize: function () {
_.bindAll(this);
// Listen for session logged_in state changes and re-render
app.session.on("change:logged_in", this.onLoginStatusChange);
},
events: {
"click #logout-link" : "onLogoutClick",
"click #remove-account-link" : "onRemoveAccountClick"
},
onLoginStatusChange: function(evt){
this.render();
if(app.session.get("logged_in")) app.showAlert("Success!", "Logged in as "+app.session.user.get("username"), "alert-success");
else app.showAlert("See ya!", "Logged out successfully", "alert-success");
},
onLogoutClick: function(evt) {
evt.preventDefault();
app.session.logout({}); // No callbacks needed b/c of session event listening
},
onRemoveAccountClick: function(evt){
evt.preventDefault();
app.session.removeAccount({});
},
render: function () {
if(DEBUG) console.log("RENDER::", app.session.user.toJSON(), app.session.toJSON());
this.$el.html(this.template({
logged_in: app.session.get("logged_in"),
user: app.session.user.toJSON()
}));
return this;
},
});
return HeaderView;
});
when i load the page I get the error
Uncaught ReferenceError: doT is not defined
I can call the doT.template() function in the app.js file and I can see that doT.js is loaded in my network tab but when I try and use it in the HeaderView.js I keep getting the error. I am new to require.js so I'm sure I'm misunderstanding how it works.
Looking at the source of doT I see that it calls define by itself. So you do not need a shim configuration for it. Providing a shim for a module that calls define can confuse RequireJS.
Moreover, in the case at hand here, I see that if doT detects that it is an AMD environment (which RequireJS is), then it does not define itself in the global space as doT. So your HeaderView.js file will have to have doT among the required modules. Something like:
define([
"app",
"text!templates/header.html",
"doT",
"utils",
"bootstrap"
], function(app, HeaderTpl, doT){
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');
}
I am building a Backbone app using require.js for modular loading and Marionette to help with my application structuring and functionality. I have set up a require module for the event aggregator like this:-
define(['backbone', 'marionette'],function(Backbone, Marionette){
var ea = new Backbone.Wreqr.EventAggregator();
ea.on('all', function (e) { console.log("[EventAggregator] event: "+e);});
return ea;
});
I was hoping to pass it into my other require modules and have it function as a central event handling and messaging component and I am getting some success with this. I can pass the vent as a dependency into other modules without problem like so:-
define(['marionette', 'text!templates/nav.html', 'shell/vent'], function (Marionette, text, vent) {
return SplashView = Marionette.ItemView.extend({
template : text,
events : {
'click #splashContinueButton': 'onButtonClick'
},
onButtonClick : function(evt) {
vent.trigger('onSplashContinueClick');
}
});
});
The problem I am having is that although all the events are getting triggered across the different places in my app (which I can see in the console log), I am not able to listen to them in some parts of my app. For instance I have a Marionette module (loaded at runtime as a require module) which is trying to pick up some events like this:-
var SplashModule = shellApp.module("SplashModule");
SplashModule.addInitializer(function(){
vent.on("onSplashModelLoaded", this.onSplashModelLoaded);
vent.on("onSplashContinueClick", this.onSplashContinueClick);
}
I get nothing, even though if I log the vent from this place I can see it as an object. In the log, it contains an array of events that actually only contain the events being listened to by the root level application, not any other events that other parts of the app are listening for. And this is where my understanding falls apart: I thought I could use the event aggregator as a global communication and messaging system across my application structure. Can anyone please shed any insight into what might be going on?
Much thanks,
Sam
* UPDATE/EDIT/SOLUTION *
Hello, well, I have it working now (only 5 minutes after posting the above - doh!). Basically, adding my listeners in the initializer event of the module was too early (as far as I can tell). I moved them further along the chain of functions and now everything is behaving as expected.
The change I had to make to get it working was that I had to remove the vent listener "onSplashContinueClick" within the module further along. Before this change, it was in the initializer function but now it is further along:-
define(["backbone", "marionette", "shell/vent", "shell/shellapp", "shell/splash/splashmodel", "shell/splash/splashview"], function(Backbone, Marionette, vent, shellApp, SplashModel, SplashView){
var SplashModule = shellApp.module("SplashModule");
SplashModule.addInitializer(function(){
trace("SplashModule.addInitializer()");
SplashModule.model = SplashModel;
SplashModule.model.fetch({
success:function(){
//trace("splashModel.fetch success")
SplashModule.onSplashModelLoaded();
},
error:function() {
//trace("splashModel.fetch error")
}
});
});
SplashModule.addFinalizer(function(){
});
SplashModule.initView = function () {
//trace("SplashModule.initView()");
SplashModule.mainView = new SplashView({model: SplashModel});
shellApp.mainRegion.show(SplashModule.mainView);
vent.on("onSplashContinueClick", this.onSplashContinueClick);
};
SplashModule.end = function () {
trace("SplashModule.end()");
shellApp.mainRegion.close();
vent.trigger("onSplashModuleComplete");
};
// events
SplashModule.onSplashModelLoaded = function () {
trace("SplashModule.onSplashModelLoaded");
SplashModule.initView();
};
SplashModule.onSplashContinueClick = function () {
trace("SplashModule.onSplashContinueClick()");
SplashModule.end();
};
return SplashModule;
});
I am guessing the problem has to do with the order of when dependencies are available and/or ready. I believe the vent was not ready for the listener during the initializer method. This may well be tied up to my usage of Marionette modules within require modules.
Using RequireJS also involves some clean modules...
Backbone.Wreqr.EventAggregator is a module that is part of Marionette.js (for the record, Derrick Bailey just put this module that is made by someone else inside his library, same thing for Backbone.BabySitter)
using RequireJS, you are limited to see what is exported by the library, and in this case Marionette
I think the best way is to split marionette into the 3 modules it actually contains backbone.babysitter, backbone.wreqr, and marionette.
Then you have to create a shim for each module
I used this
require.config({
baseUrl: "/Scripts/",
paths: {
"json2": "vendor/JSON2",
"backbone": "vendor/backbone/backbone.1.1.0",
"localStorage": "vendor/backbone/backbone.localStorage.1.1.9",
"marionette": "vendor/backbone/backbone.marionette.1.8.6",
"bootstrap": "vendor/bootstrap/bootstrap.3.1.1",
"jquery": "vendor/jquery/jquery.1.8.3",
"text": "vendor/Require/text.0.27.0",
"underscore": "vendor/underscore/underscore.1.5.2",
"wreqr": "vendor/backbone/backbone.wreqr",
"babysitter": "vendor/backbone/backbone.babysitter",
},
shim: {
"json2": {
exports: "JSON"
},
"jquery": {
exports: "$"
},
"underscore": {
exports: "_"
},
"bootstrap": {
deps: ["jquery"]
},
"backbone": {
deps: ["underscore", "jquery"],
exports: "Backbone"
},
"validation": {
deps: ["backbone"],
exports: "Backbone.Validation"
},
"wreqr": {
deps: ["backbone", "underscore"],
exports: "Backbone.Wreqr"
},
"marionette": {
deps: ["backbone", "babysitter", "wreqr"],
exports: "Backbone.Marionette"
},
"localStorage": {
deps: ["backbone"],
exports: "Backbone.LocalStorage"
}
}
});
once you have this, you will be able to use wreqr
There's another trick in your script, the fact you write
define(['backbone', 'marionette'],function(Backbone, Marionette){
is a bit disturbing, because you will never know if the use of Backbone or Marionette in your implementation is made on purpose or not. I mean, the namespaces related to backbone and marionette are Backbone and Marionette; I suggest you alias Backbone as backbone and Marionette as marionette like this:
define(['backbone', 'marionette'],function(backbone, marionette){. Doing such , you will be able to check if your module has been downloaded on demand by RequireJS or not.
Then once the shim has been created, your first block code should look like this
define(["wreqr"],function(wreqr){
var ea = new wreqr.EventAggregator();
ea.on('all', function (e) { console.log("[EventAggregator] event: "+e);});
return ea;
});
I got little problem with combination busterjs+requirejs+backbone, structure of my project:
js-src
--lib //jquery, require, etc
--views
--models
-app.js //require config and start of app
js (compiled same structure as above)
test
-buster.js
-require-config.js
-test-test.js
require-config.js:
require.config({
baseUrl: 'js-src/',
paths: {
jquery: 'lib/jquery',
jplugins: 'lib/jquery.plugins',
underscore: 'lib/underscore',
backbone: 'lib/backbone'
},
shim: {
'backbone': {
deps: ['underscore', 'jplugins'],
exports: 'Backbone'
},
'jplugins': {
deps: ['jquery']
}
}
});
typical file exept off that in lib:
define(function (require) {
var $ = require('jquery'),
Backbone = require('backbone'),
otherElem = require('views/other'),
View = Backbone.View.extend({
el: '#el',
initialize: function () {
},
showLinks: function (value) {
},
render: function ) {
}
});
return View;
});
buster.js:
var config = module.exports;
config['browser-all'] = {
autoRun: false,
environment: 'browser',
rootPath: '../',
libs: [
'js-src/lib/require.js',
'test/require-config.js'
],
sources: [
'js-src/**/*.js'
],
tests: [
'test/*-test.js'
]
// extensions: [
// require('buster-amd')
// ]
};
test-test.js:
buster.spec.expose();
require(['views/View'], function (module) {
describe("An AMD module", function () {
it("should work", function () {
expect(true).toEqual(true);
});
});
});
When i run it using buster test i get:
Uncaught exception: ./js-src/lib/require.js:192 Error: Script error
http://requirejs.org/docs/errors.html#scripterror
TypeError: uncaughtException listener threw error: Cannot read property 'id' of undefined
at Object.uncaughtException (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/lib/runners/browser/progress-reporter.js:49:50)
at notifyListener (/usr/local/lib/node_modules/buster/node_modules/buster-core/lib/buster-event-emitter.js:37:31)
at Object.emit (/usr/local/lib/node_modules/buster/node_modules/buster-core/lib/buster-event-emitter.js:101:17)
at Object.emitCustom (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/lib/runners/browser/remote-runner.js:283:14)
at /usr/local/lib/node_modules/buster/node_modules/buster-test-cli/lib/runners/browser/remote-runner.js:89:16
at /usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/lib/pubsub-client.js:79:47
at Object.trigger (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/node_modules/faye/node/faye-node.js:383:19)
at Object.distributeMessage (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/node_modules/faye/node/faye-node.js:666:30)
at Object._deliverMessage (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/node_modules/faye/node/faye-node.js:1065:20)
at Object.<anonymous> (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/node_modules/faye/node/faye-node.js:1004:12)
Firefox 16.0, Linux:
How to write proper test with that structure?
It will help if you run the browser tests and check the outputs in the console. The error messages therein are usually much more expressive. You should also remove the autoRun directive from the buster configuration and re-enable the "buster-amd" extension.
I'm new to Buster.js as of yesterday, but I'll add the following 4 part suggestion.
1.) Uncomment "extensions: [require('buster-amd')]" in your 'buster.js'
2.) Remove 'baseUrl' from your 'require.config'
3.) Explicitly set paths to your libs. For example "jplugins: 'lib/jquery.plugins'" would become "jplugins: 'js-src/lib/jquery.plugins'", this would also be needed for models, collections, views, and other files sitting on the 'js-src/' directory.
require.config({
paths: {
jquery: 'js-src/lib/jquery',
views: 'js-src/lib/views',
somerootfile: 'js-src/somerootfile'
4.) Change your test to be like this ...
describe('some test', function(run) {
require(['models/your_model'], function(YourModel) {
run(function() {
it('should load', function() {
var yourModel = new YourModel();
yourModel.set('cat', 'dog');
expect(YourModel.get('cat')).toEqual('dog');
});
});
});
});
The problem seems to be that the 'baseUrl' in 'require.config' confuse Buster.js and tell it to no long respect the 'rootPath' set in 'buster.js'.
I'm in the process of creating a Backbone.js app using Require.js. Each view file corresponds to one resource (e.g. 'News'). Within each view file, I declare a backbone
view for each action ('index', 'new', etc). At the bottom of the view file I receive
the necessary info from the router and then decide which view to instantiate (based on the info passed in from the router).
This all works well, but it requires lots of code and doesn't seem to be the 'backbone.js way'. For one thing, I'm rellying on the url to manage state. For another, I'm not using _.bind which pops up in a lot of backbone.js examples. In other words, I don't think I'm doing it right, and my code base smells... Any thoughts on how to structure my app better?
router.js
define([
'jquery',
'underscore',
'backbone',
'views/news'],
function($, _, Backbone, newsView){
var AppRouter = Backbone.Router.extend({
routes:{
'news':'news',
'news/:action':'news',
'news/:action/:id':'news'
},
news: function(action, id){
newsView(this, action, id).render();
}
});
var intialize = function(){
new AppRouter;
Backbone.history.start()
};
return{
initialize: initialize;
};
}
news.js ('views/news')
define([
'jquery',
'underscore',
'backbone',
'collections/news',
'text!templates/news/index.html',
'text!templates/news/form.html'
], function($, _, Backbone, newsCollection, newsIndexTemplate, newsFormTemplate){
var indexNewsView = Backbone.View.extend({
el: $("#content"),
initialize: function(router){
...
},
render: function(){
...
}
});
var newNewsView = Backbone.View.extend({
el: $("#modal"),
render: function(){
...
}
});
...
/*
* SUB ROUTER ACTIONS
*/
var defaultAction = function(router){
return new newsIndexView(router);
}
var subRouter = {
undefined: function(router){return defaultAction(router);},
'index': function(router){ return defaultAction(router);},
'new': function(){
return new newNewsView()
},
'create': function(router){
unsavedModel = {
title : $(".modal-body form input[name=title]").val(),
body : $(".modal-body form textarea").val()
};
return new createNewsView(router, unsavedModel);
},
'edit': function(router, id){
return new editNewsView(router, id);
},
'update': function(router, id){
unsavedModel = {
title : $(".modal-body form input[name=title]").val(),
body : $(".modal-body form textarea").val()
};
return new updateNewsView(router, id, unsavedModel);
},
}
return function(router, action, id){
var re = /^(index)$|^(edit)$|^(update)$|^(new)$|^(create)$/
if(action != undefined && !re.test(action)){
router.navigate('/news',true);
}
return subRouter[action](router, id);
}
});
While I feel like it's important to emphasize that there isn't really a "Backbone.js way", it does seem like you're replicating work Backbone should be doing for you.
I agree that it makes sense to have a specialized Router for each independent section of your application. But it looks at first glance like what you're doing in your "sub-router" section is just recreating the Backbone.Router functionality. Your AppRouter doesn't need to deal with /news URLs at all; you can just initialize a NewsRouter with news-specific routes, and it will deal with news-related URLs:
var NewsRouter = Backbone.Router.extend({
routes:{
'news': 'index',
'news/create': 'create',
'news/update/:id': 'update',
'news/edit/:id': 'edit'
},
index: function() { ... },
create: function() { ... },
// etc
});
As long as this is initialized before you call Backbone.history.start(), it will capture URL requests for its routes, and you never have to deal with the AppRouter. You also don't need to deal with the ugly bit of code at the bottom of your view - that's basically just doing what the core Backbone.Router does for you.
I'm using require.js and backbone as well I think the main difference that i'd suggest is that each file should return just one view, model, router or collection.
so my main html page requires my main router. That router is a module that requires a few views based on each of it's routes, and a bootstrapped model. Each router method passes the relevant bootstrapped model piece to the relevant view.
From there it stays really clean as long as each file is just 1 backbone thing (model, collection, view, router) and requires just the elements it uses. This makes for a lot of js files (I have about 100 for my current project) but that's where require.js optimization comes into play.
I hope that helps.
Why don't you structure your routes like this:
routes:{
'news':'news',
'news/edit/:id':'editNews',
'news/new':'newNews',
...
}