Tabs in Backbone - backbone.js

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.

Related

How to render a backbone collection into two views

I am using twitter bootstrap link. When the user clicks the link a bootstrap modal appears.
Now because of some bootstrap technical difficulties in modal rendering i need to seperate the link and put the modal out the navbar div.
So consider i have two separate div
<div id="linkDiv">
</div>
and
<div id="modalDiv">
</div>
Now i have only one View which makes a call to the server to get the collection
app.View.FriendRequestListView = Backbone.View.extend( {
templateModalLink: _.template($('#link').html()),
templateModal: _.template($('#modal').html()),
tagName: 'div',
initialize: function(){
this.friendRequestCollection = new app.Collection.FriendRequestCollection();
this.friendRequestCollection.bind("reset", this.render, this);
this.friendRequestCollection.fetch();
},
render: function() {
$(this.el).html(this.templateModalLink({
friendRequestCollection: this.friendRequestCollection}));
return $(this.el);
},
});
Than i can render only one div like following
var list = new app.View.FriendRequestListView();
$('#linkDiv').html(list.$el);
My question is , Is it possible to render two templates at the same time and add the two templates to different DIV like for example in my case i want to get update
templateModalLink template to linkDiv and templateModal template to modalDiv with the collection I am getting from the server.
You have to instantiate the collection before app.View.FriendRequestListView(s) and pass app.View.FriendRequestListView(s) the collection:
var friendRequests = new app.Collection.FriendRequestCollection();
friendRequests.fetch(
success: function(collection, response, options) {
var list1 = new app.View.FriendRequestListView({collection: collection});
var list2 = new app.View.FriendRequestListView({collection: collection});
$('#linkDiv').html(list1.$el);
$('#modalDiv').html(list2.$el);
}
);

Backbone JS view theory

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.

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.

How can I create a Backbone View with two or more liked nodes?

Take a HTML tabbar as example. Usually you have a ul and a list of div's. All the Backbone examples that I have found, link the View with only one node by the 'el', 'tagName', etc...
HTML TabBar:
<div class=".tabbar">
<ul class=".tabbar-header">
<li>Cars</li>
<li>Houses</li>
</ul>
<div id="tab-cars" class=".tabbar-item">...</div>
<div id="tab-houses" class=".tabbar-item">...</div>
</div>
Backbone Code:
window.TabBarView = Backbone.View.extend({
el: ???,
tabs: [],
render:function (eventName) {
// Render all tabs in this.tabs
_.each(this.tabs, function (item, position) {
// Render each tab with item.render()
}, this);
return this;
}
});
window.TabBarItemView = Backbone.View.extend({
el: ???,
initialize:function () {
this.model.bind("change", this.render, this);
this.model.bind("destroy", this.close, this);
},
render:function (eventName) {
// Render the tab header and tab content
return this;
}
});
I wish to add several TabBarItemView's to the TabBarView and each one creates itself the li node inside the ul.tabbar-header and the div.tabbar-item as content.
I've written an article that addresses this issue: http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/
It will show you how you can either use a single view to do what you want, or a parent/child setup with a collection view and item view like you're showing in your sample code
you can go as far as to make a separate navigation view, and have the navigation add an item through the render method of your tab-item-view.
when you render the tab item view, you do something like navigation.add(new nav item);
and also add a way to remove the navigation item.
or you can keep the navigation in pure html and append a <li> item with jquery / javascript when you are rendering a tab below.
can't give you a fully working example though, if you really need it i can probably make one tonight,.

Attach ExtJS MVC controllers to DOM elements, not components

Is there a way to use the Ext.app.Controller control() method, but pass in a DOM query? I have a page that contains standard links and would like to add a click handler to them even though they were not created as Ext Buttons.
I've tried
Ext.define('app.controller.TabController', {
extend: 'Ext.app.Controller',
init: function() {
console.log("init");
this.control({
'a': {
click: this.changeTab
}
});
},
changeTab: function() {
alert("new tab!");
}
});
But clicking on links does not fire the alert.
Is there a way to specify a CSS selector with this.control? Or does it only work with components?
I asked this question at SenchaCon this year, the Sencha developers stated that their intent is that DOM listeners should be attached within your view, and the view should abstract them into more meaningful component events and refire them.
For example, suppose you're creating a view called UserGallery that shows a grid of people's faces. Within your UserGallery view class, you would listen for the DOM click event on the <img> tag to receive event and target, and then the view might fire a component event called "userselected" and pass the model instance for the clicked user instead of the DOM target.
The end goal is that only your views should be concerned with things like interface events and DOM elements while the application-level controller only deals with meaningful user intents. Your application and controller code shouldn't be coupled to your markup structure or interface implementation at all.
Sample View
Ext.define('MyApp.view.UserGallery', {
extend: 'Ext.Component'
,xtype: 'usergallery'
,tpl: '<tpl for="users"><img src="{avatar_src}" data-ID="{id}"></tpl>'
,initComponent: function() {
this.addEvents('userselected');
this.callParent(arguments);
}
,afterRender: function() {
this.mon(this.el, 'click', this.onUserClick, this, {delegate: 'img'});
this.callParent(arguments);
}
,onUserClick: function(ev, t) {
ev.stopEvent();
var userId = Ext.fly(t).getAttribute('data-ID');
this.fireEvent('userselected', this, userId, ev);
}
});
Notes on views
Extend "Ext.Component" when all you want is a managed <div>, Ext.Panel is a lot heavier to support things like titlebars, toolbars, collapsing, etc.
Use "managed" listeners when attaching listeners to DOM elements from a component (see Component.mon). Listeners managed by a components will be automatically released when that component gets destroyed
When listening for the same event from multiple DOM elements, use the "delegate" event option and attach the listener to their common parent rather than to individual elements. This performs better and lets you create / destroy child elements arbitrarily without worrying about continuously attaching/removing event listeners to each child. Avoid using something like .select('img').on('click', handler)
When firing an event from a view, Sencha's convention is that the first parameter to the event be scope -- a reference to the view that fired the event. This is convenient when the event is being handled from a controller where you'll need the actual scope of the event handler to be the controller.
Sample Controller
Ext.define('app.controller.myController', {
extend: 'Ext.app.Controller'
,init: function() {
this.control({
'usergallery': {
userselected: function(galleryView, userId, ev) {
this.openUserProfile(userID);
}
}
});
}
,openUserProfile: function(userId) {
alert('load another view here');
}
});
I have found a work around for this problem. It isn't as direct as one may hope, but it leaves all of your "action" code in the controller.
requirment: Wrap the html section of your page in an actual Ext.Component. This will likely be the case either way. So for instance, you may have a simple view that contains your HTML as follows:
Ext.define('app.view.myView', {
extend: 'Ext.panel.Panel',
alias: 'widget.myView',
title: 'My Cool Panel',
html: '<div>This link will open a window</div><br /> <label for="myInput">Type here: </label><input name="myInput" type="text" value="" />',
initComponent: function(){
var me = this;
me.callParent(arguments);
}
});
Then in the controller you use the afterrender event to apply listeners to your DOM elements. In the example below I illustrate both links (a element) and input elements:
Ext.define('app.controller.myController', {
extend: 'Ext.app.Controller',
init: function() {
this.control({
'myView': {
afterrender: function(cmp){
var me = this; //the controller
var inputs = cmp.getEl().select('input'); // will grab all DOM inputs
inputs.on('keyup', function(evt, el, o){
me.testFunction(el); //you can call a function here
});
var links = cmp.getEl().select('a'); //will grab all DOM a elements (links)
links.on('click', function(evt, el, o){
//or you can write your code inline here
Ext.Msg.show({
title: 'OMG!',
msg: 'The controller handled the "a" element! OMG!'
});
});
}
}
});
},
testFunction: function(el) {
var str = 'You typed ' + el.value;
Ext.Msg.show({
title: 'WOW!',
msg: str
});
}
});
And there you have it, DOM elements handled within the controller and adhering to the MVC architecture!
No, this seems not to be possible. The Ext.EventBus listens to events fired by ExtJS components. Your standard DOM elements do not fire those events. Additionally the query is checked with the ExtJS componets is( String selector ) method, wich can't be called by DOM elements. Someone might correct me if i'm wrong, but so i'm quite sure it's not possible, unfortunately.
I also have a solution that works around this problem. I use this technique regardless though as it has other benefits: I created an application wide messaging bus. Its an object in my application that extends Observable, and defines a few events. I can then trigger those events from anywere in my app, including html a links. Any component that wants to listen to those events can relay them and they'll fire as if fired from that component.
Ext.define('Lib.MessageBus', {
extend: 'Ext.util.Observable',
constructor: function() {
this.addEvents(
"event1",
"event2"
);
this.callParent(arguments);
}
});
Then, each other compnent can add this after initialisation:
this.relayEvents('Lib.MessageBus', ['event1','event2']);
and then listen to those events.
You can trigger the events from anything by doing:
Lib.MessageBus.fireEvent('event1', 'param a', 'param b')
and you can do that from anything including html links.
Very handy to fire off events from one part of the app to another.
I had the same problem recently (mixing mvc with some non components).
Just thought I'd throw this in as an answer as it seems pretty simple and works for me :)
Ext.define('app.controller.TabController', {
extend: 'Ext.app.Controller',
init: function() {
console.log("init");
this.control({
/* 'a': {
click: this.changeTab
} */
});
var link = Ext.dom.Query.selectNode('a');
Ext.get(link).on('click', this.changeTab);
},
changeTab: function() {
alert("new tab!");
}
});

Resources