In my Backbone app (Rails backend), I am using [Handlebars] Javascript templates (JST's) for rendering my Backbone views. Whenever I do a browser refresh while on one of those URL templates, it jumps back to the root URL. I want it to stay on that current template after page-refresh. I want to do something like this...
getPage: function() {
$(window).on('beforeunload', function(){
var fragment = Backbone.history.fragment;
});
if (fragment === "this_current_template") {
return App.getCurrentTemplatePage(); // render it
} else if (fragment === "") {
return App.getFrontMainPage(); // render it
}
}
getPage is a method in my backbone App object. My fragment variable can't be recognized outside of the $(window) event handler. I can't seem to find a way to persist my previous Backbone.history.fragment in a variable, even if I make fragment global. It's all swept away after refresh. I know about Backbone.history.loadUrl(Backbone.history.fragment);, but I just can't get that to work either. It only seems to work prior to browser refresh.
Is it something I'll have to change in the root route of my backend code? or is there a way to persist the current page using Backbone / JS on page reload? Any help on this would be much appreciated!
First of all you need to create a Router. According to Backbone docs:
var Router = Backbone.Router.extend({
routes: {
"main": "main"
},
main: function() {
// your code here
}
});
After that you only need to init Backbone.history:
Backbone.history.start({});
After that Backbone automatically will try to find appropriate route for your hashbang and fire the appropriate action. In given example if your initial hashbang will be #main then Router.main action will be executed.
Related
I have the View containing some button. When that View becomes activated it should take the some URL parameter (in my case -- site id) and set it to the button attribute "data-site_id". There is a router too in the app. But I don't know how to implement it with the best way. Till now I see 3 supposed solutions:
Extract site id from URL hash. URL is build by such a pattern:
"sites/edit/:id(/:tab)": "editSite",
the question is -- may I use here a router itself (and, if yes then how?) or can not, and should parse it with common JS way? Of course, router & view are two different objects and located in different files/scopes.
Save the site_id in model. But I'm not sure how to store it from router. I think I can create an instance of model, set it as variable under router scope and then treat it as usual, something like this:
(function(app, $, config, _) {
var Model = new app.modelName();
var Router = app.Router = Backbone.Router.extend({
routes: {
"": "start",
//....
"sites/edit/:id(/:tab)": "editSite",
//...
},
//....
editSite: function(id, tab){
Model.set('site_id', id);
}
//....
})(window.Application, jQuery, window.chatConfig, _);
after that I can extract the site id from model in any time I want
Assign a data-site_id attribute to the button just from the router. But this doesn't look as a best practice, right?
So what is your advice?
Your second suggested solution makes the most sense, you could then instantiate your view passing that model to the constructor.
See http://backbonejs.org/#View-constructor
This is an architectural question about Backbone JS:
AppView that contains a DOM element placeholder that is loaded with LeadsView.
LeadsView contains a DOM placeholder to present LeadView
My current route is #app/leads/1 which means that all 3 Views are loaded. AppView->LeadsView-> LeadView of lead #1.
Now suddenly the user hit the refresh button of the browser. The router would try to take it to #app/leads/:1 which routes to "app/leads/:lead_id" : "showLeadView", but AppView and LeadsView has not been rendered this time, hence the rendering of LeadView will fail.
It's looking for the DOM element to render itself into, but cannot find it.
How is that handled with Backbone?
TIA,
Nimrod.
You can programmatically initialize and render the other views when the "app/leads/:lead_id" : "showLeadView" route is hit.
The problem now becomes: How to find if a view was rendered or not ?
I solved this by adding a new property to my Models and Collections that would tell me if they were populated (either if they were fetched or if they have attributes set). An implementation for this can be found in the Thorax library repo.
And it looks like this:
isPopulated: function() {
/*jshint -W089 */
// We are populated if we have attributes set
var attributes = _.clone(this.attributes),
defaults = getValue(this, 'defaults') || {};
for (var default_key in defaults) {
if (attributes[default_key] != defaults[default_key]) {
return true;
}
delete attributes[default_key];
}
var keys = _.keys(attributes);
return keys.length > 1 || (keys.length === 1 && keys[0] !== this.idAttribute);
}
With this method i can verify if the collection needed to render the parent views has been populated. If it was't than call the methods that would be called if you navigated to app and then to leads (you practically access the routes programmatically).
It could look something like this:
if(!appModel.isPopulated())
this.showAppView();
if(!leadsCollection.isPopulated())
this.showLeadsView();
As you can see this means keeping references to appModel and leadsCollection in your router.
Update (clarification)
In the first line of my answer I was mentioning something like this:
AppView has a reference to a sub-view LeadsView that has a reference to a sub-view LeadView.
in your router you have an instance of AppView.
you pass appModel to AppView and call .render() on it when you hit app router.
you pass leadsCollection to AppView.getLeadsView() and call .render() on it when you hit leads route.
you pass the right lead model from leadsCollection to AppView.getLeadsView().getLeadView() and call .render() on it when you hit the route.
Or you can use something like Thorax
It already adds handling for subviews (takes care of event delegation, event destruction, DOM cleaning, etc.) and offers template handlers for it so you can point in your template where you want the sub-view to be rendered.
I hope now, it is clearer what I meant and as you can see it answers your question.
This is a simple question, but I am new to routing and haven't been able to find an answer to this.
I have a Marionette Router (if I intimidate you, its really the same thing as a Backbone Router. Very simple).
sys.routes = {
"app/:id": "onAppRoute",
};
sys.Router = new Marionette.AppRouter({
controller: {
onAppRoute: function(route) {
console.log('Called app route!');
},
},
appRoutes: sys.routes,
});
Backbone.history.start({pushState: true})
This works - if you hit the back button my browser, the url will change within my Single Page Application and will call the onAppRoute function.
However, let's say I open a new browser window and paste in my page url to a certain 'app':
http://localhost/app/myApplication
This doesn't call the onAppRoute function. It doesn't even seem like it should, though, but I really don't know.
I want it to.
I don't know if I am doing it wrong, or if I should just manually fire it by grabbing my page url on page load, parsing it, then 'navigating' to that route. Seems hacky.
Contrary to your intuition, backbone's default behaviour is to trigger matching routes on page load! cf. http://backbonejs.org/#Router - look for the option silent: true. You'd have to specify that for the router to IGNORE your matching routes on page load, i.e. not trigger the corresponding callbacks.
So your problem lies somewhere else: your routes do NOT match the url you have stated as an example. Clearly, you require an :id parameter, trailing http://localhost/app/myApplication. Therefore, http://localhost/app/myApplication/213 would cause your callback to be triggered on page load, given you didn't pass silent: true as an option to backbone.history.start().
If you want to match the 'root' url, i.e. no params, you would define the following route:
routes: {
'/': someFunction
}
The :id part is a parameter, which will be extracted by Backbone.Router and sent as an argument to onAppRoute.
But in your URL you don't have any parameters /localhost/app/myApplication
If I navigate to a view by clicking on a link such as 127.0.0.1/#/project/1, the correct view gets displayed. However, if I call this url directly in the browser (or hit refresh), the view won't be displayed. What could be the reason for this behaviour?
The way I set up the Router is as follows:
var AppRouter = Backbone.Router.extend({
routes: { },
initialize:function () { }
});
var app = new AppRouter();
and then in every module (I'm using require.js), a route and handler will be added
app.route("project/:id", "showProject");
Could it be that the routes aren't registered yet and thus the callbacks won't be called?
Make sure that you are calling Backbone.history.start() after all of your routers are loaded/instantiated and routes defined: http://backbonejs.org/#History-start
Alternatively, you could stop the history with Backbone.history.stop(), and start it again. Then the added route(s) will be picked up.
BTW, you can test if the history is currently started with the boolean Backbone.History.started (note the capital 'H' is necessary).
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)