Browserify+Backbone.js "ApplicationState" shared module - javascript

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

Related

Backbone sub views definition - main view vs router

Here is how my Backbone Router looks like
define([
"jquery",
"underscore",
"backbone"
], function ($, _, Backbone) {
return Backbone.Router.extend({
routes: {
"overview": "overview"
},
overview: function () {
require([
"views/overview",
"models/user-collection",
"grid",
"spreadsheet"
], function (OverviewView, TestCollection, GridView, SpreadSheetView) {
// Data
var collection = new TestCollection();
// Main view
var view = new OverviewView({
el: "#page",
collection: collection
});
// Sub view #1
var gridView = new GridView({
el: "#backgridWrapper"
});
// Sub View #2
var spreadsheetView = new SpreadSheetView({
el: "#handsontableWrapper"
});
// Flow
collection.fetch({
success: function () {
view.render();
gridView.render();
spreadsheetView.render();
}
});
});
}
});
});
As you can see there are several views:
Main view
Sub view #1
Sub view #2
I've did a lot of searching on how to organize the views and sub-views in Backbone, however all of them supposed to create a new sub-view instance directly within a view definition, so that router only knows about Main view...
So the question is - is it a good idea to handle sub-views at a router, instead of directly at view constructor?
The router should be just handling routes and initializing stuff.
Things like fetching data should go in the view that uses it - The view displays the data or error messages (in case of a failure), so I think it's wise to let the view fetch the data rather than some router who's only interested in the routes and have no interest in the data.
and I prefer initializing the sub views, inside their parent view, rather than somewhere else. The parent - child relationship itself justifies that, you better keep the children with their parents than a stranger so they will be under better control and you can easily find them later as well :)
Mostly it's a matter of opinion, but the thing is if you don't, all your code will soon get cluttered in the router rather than being well organized.
Below is how I'll structure the same thing.
Note that I'm initializing child views as part of parent views render method. It could be done when the parent view is initialized, but I see no point in doing so unless the parent view successfully fetches data and is proceeding to render itself.
define([
"jquery",
"underscore",
"backbone"
], function($, _, Backbone) {
return Backbone.Router.extend({
routes: {
"overview": "overview"
},
overview: function() {
require(["views/overview"], function(OverviewView) {
// initialize Main view
var view = new OverviewView({
el: "#page"
});
});
}
});
});
define([
"jquery",
"underscore",
"backbone",
"models/user-collection",
"grid",
"spreadsheet"
], function($, _, Backbone, TestCollection, GridView, SpreadSheetView) {
return Backbone.View.extend({
initialize: function(options) {
this.collection = new TestCollection();
this.fetchData();
},
events: {},
render: function() {
// rendering subviews is part of rendering their parent view.
//I prefer to do that here
// Sub view #1
this.gridView = new GridView({
el: "#backgridWrapper"
});
// Sub View #2
this.spreadsheetView = new SpreadSheetView({
el: "#handsontableWrapper"
});
//Below lines can be handled while initializing the respective view
// (In their initialize() method, or after fetching some data etc
// or can be chained with the above initialization if their render() method returns a reference to itself (`return this`)
this.gridView.render();
this.spreadsheetView.render();
},
fetchData: function() {
var view = this;
this.collection.fetch({
success: function() {
view.render();
}
});
}
});
});
side note : I strongly suggest not to put a collection under models folder.

Organizing Backbone.js one-page app

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?

Backbone.js render called before content is fetched

I found an example of some Backbone.js code that I then adopted to my needs.
The render function of CommentListView is called before any content is fetched. It seems that it not called again when there are content to render.
The backend returns two results, so that is not the problem.
// Models
window.Comment = Backbone.Model.extend();
window.CommentCollection = Backbone.Collection.extend({
model:Comment,
url:"/api/comments/cosmopolitan"
});
// Views
window.CommentListView = Backbone.View.extend({
tagName:'ul',
initialize:function () {
this.model.bind("reset", this.render, this);
},
render:function (eventName) {
console.log(this.model.models);
_.each(this.model.models, function (comment) {
console.log(comment);
$(this.el).append(new CommentListItemView({model:comment}).render().el);
}, this);
return this;
}
});
window.CommentListItemView = Backbone.View.extend({
tagName:"li",
template:_.template($('#tpl-comment-list-item').html()),
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
// Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"list"
},
list:function () {
this.commentList = new CommentCollection();
this.commentListView = new CommentListView({model:this.commentList});
this.commentList.fetch();
$('#sidebar').html(this.commentListView.render().el);
}
});
var app = new AppRouter();
Backbone.history.start();
The behavior of fetch has changed a bit in Backbone 1.0.0. From the ChangeLog:
Renamed Collection's "update" to set, for parallelism with the similar model.set(), and contrast with reset. It's now the default updating mechanism after a fetch. If you'd like to continue using "reset", pass {reset: true}.
And Collection#fetch says:
fetch collection.fetch([options])
Fetch the default set of models for this collection from the server, setting them on the collection when they arrive. [...] When the model data returns from the server, it uses set to (intelligently) merge the fetched models, unless you pass {reset: true},
Your initialize just binds to "reset":
this.model.bind("reset", this.render, this);
You can either bind to the "add", "remove", and "change" events that Collection#set will generate or you can explicitly ask for a "reset" event when you fetch:
this.commentList.fetch({ reset: true });
A couple other things while I'm here:
Since your CommentListView view is using a collection rather than a model, you might want to call it collection:
this.commentListView = new CommentListView({collection: this.commentList});
and then refer to this.collection inside the view. See View#initialize for details on how view constructors handle their arguments.
Collections have various Underscore methods mixed in so you can say this.collection.each(function(model) { ... }) instead of _.each(this.model.models, ...).
Views maintain a cached version of the jQuery-wrapped el in $el so you can say this.$el instead of $(this.el).
Be careful with things like console.log(this.model.models). The console usually grabs a live reference so what shows up in the console will be the state of this.model.models when you look rather than when console.log is called. Using console.log(this.model.toJSON()) is more reliable when faced with timing and AJAX issues.
You might want to switch to listenTo instead of bind (AKA on) as that is less susceptible to memory leaks.
Usually is use to create a listener for the fetch, when fetch is complete and change the model or collection there is a callback. try this:
var AppRouter = Backbone.Router.extend({
routes:{
"":"list"
},
list:function () {
this.commentList = new CommentCollection();
this.commentListView = new CommentListView({model:this.commentList});
this.listenTo(this.commentList,'change', this.makeRender);
this.commentList.fetch();
},
makeRender: function(){
$('#sidebar').html(this.commentListView.render().el);
}
});

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);
},
...

Categories

Resources