Need help understanding the basics of nested views in backbone - backbone.js

I've been doing a bunch of reading about nested views in backbone.js and I understand a good amount of it, but one thing that is still puzzling me is this...
If my application has a shell view that contains sub-views like page navigation, a footer, etc. that don't change in the course of using the application, do I need to render the shell for every route or do I do some kind of checking in the view to see if it already exists?
It would seem so to me if someone didn't hit the "home" route before moving forward in the app.
I haven't found anything helpful about this in my googling, so any advice is appreciated.
Thanks!

Since your "shell" or "layout" view never changes, you should render it upon application startup (before triggering any routes), and render further views into the layout view.
Let's say your layout looked something like this:
<body>
<section id="layout">
<section id="header"></section>
<section id="container"></section>
<section id="footer"></section>
</section>
</body>
Your layout view might look something like this:
var LayoutView = Backbone.View.extend({
el:"#layout",
render: function() {
this.$("#header").html((this.header = new HeaderView()).render().el);
this.$("#footer").html((this.footer = new FooterView()).render().el);
return this;
},
renderChild: function(view) {
if(this.child)
this.child.remove();
this.$("#container").html((this.child = view).render().el);
}
});
You would then setup the layout upon application startup:
var layout = new LayoutView().render();
var router = new AppRouter({layout:layout});
Backbone.history.start();
And in your router code:
var AppRouter = Backbone.Router.extend({
initialize: function(options) {
this.layout = options.layout;
},
home: function() {
this.layout.renderChild(new HomeView());
},
other: function() {
this.layout.renderChild(new OtherView());
}
});
There are a number of ways to skin this particular cat, but this is the way I usually handle it. This gives you a single point of control (renderChild) for rendering your "top-level" views, and ensures the the previous element is removed before new one is rendered. This might also come in handy if you ever need to change the way views are rendered.

Related

Backbonejs view binding conceptual feedback

I ran into this article (http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) and was wondering if the idea of binding and rendering views in the router after instantiating them is best practice. I have been binding my views and rendering them in my view definition.
Currently this is how I've been setting up and calling my views:
EmployeeView:
EmployeeView = Backbone.View.extend({
el: '#content',
template:template,
initialize: function () {
this.collection.fetch({
reset: true
});
this.collection.on('reset',this.render, this);
},
render: function(){
this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('render called');
}
My Router:
employeeList: function () {
var c = new EmployeeCollection
new EmployeeView( {
collection: c
});
}
It works fine. But according to the article a better practice is to do the following:
EmployeeView = Backbone.View.extend({
template:template,
initialize: function () {
this.collection.fetch({
reset: true
});
this.collection.on('reset',this.render, this);
},
render: function(){
this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('render called');
return this;
}
Router
employeeList: function () {
var c = new EmployeeCollection
$('#content').html(new EmployeeView( {collection: c}).render().el);
},
I like the solution in the article because it decouples the views from other DOM events as the article said and allows me to focus all my tweaking and customizing in one place, the router. But because I'm passing in a collection/model and need to fetch the data in my initialize my page renders twice. My questions are:
Is this really best practice?
How do I avoid calling the render twice if I want to use the suggested method?
What if I have cases where I have some front end user interaction and then need to refresh the view collection/model? Would I have to do it in my view or could that happen in the router as well?
The view you have here, and the one in the article are totally different.
In your example, the view is bound to an element in DOM (#content),
which is not a good practice, especially for beginners and causes lots of bugs that we see here every day.
For example if you create 2 instances of your view then event will starts firing multiples times and along with that all hell will break loose.
The view in the article creates a new <div> element in memory per instance, which is a good practice.
Now, to add this in DOM, newbies often do stuff like the following inside the view's render:
$('#content').html(this.$el);
This creates a global selector inside the view and makes it aware of the outer world which is not a good practice.
The article probably (I didn't read it) address this is issue and presents and alternative of adding the view element to DOM from the router, which is a good practice in my opinion.
To avoid rendering twice in the code from article you can just do:
$('#content').html(new EmployeeView( {collection: c}).el);
el being a live reference, it'll be updated when the fetch succeeds. .render().el is another common mis-understanding spread by all the existing blogs and tutorials.
Side note: Since we are discussing best practices, omitting the semicolon and parenthesis as in var c = new EmployeeCollection is not a good practice either. Go with var c = new EmployeeCollection();
You got it almost right. You're just rendering it twice, which I don't think is the right way to go, as there is no point.
EmployeeView = Backbone.View.extend({
template:template,
initialize: function(){
console.log("Will print second");
this.collection.fetch({ reset: true });
this.collection.on('reset', this.appendEmployees, this);
},
render: function(){
//this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('Will print 3rd. render called');
return this;
}
appendEmployees: function(){
console.log("Will print 4th. Appending employees");
$(this.el).html(Mustache.to_html(this.template, {employee_list: this.collection.toJSON() });
}
})
Router
employeeList: function () {
var c = new EmployeeCollection()
var view = new EmployeeView({ collection: c });
console.log("Will print 1st");
$('#content').html(view.render().el);
}
First, when you do view.render().el it will append view's element (which will be empty by that time) to #content
Second, you're executing appendEmployees function when collection resets. By the time this will happen your element will already be placed in the DOM.
In case you need to refresh, it can be done inside the view, by calling the appendEmployees function, or even by resetting your collection. Or if you navigate to the same route via backbone, the whole process will be repeated hence your collection will be called again, and the page will render from beginning. So it comes down to your preferences on when/why you'd choose one over the other. Hope this helps.

Difference between $(this.el) and this.$el in Backbone

I'm developing a single-page web application using Backbone and Laravel. I've set my router to use pushState and configured Laravel to send all other requests to the main view of the backbone application, where backbone takes care of the routing.
My problem/question is as follows:
I have a route called 'dashboard', this route is the main application view and is shown after login. It uses a collection called Clients.
dashboard:function(uri){
dashboardCallback = function(data){
if(data.check){
console.log('generate dashboard');
//get clients collection
clientsCollection = new Dash.Collections.Clients();
clientsCollection.fetch().then(function(clients){
//genenerate dashboard view
new Dash.Views.Dashboard({collection:clientsCollection}).renderDashboard();
});
}
else{
router.navigate('/', {trigger:true, replace:true});
}
}
Dash.Utilities.user.isLoggedIn(dashboardCallback);
},
The Dash.Views.Dashboard view takes care of all the views in the application, when calling the renderDashboard(); method, it starts rendering all client views. This is where it gets interesting.
The code for rendering all the client views is as follows:
renderClients:function(){
console.log('Rendering all clients', this.collection);
clientsView = new Dash.Views.Clients({collection:this.collection}).render();
$(this.el).html(clientsView.el);
}
with the above code, it works in all cases. With that i mean when I log in first and the application routes me to the dashboard view all the clients gets rendered and appended to the DOM, the same thing happens when I access /dashboard immediately (afther the application checks if i'm logged in).
But, when I use the following code it doesn't load the client views when I first log in. It does load the client views when i access /dashboard directly.
renderClients:function(){
console.log('Rendering all clients', this.collection);
clientsView = new Dash.Views.Clients({collection:this.collection}).render();
this.$el.html(clientsView.el);
}
It took me a while to figure out that the fix of the problem was that I had to replace this.$el with $(this.el), but I alway's thought it didn't matter because they are essentially the same, or am I wrong in this assumption?
Can someone explain to me this weird behaviour?
As requested, here is my global Dashboard view
Dash.Views.Dashboard = Backbone.View.extend({
tagName:'div',
id:'main',
className:'dashboard',
initialize: function(){
console.log('Initializing Global Dashboard View');
//make sure the main element is only added once.
if(!$('.dashboard').length){
$('body').append(this.el);
}
else{
this.el = $('.dashboard');
}
},
renderDashboard: function(){
console.log('Render all Dashboard components');
this.renderNavBar();
this.renderClients();
},
renderNavBar: function(){
var navBarView = new Dash.Views.NavBar().render();
$(this.el).before(navBarView.el);
},
renderLogin: function(){
var logInView = new Dash.Views.Login().render();
$(this.el).html(logInView.el);
},
renderWhoops:function(error){
console.log('Render Whoops from Global Dashboard');
var whoopsModel = new Dash.Models.Whoops(error);
$(this.el).html(new Dash.Views.Whoops({model:whoopsModel}).render().el)
},
renderClients:function(){
console.log('Rendering all clients', this.collection);
clientsView = new Dash.Views.Clients({collection:this.collection}).render();
$(this.el).html(clientsView.el);
}
});
I'd guess that your problem is right here:
if(!$('.dashboard').length){
$('body').append(this.el);
}
else{
this.el = $('.dashboard'); // <----- Broken
}
If there is no .dashboard then you directly assign to this.el and that's a mistake as it won't update this.$el. The result is that this.el and this.$el reference different things and nothing works. You should use setElement to change a view's el:
setElement view.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use setElement, which will also create the cached $el reference and move the view's delegated events from the old element to the new one.
So you should be saying this:
if(!$('.dashboard').length){
$('body').append(this.el);
}
else{
this.setElement($('.dashboard')); // <----- Use setElement
}

Understanding Backbone architecture base concepts

I'm trying to working with backbone but I'm missing it's base concepts because this is the first JavaScript MVVM Framework I try.
I've taken a look to some guide but I think I still missing how it should be used.
I'll show my app to get some direction:
// Search.js
var Search = {
Models: {},
Collections: {},
Views: {},
Templates:{}
};
Search.Models.Product = Backbone.Model.extend({
defaults: search.product.defaults || {},
toUrl:function (url) {
// an example method
return url.replace(" ", "-").toLowerCase();
},
initialize:function () {
console.log("initialize Search.Models.Product");
}
});
Search.Views.Product = Backbone.View.extend({
initialize:function () {
console.log("initialize Search.Views.Product");
},
render:function (response) {
console.log("render Search.Views.Product");
console.log(this.model.toJSON());
// do default behavior here
}
});
Search.Models.Manufacturer = Backbone.Model.etc...
Search.Views.Manufacturer = Backbone.View.etc...
then in my web application view:
<head>
<script src="js/jquery.min.js"></script>
<script src="js/underscore.min.js"></script>
<script src="js/backbone/backbone.min.js"></script>
<script src="js/backbone/Search.js"></script>
</head>
<body>
<script>
var search = {};
search.product = {};
search.product.defaults = {
id:0,
container:"#search-results",
type:"product",
text:"<?php echo __('No result');?>",
image:"<?php echo $this->webroot;?>files/product/default.png"
};
$(function(){
var ProductModel = new Search.Models.Product();
var ProductView = new Search.Views.Product({
model:ProductModel,
template:$("#results-product-template"),
render:function (response) {
// do specific view behavior here if needed
console.log('render ProductView override Search.Views.Product');
}
});
function onServerResponse (ajax_data) {
// let's assume there is some callback set for onServerResponse method
ProductView.render(ajax_data);
}
});
</script>
</body>
I think I missing how Backbone new instances are intended to be used for, I thought with Backbone Search.js I should build the base app like Search.Views.Product and extend it in the view due to the situation with ProductView.
So in my example, with render method, use it with a default behavior in the Search.js and with specific behavior in my html view.
After some try, it seems ProductModel and ProductView are just instances and you have to do all the code in the Search.js without creating specific behaviors.
I understand doing it in this way make everything easiest to be kept up to date, but what if I use this app in different views and relative places?
I'm sure I'm missing the way it should be used.
In this guides there is no code used inside the html view, so should I write all the code in the app without insert specific situations?
If not, how I should write the code for specific situations of the html view?
Is it permitted to override methods of my Backbone application?
Basically, you should think of the different parts like this:
templates indicate what should be displayed and where. They are writtent in HTML
views dictate how the display should react to changes in the environment (user clicks, data changing). They are written in javascript
models and collections hold the data and make it easier to work with. For example, if a model is displayed in a view, you can tell the view to refresh when the model's data changes
then, you have javascript code that will create new instances of views with the proper model/collection and display them in the browser
I'm writing a book on Marionette.js, which is a framework to make working with Backbone easier. The first chapters are available in a free sample, and explain the above points in more detail: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf

Backbone routes and view states

Apologies for the possibly poorly formulated title. New to Backbone.
I'm having trouble wrapping my head around how to deal with routes in association with views. Basically I have a view (let's call it ListView) that, depending on its viewMode, renders ItemViews using different templates. It looks something like this:
var ListView = Backbone.View.extend({
// Cache a bunch of templates here
viewMode: 'list', // Default is list
render: function() {
switch(this.viewMode) {
case 'list':
// Render ItemView based on list template
break;
case 'gallery':
// Render ItemView based on gallery template
break;
}
// Render all items in list
this.collection.each(function(model, index) {
new ItemView(); // Maybe pass viewMode as a parameter
});
}
});
My goal is that whenever ListView uses the viewMode "list" or "gallery", this should be reflected in the address bar, and likewise manually entering or clicking a link that leads to e.g. mysite.com/page.html#items/list or #items/gallery should render the same results.
Is there a way of automating this process, or in some other way solve it?
Think your router would be something like:
var yourRouter = Backbone.Router.extend({
routes: {
"items/list": "showList",
"items/gallery": "showGallery"
},
showList: function() {
listView.viewMode = "list"
listView.render();
}
showGallery: function() {
listView.viewMode = "gallery"
listView.render();
}
});
Then in your view events, you can call the navigate method of your router. This will update the address bar.
yourRouter.navigate("items/list")

Backbone js Routes / Views relationship

I am building an app using Meteor and am having trouble understanding the relationship between Routes and Views. I have Routers working properly, but after having done research on calling new Views am baffled.
Do I use App.navigate ? Do I call something like:
var newView = new MyView();
within the proper router function? This is the code I am using (that works) and my app only has two pages - the index page and item view:
var Aphorism = Backbone.Router.extend({
routes: {
"saying/:id": "showSaying"
},
showSaying: function (id) {
alert('Saying id ' + id + '.');
}
});
You define what routes exist in the Router. You usually only need one of those, unless you have a very complex app.
Then you hook up links and buttons in the app to execute app.navigate when clicked. You can do this with a view or do it yourself with something like jQuery, it's up to you.
For instance:
<div id="myButton">Click me!</div>
var myView = Backbone.View.extend({
el: "#myButton",
events: {
"click": "go"
},
go: function() {
myRouter.navigate("/someUrl", {trigger: true});
}
});

Resources