Organizing Backbone.js one-page app - javascript

I have to make the one-page application in Backbone.js with a dozen of internal pages.
I decided to do it such way: every page is matched with appropriate function in Router object and consists of several Views, that representing special elements of page.
As for now, I made only two pages, but already have a total mess in a code - this pages and even internal views, that are almost completely different, have many common elements such as events handlers and another internal functions.
So, how it looks like now (briefly):
var AppRouter = Backbone.Router.extend({
routes: {
'': 'main',
'user/:username': 'account'
},
main: function() {
new MainView();
},
account: function(username) {
new AccountTopView();
new AccountMiddleView();
}
});
var LinkEvents = {
'click a': 'navigate'
};
var navigate = function(event) {
event.preventDefault();
var link = event.currentTarget;
var url = link.href;
url = RemoveBaseUrl(url);
app.navigate(url, {trigger: true});
}
var AccountMiddleView = ModalView.extend({
template: _.template($('#tpl-account-middle').html()),
events: _.extend(
{},
LinkEvents
),
render: function() {
this.$el.html(
this.template(
)
);
return this;
},
navigate: navigate
});
var AccountTopView = ModalHatView.extend({
template: _.template($('#tpl-account-top').html()),
events: _.extend(
{},
LinkEvents
),
render: function() {
this.$el.html(
this.template(
)
);
return this;
},
navigate: navigate
});
The real code is much larger, because there are already many events. Are there any ways to optimize such structure? And maybe any other advices?

Related

Browserify+Backbone.js "ApplicationState" shared module

I don't understand how to share some sort of "singleton" application-state holding object model, between different views, using browserify.
Books and tutorials often use a global namespace such as:
var app = app || {};
I have a simple example app which consists of:
app.js
var $ = require('jquery');
var Backbone = require('backbone');
Backbone.$ = $;
var MenuView = require('./views/MenuView');
var ContainerView = require('./views/ContainerView');
new MenuView();
new ContainerView();
MenuView.js
var Backbone = require('backbone');
var ApplicationState = require('../models/ApplicationState');
module.exports = Backbone.View.extend({
el: '#menuView',
events: {
'click .menuLink': 'changePage'
},
changePage: function(event) {
event.preventDefault();
var viewName = $(event.target).attr('data-view');
ApplicationState.set('currentView',viewName);
}
});
ContainerView.js
var Backbone = require('backbone');
var ApplicationState = require('../models/ApplicationState');
module.exports = Backbone.View.extend({
el: '#containerView',
initialize: function() {
this.listenTo( ApplicationState, 'change', this.render );
this.render();
},
render: function() {
this.$el.html( ApplicationState.get('currentView') );
},
close: function() {
this.stopListening();
}
});
This seems working using this approach:
ApplicationState.js
var Backbone = require('backbone');
var ApplicationState = Backbone.Model.extend({
defaults: {
currentView: 'TransactionListView'
}
});
module.exports = new ApplicationState();
Is the ApplicationState module really created only once (caching) ?
Or is there the risk of recreating / resetting the module?
What is the best practice for my use case? Thank you a lot.
Yes, there will only be one ApplicationState in the example you gave. Browserify executes anything following module.exports = as soon as the js file is run and then anything that requires that file is passed a reference to the result.
However it’s generally better practice to avoid sharing state this way between views and instead use a parent view that delegates to subviews. There are a number of ways to set this up. For ideas on best practices for organizing a backbone app, check out this talk: https://www.youtube.com/watch?v=Lm05e5sJaE8
For the example you gave I would highly consider using Backbone's Router.
In your example you have a nav that changes the "main" view. Backbone.Router intercepts navigation and checks it against your specified routes calling your view method. For instance:
router.js
module.exports = Backbone.Router.extend({
initialize: function(options){
this.ContainerView = new ContainerView();
},
routes: {
'transactions': 'showTransactionListView',
'transaction/:id': 'showTransactionDetailView'
},
showTransactionListView: function() {
this.ContainerView.render('TransactionListView');
},
showTransactionDetailView: function(id) {
this.ContainerView.render('TransactionDetailView', id);
}
});
Then any link to #transations (or just transactions if you're using Backbone History) will call your ContainerView.render('TransactionListView'). And, as a bonus, if you reload the page you'll still be looking at TransactionListView.
Other notes:
You'll want to make sure you discard old views when you replace them (by calling .remove() on them) so as to avoid memory leaks. Further Reading
You can add some flexibility to your router and use a controller pattern to render subviews with this nifty plugin

Laravel and backbone web application - var app undefined?

I am working on a web applicaiton that interacts with a RESTful api. The client is built in the laravel and backbone, what I am struggling to do is come up with an intelligent way to load in the correct, models, collections and view based on the current URL.
I have a blade template that gives me universal branding, and then are templates for each of the sections of the site, that load in appropriate underscore templates, scripts, data etc.
For example the users page is accessed at http://domain.local/users and this loads in the following scripts,
User.Collection.js
(function( Users, app_arg ){
'use strict';
Users.Collection = app.UserCollection
}(POPS.module('users'), POPS ));
User.Model.js
(function( Users, app ) {
'use strict';
Users.Model = Backbone.Model.extend({
});
})(POPS.module('users', POPS));
User.Views.Master.js
(function( Users, app_arg ) {
'use strict';
Users.Views.Master = Backbone.View.extend({
el: '#app',
template: _.template( $('#tpl-user').html() ),
events: {
"click .js-add-new-user" : "launchModal",
},
initialize: function() {
this.listenTo( this.collection, 'reset', this.render );
this.listenTo( this.collection, 'add', this.render );
// app.dashProjectCollection = this.collection;
},
render: function() {
this.$el.html( this.template() );
new app.UsersView({ collection: this.collection });
new app.userModal({ model: app.User, collection: this.collection });
//this.filterView = new Dashboard.Views.Filter();
//this.projectView = new Dashboard.Views.Projects({ collection: this.collection });
//this.CollaboratorView = new Dashboard.Views.Collaborators();
return this;
},
launchModal: function(e) {
e.preventDefault();
$("#newUser").modal();
}
});
}( POPS.module('users'), POPS ));
Everything gets fired from app.js
// Main object for the entire app
window.POPS = {
config: {
api: {
base:'http://pops.local/api/v1/',
}
},
// Create this closure to contain the cached modules
module: function() {
// Internal module cache.
var modules = {};
// Create a new module reference scaffold or load an
// existing module.
return function(name) {
// If this module has already been created, return it.
if(modules[name]) {
return modules[name];
}
// Create a module and save it under this name
modules[name] = { Views: {} };
return modules[name];
};
}(),
init: function() {
// :: app start :: //
var app = POPS;
var Module = app.module( $( '#popsapp' ).data('route') );
// Creates a Master object in the global namespace so data can be passed in from the DOM.
// This would be replaced with a master Router if we weren't using actual pages
app.Initialiser = function( initialCollection ) {
this.start = function() {
//don't cache ajax calls
$.ajaxSetup({ cache: false });
if(Module.Collection !== undefined) {
this.collection = new Module.Collection();
this.view = new Module.Views.Master({ collection: this.collection });
} else {
this.view = new Module.Views.Master();
}
if(this.collection !== undefined) {
this.collection.reset( initialCollection );
}
//moved this here so script runs after the DOM has loaded.
//but script.js still needs completely removing.
};
};
}
};
// Entry point into the application
POPS.init();
I cannot fathom why app is undefined!
in your first block you have
(function( Users, app_arg ){
'use strict';
Users.Collection = app.UserCollection
}(POPS.module('users'), POPS ));
you define app_arg in the function arguments but use app.UserCollection within the function. app here will be undefined.
This would also be the same for User.Views.Master.js

backbone.js, handlebars error : this._input.match is not a function

I'm new to backbone.js and handlebars and I'm having a problem getting my template to render out the data.
Here is my collection and model data from tagfeed.js module:
// Create a new module.
var Tagfeed = app.module();
// Default model.
Tagfeed.Model = Backbone.Model.extend({
defaults : {
name : '',
image : ''
}
});
// Default collection.
Tagfeed.Collection = Backbone.Collection.extend({
model : Tagfeed.Model,
url : Api_get('api/call')
});
Tagfeed.TagView = Backbone.LayoutView.extend({
template: "tagfeed/feed",
initialize: function() {
this.model.bind("change", this.render, this);
},
render: function(template, context) {
return Handlebars.compile(template)(context);
}
});
Then in my router I have:
define([
// Application.
"app",
// Attach some modules
"modules/tagfeed"
],
function(app, Tagfeed) {
// Defining the application router, you can attach sub routers here.
var Router = Backbone.Router.extend({
routes: {
"index.html": "index"
},
index: function() {
var collection = new Tagfeed.Collection();
app.useLayout('main', {
views: {
".feed": new Tagfeed.TagView({
collection: collection,
model: Tagfeed.Model,
render: function(template, context) {
return Handlebars.compile(template)(context);
}
})
}
});
}
});
return Router;
});
THis successfully makes a call to the api, makes a call to get my main template, and makes the call to get the feed template HTML. If I don't include that render(template, context) function, then it renders on the page as the straight up HTML that I have in the feed template with the {{ name }} still included. however when its included, I get the error
TypeError: this._input.match is not a function
[Break On This Error]
match = this._input.match(this.rules[rules[i]]);
and if I examine the variables that get passed into the appLayout views render function for feed, I see that the template var is a function, and the context var is undefined, then it throws that error.
Any ideas what I'm doing wrong? I know I have at least one problem here, probably more.
Since you're using requirejs, you can use the text module to externalise your templates or better still pre-compile them and include them in your view. Check out http://berzniz.com/post/24743062344/handling-handlebars-js-like-a-pro
E.g. using pre-compiled templates
// router.js
define(['views/tag_feed', 'templates/feed'], function(TagFeedView) {
var AppRouter = Backbone.Router.extend({
// ...
});
})
// tag_feed.js
define(['collections/tag_feed'], function() {
return Backbone.View.extend({
// ...
render: function() {
this.$el.html(
Handlebars.templates.feed({
name: '...'
})
);
}
});
})
For reference I've created simple boilerplate for a backbone/require/handlebars setup https://github.com/nec286/backbone-requirejs-handlebars

How does one "listen to the router" (respond to Router events in Views/Models) in Backbone.js?

In the Backbone.js documentation, in the entry for the Router.routes method, it is stated
When the visitor presses the back button, or enters a URL, and a particular route is matched,
the name of the action will be fired as an event, so that other objects can listen to the router,
and be notified.
I have attempted to implement this in this relatively simple example:
The relevant JS:
$(document).ready(function(){
// Thing model
window.Thing = Backbone.Model.extend({
defaults: {
text: 'THIS IS A THING'
}
});
// An individual Thing's View
window.ThingView = Backbone.View.extend({
el: '#thing',
initialize: function() {
this.on('route:showThing', this.anything);
},
anything: function() {
console.log("THIS DOESN'T WORK! WHY?");
},
render: function() {
$(this.el).html(_.template($('#thing-template').html(), {
text: this.model.get('text')
}));
return this;
}
});
// The Router for our App
window.ThingRouter = Backbone.Router.extend({
routes: {
"thing": "showThing"
},
showThing: function() {
console.log('THIS WORKS!');
}
});
// Modified from the code here (from Tim Branyen's boilerplate)
// http://stackoverflow.com/questions/9328513/backbone-js-and-pushstate
window.initializeRouter = function (router, root) {
Backbone.history.start({ pushState: true, root: root });
$(document).on('click', 'a:not([data-bypass])', function (evt) {
var href = $(this).attr('href');
var protocol = this.protocol + '//';
if (href.slice(protocol.length) !== protocol) {
evt.preventDefault();
router.navigate(href, true);
}
});
return router;
}
var myThingView = new ThingView({ model: new Thing() });
myThingView.render();
var myRouter = window.initializeRouter(new ThingRouter(), '/my/path/');
});
The relevant HTML:
<div id="thing"></div>
<!-- Thing Template -->
<script type="text/template" id="thing-template">
<a class='task' href="thing"><%= text %></a>
</script>
However, the router event referenced in the View's initialize function does not seem to get picked up (everything else works--I'm successfully calling the "showThing" method defined in the Router).
I believe I must have some misconception about what the documentation intended by this statement. Therefore, what I'm looking for in a response is: I'd love to have someone revise my code so that it works via a Router event getting picked up by the View, or, clearly explain what the Router documentation I listed above intends us to do, ideally with an alternative code sample (or using mine, modified).
Many thanks in advance for any assistance you can provide!
This is beacuse you are binding a listener to the wrong object. Try this in your View :
window.ThingView = Backbone.View.extend({
initialize: function() {
myRouter.on('route:showThing', this.anything);
},
...

How do I bind an event to a model that isn't loaded yet?

So I've got a pretty simple backbone app with a model, a collection, and a couple of views. I'm fetching the actual data from the server by doing a collection.fetch() at page load.
My problem is that one of my views is a "detail" view, and I want to bind it to a particular model - but I don't have the model yet when the page loads. My code looks a lot like this:
window.App = {
Models: {},
Collections: {},
Views: {},
Routers: {}
}
App.Models.Person = Backbone.Model.extend({
urlRoot: '/api/people'
});
App.Collections.People = Backbone.Collection.extend({
model: App.Models.Person,
url: '/api/people'
});
people = new App.Collections.People()
App.Views.List = Backbone.View.extend({
initialize: function() {
this.collection.bind('reset', this.render());
},
render: function() {
$(this.el).html("We've got " + this.collection.length + " models." )
}
});
listView = new App.Views.List({collection: people})
App.Views.Detail = Backbone.View.extend({
initialize: function() {
this.model.bind('change', this.render());
},
render: function() {
$(this.el).html("Model goes here!")
}
});
App.Routers.Main = Backbone.Router.extend({
routes: {
'/people': 'list',
'/people/:id': 'detail'
},
list: function() {
listView.render();
},
detail: function(id) {
detailView = new App.Views.Detail({model: people.get(id)})
detailView.render()
}
})
main = new App.Routers.Main();
Backbone.history.start();
people.fetch();
But if I start with the detail route active, the people collection is empty, so people.get(id) doesn't return anything, so my new view has this.model undefined, and won't let me bind any events relating to it. The error is:
Uncaught TypeError: Cannot call method 'bind' of undefined
If I start with the list route active, then by the time I click on an item to bring up the detail view people is populated, so everything works.
What's the right way to bind model-related events for a "detail" view when you're fetching the data after page load?
You have a part of the answer here: Backbone.js Collections not applying Models (using Code Igniter)
Indeed, you need to wait that people.fetch finishes its ajax request before to call Backbone.history.start(); and trigger the actual route.
Your code should look like:
// [...]
main = new App.Routers.Main();
peoples.fetch({
success: function (collection, response) {
// The collection is filled, trigger the route
Backbone.history.start();
}
});
You can add a loader on the page and hide it when the collection is loaded.

Categories

Resources