I've hit a head-scratcher with a Backbone.js. The example is on jsfiddle here. I believe the issue is here:
App.Layout = new Backbone.Layout({
// Attach the Layout to the main container.
collection: App.chapters,
el: "body",
initialize: function () {},
beforeRender: function () {
// Add a sub-view for each Chapter
this.collection.each(function (model) {
this.insertView(model.get('id'), new App.ChapterView({
"id": model.get('id')
}));
}, this);
},
views: {
// But if I set the sub-view specifically if works
// "one": new App.ChapterView({id: 'one' })
}
});
In summary, the router should simply activate or deactivate backbone.layoutmanager sub-views based on the path, e.g., /#chapter/one, /#chapter/two, etc.
If I explicitly set the sub-views in App.Layout (see line 49 in the fiddle), the routing works as expected.
However, if I try to add the views by iterating a collection of models in the beforeRender function (line 40; beforeRender is coming from backbone.layoutmanager), they don't appear to be available when the router tries to find the matching view by ID.
Once the page has render, however, the view can be activated with:
App.router.navigate('/chapter/two',{"trigger": true});
Which seems to indicate that the views are properly being added and should be findable by the router with:
App.Layout.getView(name);
No doubt I'm simply overlooking something, or am about to expose my ignorance of the Backbone library. :)
The issue is that you're navigating and rendering out-of-sync. I've updated your code here: http://jsfiddle.net/6h268r7j/55/
It works when you use the declarative approach because those are outside of the render flow, essentially statically added. As soon as you use beforeRender/render you are now in an asynchronous render flow and they won't be available in your router callbacks.
The fix was to simply render the application layout first and then trigger the routing:
App.Layout.render().then(function() {
Backbone.history.start();
});
Related
I am trying to load one html file using backbone js and require js file .I am able to call initialise function of view but not able to load that html file here is my code
define([
'jquery',
'underscore',
'backbone',
'text!templates/stats.html'
], function ($, _, Backbone, statsTemplate) {
'use strict';
var AppView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: '#todoapp',
// Compile our stats template
template: _.template(statsTemplate),
// Delegated events for creating new items, and clearing completed ones.
events: {
},
// At initialization we bind to the relevant events on the `Todos`
// collection, when items are added or changed. Kick things off by
// loading any preexisting todos that might be saved in *localStorage*.
initialize: function () {
alert('-in--')
},
// Re-rendering the App just means refreshing the statistics -- the rest
// of the app doesn't change.
render: function () {
}
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
});
return AppView;
})
I am getting the alert in initialize function not able to load the html file
here is my code
http://plnkr.co/edit/rhoE1H9A8nfu6II64aEo?p=preview
Thanks for taking the time to set up a working example.
Unfortunately Backbone doesn't give you much for free, so there are a number of manual steps to get this working:
Add a <div id="todoapp"></div> to index.html as you are targeting it with el: '#todoapp' but it doesn't exist.
Doing template: _.template(statsTemplate) will return a compiled version of the template (as a function). You then need to call it like a function, optionally passing in a context so that the template can render dynamic data. e.g. this.template().
The render method won't get called automatically, so when you are ready to render the view (usually instantly but it could be after an AJAX response) you need to call this.render(). In your case straight away in initialize.
Finally in render you can attach the rendered template to the view's element: this.$el.html(this.template());
Updated example: http://plnkr.co/edit/6HyOhuQ7LGL91rS8slJX?p=preview
I recommend you create a BaseView with this common render flow so you don't have to repeat it every time. Also it's a good idea in that BaseView to set up a concept of sub views which clean up properly when the parent's remove is called, and re-render when a parent's render is called.
Here is an example of using a BaseView: http://jsfiddle.net/ferahl/xqxcozff/1/
tl;dr: When moving from page to page, change/destroy only these blocks that need it (not re-rendering whole page). And keep application router as simple as possible.
I'm new to backbone and all of the examples to get started with it that I've seen were about small apps with a single page that changes a little sometimes (adding/removing some elements, but never completely re-rendering). But when I started doing my app which is a little bit more complex, I faced the problem of subviews organization...
Problem: When every page of application consists of subviews (each subview can have another subviews, ie nested subviews), which are responsible for displaying their blocks in the page, a reasonable desire would be not to re-render blocks that not changing when you're navigating through pages. Sometimes you need to re-render whole page, when it's completely different from previous, sometimes just some blocks on the page. But in this case application router becomes monster-object that contains too much logic, so that it's hard to maintain.
What I want: I want my router to be like
define(['jquery', 'backbone'], function ($, Backbone) {
return Backbone.Router.extend({
routes: {
"": "home",
..........
},
home: function () {
require(['views/home'], function (View) {
var view = new View({ el: $("body") });
view.render();
});
},
..........
});
});
So, my goal is to move logic for rendering subviews to views. So that view will decide what to do: render itself including subviews or just ask subviews to decide same question for themselves.
Possible solution: Thinking about it I come with idea of global object that contains tree of views as application state (e.g. first_design(home(about, login)) ). And when we're moving to next page, that has the same main view (ie first_design) we don't render first_design view, but just it's subviews (in this case only home). And for every page in router we now need to manually define this tree of views. For example like this:
define(['jquery', 'backbone'], function ($, Backbone) {
return Backbone.Router.extend({
routes: {
"": "home",
"contacts": "contacts",
..........
},
home: function () {
require(['views/firstDesign', 'views/home', 'views/about', 'views/login'], function (FirstDesign, Home, About, Login) {
new FirstDesign({ el: $("body"), subviews: { new Home({ subviews: { new About(), new Login() } }) } });
});
},
contacts: function () {
require(['views/firstDesign', 'views/contacts'], function (FirstDesign, Contacts) {
new FirstDesign({ el: $("body"), subviews: { new Contacts() } });
});
},
..........
});
});
So, the question is...: I believe i'm reinventing the wheel right now and obviously my wheel is not good enough (possibly even not round :D). So are there any implementations of what I need? Or if not, then how I should do it properly? Thanks!
P.S: I'm using backbone.js with require.js. And sorry for my English, it's not my native language...
There is a Backbone plugin called LayoutManager. From their wiki page:
LayoutManager provides a logical foundation for assembling layouts and
views within Backbone.
I use it and it serves me well.
https://github.com/tbranyen/backbone.layoutmanager/wiki
I realized that my solution is not so flexible. And after all I decided to use Marionette. What really helped me to solve my problem is this question. p.s: Marionette has now a hasView method in Region class.
I've read Derick Bailey's article on Zombies but can't seem to figure it out. I have a backbone application that uses require.js and need to be able to close/destroy a view when I navigate away from it.
There's a lot of ways to initiate a backbone app, but what is the right way when using require to allow for clean up?
And how can I call a close() function on views just before navigating away?
Main.js
require([ "app", "backbone", "router", "facebook"], function(App, Backbone, Router, FB) {
//theres a lot of facebook integration
FB.init({
appId : '********',
version : 'v2.0'
});
//force the page to go to index when first arriving
window.location.hash = "";
new App;
Backbone.history.start();
});
App.js
define([ "backbone", "router" ], function(Backbone, Router){
var App = function () {
Router;
}
return App;
})
Router.js
define([ "backbone", "models/user" ], function(Backbone, User){
var AppRouter = Backbone.Router.extend({
routes: {
//All my routes
},
index: function () {
require([ "views/index", "models/user" ], function (IndexView, UserModel) {
var indexView = new IndexView({ model: UserModel });
})
},
// Remaining route functions
return new AppRouter;
})
That post solves a problem that your code here doesn't have.
The problem (zombie views) only occurs when your views have attached event handlers to a model instance.
var View = Backbone.View.extend({
setup: {
// model instance will now be storing a callback which is bound
// to *this* instance of a view
this.model.on('change', this.render, this);
},
render: function () {
// whatever code that uses the context, `this`
this.$el.innerHTML(this.model.get('title'));
}
});
Then in your app lifetime, the above view got rendered and then the page changed, or whatever happened, and the view is not needed anymore. But there might anything else that is using the model that this view has used – and that model might keep changing and then it will fire a callback for change event, the render method which will point at seemingly non-existent view.
But since that view might not have its element in the DOM anymore, you'll get DOM errors (if for example your render method referenced this.$el.parent()) and the views will remain in memory without you knowing it, eventually causing your page to get slow or even unresponsive.
Since that post was written there's now a new way of attaching event handlers, called listenTo, which makes it easier to stopListening.
There's also now View.prototype.remove method which will remove the view's element from the DOM and also call stopListening which will help in case you used listenTo to attach event handlers for the models.
I am using the Backbone Boilerplate https://github.com/tbranyen/backbone-boilerplate and don't know what's the best way to handle more than one page. I cannot find answer that helps me understand easily. Basically, I am thinking of those options:
Should each page has a different config.js? Like config-userpage.js, config-homepage.js...?
Should I have different router.js for different page instead? Like router-userpage.js or router-homepage.js,...?
Should I just try a different boilerplate like https://github.com/hbarroso/backbone-boilerplate?
You can definitely try a different boilerplate, but I'm not sure that will
help. Multiple pages can be achieved in many different ways.
A good reference example for the Backbone Boilerplate is:
http://githubviewer.org/. I have released the entire thing as open source and
you can View how basic pages are added there.
You may want to get creative and make a Page model that handles what page
you're on and inside of each route set the new page title and which layouts to
use.
A very basic, proof-of-concept, implementation inside of app/router.js might
look something like this:
define([
// Application.
"app",
// Create modules to break out Views used in your pages. An example here
// might be auth.
"modules/auth"
],
function(app, Auth) {
// Make something more applicable to your needs.
var DefaultPageView = Backbone.View.extend({
template: _.template("No page content")
});
// Create a Model to represent and facilitate Page transitions.
var Page = Backbone.Model.extend({
defaults: function() {
return {
// Default title to use.
title: "Unset Page",
// The default View could be a no content found page or something?
view: new DefaultPageView();
};
},
setTitle: function() {
document.title = this.escape("title");
},
setView: function() {
this.layout.setView(".content", this.get("view")).render();
},
initialize: function() {
// Create a layout. For this example there is an element with a
// `content` class that all page Views are inserted into.
this.layout = app.useLayout("my-layout").render();
// Wait for title and view changes and update automatically.
this.on({
"change:title": this.setTitle,
"change:view": this.setView
}, this);
// Set the initial title.
this.setTitle();
// Set the initial default View.
this.setView();
}
});
// Defining the application router, you can attach sub routers here.
var Router = Backbone.Router.extend({
routes: {
"": "index"
},
index: function() {
// Set the login page as the default for example...
this.page.set({
title: "My Login Screen!",
// Put the login page into the layout.
view: new Auth.Views.Login()
});
},
initialize: function() {
// Create a blank new Page.
this.page = new Page();
}
});
return Router;
});
As you can see, this is an opinionated way of creating "pages" and I'm sure
other's have better implementations. At Matchbox, I have a very robust Page
model that does breadcrumbs and figures out which navigation buttons to
highlight based on the state. You can also create Routers inside your modules
to encapsulate functionality and expose the Page model on the app object so
that it's available throughout your application.
Hope this helps!
I am cleaning up a multi-page app of 65+ html pages and a central javascript library. My html pages have a ton of redundancies and the central js library has become spaghetti. I face limitations on consolidating pages because I am working within a larger framework that enforces a certain structure. I want to reduce the redundancies and clean up the code.
I discovered backbone, MVC patterns, microtemplating and requirejs, but they seem best for single page applications. Somehow I need to let the main module know what page is being loaded so it will put the right elements on the page. I am thinking of passing in the title of the html which will turn grab the correct collection of page elements and pass them into App.initialize as an object.
1) Can anyone validate this approach? If not are there alternate approaches recommended? How about extensions to backbone like marionette?
2) Can anyone recommend a means to get page specifics into the backbone framework?
Following backbone tutorials I built a successful test page with a main.js that calls an App.initialize method that calls a view.render method. My first thought is to read the html page title and use it to select a model for the specific page being loaded. I'd have to pass in an object with the specifics for each pages layout. Here's the view's render method so you can see what I am trying to do:
render: function () { // pass parameter to render function here?
var data = new InputModel,
pageTitle = data.pageTitle || data.defaults.pageTitle,
compiled,
template;
var pageElements = [
{ container: '#page_title_container', template: '#input_title_template' },
{ container: '#behavior_controls_container', template: '#behavior_controls_template' },
{ container: '#occurred_date_time_container', template: '#date_time_template' }]
for (var i = 0; i < pageElements.length; i++) {
this.el = pageElements[i].container;
compiled = _.template($(InputPageTemplates).filter(pageElements[i].template).html());
template = compiled({ pageTitle: pageTitle });
//pass in object with values for the template and plug in here?
$(this.el).html(template);
}
}
Your help will be greatly appreciated. I am having a lot of fun updating my circa 1999 JavaScript skills. There's a ton of cool things happening with the language.
Using the document title to choose the loaded scripts sounds a tad kludge-y. If it works, though, go for it.
Another idea worth exploring might be to utilize Backbone.Router with pushState:true to setup the correct page. When you call Backbone.history.start() on startup, the router hits the route that matches your current url, i.e. the page you are on.
In the route callback you could do all the page-specific initialization.
You could move the template and container selection out of the view into the router, and set up view in the initialize() function (the view's constructor). Say, something like:
//view
var PageView = Backbone.View.extend({
initialize: function(options) {
this.model = options.model;
this.el = options.el;
this.title = options.title;
this.template = _.template($(options.containerSelector));
},
render: function() {
window.document.title = title;
var html = this.template(this.model.toJSON());
this.$el.html(html);
}
});
Handle the view selection at the router level:
//router
var PageRouter = Backbone.Router.extend({
routes: {
"some/url/:id": "somePage",
"other/url": "otherPage"
},
_createView: function(model, title, container, template) {
var view = new PageView({
model:model,
title:title
el:container,
templateSelector:template,
});
view.render();
},
somePage: function(id) {
var model = new SomeModel({id:id});
this._createView(model, "Some page", "#somecontainer", "#sometemplate");
},
otherPage: function() {
var model = new OtherModel();
this._createView(model, "Other page", "#othercontainer", "#othertemplate");
}
});
And kick off the application using Backbone.history.start()
//start app
$(function() {
var router = new PageRouter();
Backbone.history.start({pushState:true});
}
In this type of solution the view code doesn't need to know about other views' specific code, and if you need to create more specialized view classes for some pages, you don't need to modify original code.
At a glance this seems like a clean solution. There might of course be some issues when the router wants to start catching routes, and you want the browser to navigate off the page normally. If this causes serious issues, or leads to even bigger kludge than the title-based solution, the original solution might still be preferrable.
(Code examples untested)