Backbone JS view theory - backbone.js

I'm new to Backbone JS, and am having some trouble wrapping my head around a concept.
I have an interface with panels, where one panel is displayed at a time on the screen. Each panel is controlled by its own view, with its own model attached. Now, each panel as an "activator" tab that can be clicked to show the next panel.
In my mind, those tabs are actually sub-views of the parent panel view. Without a panel, the tab shouldn't exist. However, all tabs must appear on the screen at once, so that the user can switch between panels (views). So essentially all panels (their templates, anyway) would be loaded, but hidden until triggered by the click of a tab, at which time its content will be populated or else updated.
My problem, architecturally, comes with binding events to the tab views. So, for example:
window.PanelTabView=Backbone.View.extend({
className: 'view panel-tab-view',
el: '#appPanelTabs',
tagName: 'li',
events: {
'click a': 'test'
},
initialize: function() {
},
render: function(panel) {
this.$el.append(this.template(panel.toJSON()));
},
test: function(x) {
console.log(this.cid);
}
});
So when the tab is clicked, every click event is fired for all tab views.
Maybe I should treat all tabs as a single view, then? But I like the idea of each tab having it's own view from the point of modularity in the template.
Or maybe I'm missing something greater about Backbone and its MVC-esque approach.
What would you do in this scenario?

Why not have a parent view that simply manages the tabs and then simply delegates the click to the tab views? It would be a pretty simple approach.
Alternatively, you could be using a router, and in the router you could create your individual tab views based on a particular route taken. This would allow your page to be linkable.
First Approach:
ParentView = Backbone.View.extend({
el : '#your-tabs',
events : {
'click #tab1' : 'tab1',
'click #tab2' : 'tab2'
},
tab1 : function() {
var t1 = new Tab1();
t1.render();
},
tab2 : function() {
var t2 = new Tab2();
t2.render();
}
});
With a Router:
MyRouter = Backbone.Router.extend({
routes : {
"tab/1" : 'tab1',
"tab/2" : 'tab2'
},
tab1 : function() {
var t1 = new Tab1();
t1.render();
},
tab2 : function() {
var t2 = new Tab2();
t2.render();
}
});
Backbone isn't particularly opinionated about how views are constructed, but these seems to fit into their line of thinking.
Treating all tabs as a single view wouldn't make much sense. It would be silly to re-render all tabs just because the data on one tab changed.

I didn't understand much of your problem but at least I got that you in fact have only 1 button. Therefore you shouldn't have several views (it makes no sense anyway).
What you could do however, is use a "selected" attribute in your models which your views would listen to. So basically, when the user clicks on your button, you'll get the next panel thanks to your collection (I guess), unselect the former panel (set the selected attribute to false), therefore its view would disappear, and select the next, and its view would appear. I'll put some code later if you need.

Related

Detect click on itemview container in Backbone/Marionette

I have the following item view:
return Marionette.ItemView.extend({
template:tpl,
tagName: 'div',
className: 'v_itv_record_type_item',
events:{
'click #ui.item':'itemClicked'
},
ui:{
item:'.v_itv_record_type_item'
},
itemClicked:function(e){
console.log('clicked');
}
});
that uses the following handlebars template:
<div class="clicktarget">
Stuff Goes Here
</div>
If you click on one of these item views, it does not register the click event. I understand that Backbone restricts access to just the views slice of the DOM, but apparently this does not extend to the containing div itself, even though that containing div is not part of any template, parent view or otherwise.
If we change the ui hash and point item at .clicktarget the click is registered. But this gives me me a <div><div>stuff goes here</div></div> structure for seemingly no reason. Is this the only way to detect a click on the entirety of an item views DOM element?
You can register a click event on the view element by omitting the selector:
events:{
'click' :'itemClicked'
}
Note that if you have an event handler at view level, all the clicks inside the view will bubble up and trigger it's handler, unless it was stopped (event.stopPropagation()) on the way. This is the expected behavior.

Backbone.js per attribute rendering (multiple small views vs multiple templates per view )

I have a model and a view. The view displays attributes of a model and allows the user to manipulate these attributes. The problem is that when an attribute is modified it re-renders the whole view which causes a lot of problems for me.
Example blur event on a text input saves the new input to an attribute and thus fires render. Which means that if the user clicked from that text input straight to a button on the same view that event will never fire as the first event that fires will be blur causing the whole view to re-render and thus losing the button click event.
I have two ideas:
Have a single view where every attribute is in a separate template. Then I bind to a particular attribute change event and in render I update only the html of the changed attribute. This seems like a hack, as there is a lot of work to force the view to update only the changed attribute. It will add a lot of unnecessary complexity to an already complex view.
Create a master view which consists of views, where each of them represents a model's attribute. This will create a lot of views, with nearly no functionality.
I seem to prefer the 2. option. What do you think? What are the best practices? Is there any better way to handle this?
I think you can do this quite easily.
Take a step back and think about where you are binding your events. It seems that you are binding them directly on top of each individual element instead of using a parent delegate.
Here's an example
Backbone.View.extend({
el: $("div.parent"),
events: function() {
this.$el.on("click", "input[type=button]", function(){});
// jquery cross browser on this
this.$el.on("blur", "input[type=text]", function(){});
},
initialize: function() {
this.model.bind("change", this.render, this);
},
render: function() {
this.$el.html('<input type="text" /><input type="button" />');
}
});
Here's what el and it's structure looks like
<div class="parent">
<input type="text" />
<input type="button" />
</div>
So this.$el points to div.parent. I can constantly rerender the contents of this.$el, and as long as the html structure dosen't change, I don't have to worry about events getting unbound. The other solution is that if I really cannot do delegation, I would just call the events method whenever I render again.
Like you said yourself, both of your options seem very complex. But sometimes additionaly complexity is a necessary evil. However, if the updated fields are something relatively simple (like binding a value to an element or an input field), I would simply update the DOM elements without creating additional View/Template abstractions on top of them.
Say you have a model:
var person = new Person({ firstName: 'John', lastName: 'Lennon', instrument:'Guitar' });
And a view which renders the following template:
<div>First Name: <span class="firstName">{{firstName}}</span></div>
<div>Last Name: <span class="lastName">{{lastName}}</span></div>
<div>Instrument: <input class="instrument" value="{{instrument}}"></input></div>
You could declare in the view which property change should update which element, and bind the model change event to a function which updates them:
var PersonView = Backbone.View.extend({
//convention: propertyName+"Changed"
//specify handler as map of selector->method or a function.
firstNameChanged: { '.firstName': 'text' },
lastNameChanged: { '.lastName': 'text' },
instrumentChanged: { '.instrument': 'val' },
otherFieldChanged: function(val) { //do something else },
initialize: function (opts) {
this.model.on('change', this.update, this);
},
//called when change event is fired
update: function(state) {
_.each(state.changed, function(val, key) {
var handler = this[key + "Changed"];
//handler specified for property?
if(handler) {
//if its a function execute it
if(_.isFunction(handler)) {
handler(val);
//if its an object assume it's a selector->method map
} else if(_.isObject(handler)) {
_.each(handler, function(prop, selector) {
this.$(selector)[prop](val);
}, this);
}
}
}, this);
}
A solution like this doesn't scale to very complex views, because you have to add classed elements to the DOM and maintain them in the View code. But for simpler cases this might work quite well.
In addition it's always good to try to compose views of multiple, smaller views, if they naturally divide into sections. That way you can avoid the need to update single fields separately.

Tabs in Backbone

I'm new with Backbone and I'm making an example app in which I have to include tabs. The thing is that I have a collection of cities and I want to create one tab for each city (the collection fetchs from the server). I made a view called TabsView, which in the render function passes the collection to a template, and this one loops through the collection and renders the tabs.
What I want to do is that the first tab appears as 'active'. What I've done for the moment is that each tab has a href to a route in the router which changes it's class to active using jquery. Don't know if this is the best way to do this but it works. Maybe there's a better way. Also, when the user clicks a tab, I want to be able to render other view.
Hope I made myself clear. Thanks, cheers,
Martin
Ok I solved this problem doing something like the following:
var Tabs = Backbone.View.extend({
template: JST['tabs'],
events: {
'click li' : 'switchTab'
},
tagName: 'ul',
className: 'nav-tabs',
render: function() {
this.renderTabs();
return this;
},
renderTabs: function() {
this.$el.html(this.template({ cities: this.cities }));
this.$('li:first').addClass('active');
},
switchTab: function(event) {
var selectedTab = event.currentTarget;
this.$('li.active').removeClass('active');
this.$(selectedTab).addClass('active');
}
});
It works fine, maybe it can be improved.

binding backbone events in jqMobi Nav, or when the dom elements are duplicated in the dom

I've been porting my app to use jqMobi and jqUI, but I've run into a problem with backbone delegating events.
The way jqUI creates a side nav bar is umm.... interesting to say the least.
Each panel can have a distinct nav bar, but the nav bar is never actually visible to the user, you populate the nav bar, and then jqUI copies the html into the div#menu element.
My view is fairly straightforward
MyApp.Views.UserMenu = Backbone.View.extend({
el: 'nav#user_menu',
initialize: function(){
//empty out and unbind in-case it is already populated
$(this.el).empty().unbind();
this.render();
},
events: {
"click div#add_friend": "new_friend"
},
render: function(){
$(this.el).append(HandlebarsTemplates['friends/new_friend']());
// here I am trying to change the 'el' to point to where the menu is in the DOM
this.el = 'div#menu';
this.delegateEvents();
return this;
},
new_friend: function(){
alert('clicked');
}
});
I've tried changing the el to the div#menu after populating the nav, but that isn't working. I've also tried populating the div#menu directly, but that doesn't seem to work either.
Any suggestions? I'm assuming the issue is that the elements are being moved, but it could be something else, and maybe I'm not sure how to debug the other case.

Backbone: how to reuse a view rendered during page load

I am creating an url manager where user can bookmark his favorite webpage URLs and then manage them in folders.
During app load, I render a view which shows the list of folders user has created. this view is invisible on page load.
then user navigates to his home page to see a list of urls
from there, user can arrange the urls in folders
user clicks the 'Move to folder' icon in URL view
I want to display the folderSelectorView absolute positioned below the 'Move to Folder' icon. How can I do that?
Also if the url has already been moved to a folder, I want a checked sign to appear in the folderSelectorView in the folder where the url has been moved to.
How should I proceed here?
Here is how I load the list of folders.
var folderColl = new FolderColl();
new FolderSelectorView({collection: folderColl});
Here is the code for URL view.
URLView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#URLTempalte').html()),
events: {
'click .FolderChange': 'showFolderSelector'
},
initialize: function() {
_.bindAll(this, 'render');
},
render: function() {
return $(this.el).html(this.template(this.model.toJSON()));
},
showFolderSelector: function() {
// How should I display the view here
// view should display below the "Move to folder" icon
// Doing something like `new FolderSelectorView` is not what I'm after
// since that will just re-render the view for every URL displayed
}
});
You can attach a backbone view to an existing HTML element, easily. all you need to do is pass the el for the view, into the view when you instantiate it. the el can be anything you want - including an existing html element, found through a jQuery selector.
var myEl = $("#someElementOnThePage");
var myView = new MyView({
el: myEl
});
This will attach your view to the #someElementOnThePage element, and you can then use it as if you had rendered it yourself.
For more examples, see my blog post on progressive elaboration: http://lostechies.com/derickbailey/2011/09/26/seo-and-accessibility-with-html5-pushstate-part-2-progressive-enhancement-with-backbone-js/

Resources