I'm creating a really basic website with a list of projects of which you can click and then view more details about that project.
The problem I'm having is that when you scroll down the list of projects, then go to the project detail view, it doesn't reset the scroll position to the top. So you are at the bottom of the new page, which is annoying.
I'm quite new to Backbone.js – has anyone come across this before?
i found the same problem on my projects (with backbone). The solution is used is that :
App.Router.on("route", function(data){
$(document).scrollTop(0);
});
So, everytime routing is called, before render the new view, the document page return automatic on top. I hope can help.
Remember you need "bind" this on 1 time, not each view ;)
EDIT:
if the routing is not "triggered" when you create the new page, apply this line
$(document).scrollTop(0);
on the correct event :)
EDIT2 :
Watching your code (final part)
var projects = new App.Views.Projects();
var project = new App.Views.Project();
var theRouter = Backbone.Router.extend({
routes: {
'': 'home',
':id': 'project'
},
home: function() {
projects.render();
},
project: function(id) {
project.render({id: id});
}
});
var router = new theRouter();
router.on('route', function() {
// MAKE SURE ROUTING BIND IS CALLED
console.log("Hey, im routing !!!");
$(document).scrollTop(0);
});
Backbone.history.start();
Try this
Related
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 have a Rails 4 App that uses Turbolinks and Backbone.js.
Basically, my router looks like:
var BookRouter = Backbone.Router.extend({
routes: {
'': 'index'
},
index: function(){
var bookView = new BookView({
el: ".wrapper"
});
}
});
$(function() {
if (Backbone.History.started == false) {
var bookRouter = new BookRouter();
Backbone.history.start({ pushState: true, hashChange: false });
}
});
$(document).on('page:load', function (){
Backbone.history.stop();
Backbone.history.start({ pushState: true, hashChange: false });
var bookRouter = new BookRouter();
});
Here's the scenario:
A user lands on http://website.com/, which takes the user to the index route, which loads a template into the .wrapper, which is shown in my routing code above. This works fine.
A user clicks on one of my Rails generated pages (for example I have a http://website.com/books route which shows all the books that users have created.) This also works fine.
The user is still on the http://website/books route. If you click on my logo, it takes you to the index route (the one generated by the backbone code above). This works fine.
NOW THE PROBLEM: If you go back to Step 3, and instead of clicking the logo, decide to hit the back button, and go back from the books route to the index route, the contents that get put inside the $el .wrapper are repeated TWICE. So for example, in my template, I have the words "Please choose a book to continue". If you went to Step 1, you'd just see the words once. But now, you see n set of words each time you go to /books and then hit the back button n times.
The problem here is the hack used to make Backbone.js play together with Turbolinks. I guess the source of this hack is this blog post which recommends restarting the history whenever a link is handled by Turbolinks. However, stopping the history doesn't clear existing routes. So what you get after handling the page:load event is two router objects handling the same routes. And Backbone.History will trigger every callback for every matching route when you go back, even when they are registered for the same route - meaning that the index callback will run twice, hence the content duplication.
One way to deal with it would be simply removing the duplicate router:
$(document).on('page:load', function (){
Backbone.history.stop();
Backbone.history.start({ pushState: true, hashChange: false });
});
This is sufficient to trigger the router whenever Turbolinks is invoked. However, there seems to be a cleaner way to make Backbone.History trigger the router. From what I can tell from the source code, this should work correctly as well:
$(document).on('page:load', function (){
Backbone.history.checkUrl();
});
So, I want to build an app that would have the same effect as a desktop, so I have a load of routes and I want them to open in windows "(modals)", I'm not even sure if this kind of design is possible with EmberJS, but the only way I can think of is to have the URL ending to look like
app.com/#/skype/files/chrome
This ending up having the 3 windows open (skype, files & chrome)
I would love some suggestions on this kind of design.
Thanks
Honestly there is a large problem here, and that's the router is more like a stack than a list. You can't insert/remove arbitrary routes from the current path. You always push/pop from the end.
IE If you were to switch from app.com/#/skype/files/chrome to app.com/#/files/chrome you'd really be popping the three routes then pushing the two new ones which would destroy all of your state, then create it new.
This is certainly feasible with the routing scheme you are suggesting. With this design, I would suggest the following routing scheme:
App.Router.map(function() {
this.resource('firstModal', { path: '/:modal' }, function() {
this.resource('secondModal', { path: '/:modal' }, function() {
this.resource('thirdModal', { path: '/:modal' }, function() {
})
})
})
});
With the scheme, navigating to modal1.index would show only one modal, navigating to modal1.modal2.index would show two modals, and so on.
You can then define the templates for each modal content and based on this Ember.Component, display the corresponding modal. For example:
App.FirstModalRoute = Ember.Route.extend({
model: function(params) {
return params.modal;
},
renderTemplate: function() {
var modalName = this.modelFor('firstModal');
this.send('openModal', modalName);
}
deactivate: function() {
// Remove the modal on exit route
this.send('closeModal');
}
});
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)