Menu Example
This advanced example demonstrates how you can use Giraffe's features to build a route-powered menu with cached content views that save their scroll position.
var App, MenuView, MenuItemView, ContentView, ContentItemView;
The App
The App
view creates a collection representing the menu's items along with the
menu and content views.
App = Giraffe.App.extend({
initialize: function() {
this.menuItems = new Giraffe.Collection([
{name: 'menu item 1'},
{name: 'menu item 2'},
{name: 'menu item 3'}
]);
},
afterRender: function() {
this.attach(new MenuView({collection: this.menuItems}));
this.attach(new ContentView({collection: this.menuItems}));
}
});
The MenuView
The MenuView
listens for the 'route:menu'
app event and activates the
collection
item whose name
matches the route parameter.
MenuView = Giraffe.View.extend({
appEvents: {
'route:menu': 'onRouteMenu'
},
onRouteMenu: function(menuItemName) {
var activeMenuItem = this.collection.findWhere({active: true});
if (activeMenuItem) activeMenuItem.set('active', false);
this.collection.findWhere({name: menuItemName}).set('active', true);
},
dataEvents: {
'change:active collection': 'onChangeActiveItem'
},
onChangeActiveItem: function(model, active) {
if (active) this.render();
},
afterRender: function() {
var self = this;
this.collection.each(function(model) {
self.attach(new MenuItemView({model: model}));
});
}
});
The MenuItemView
The MenuItemView
takes a model
and displays its name
and active
status.
MenuItemView = Giraffe.View.extend({
template: '#menu-item-template',
serialize: function() {
var name = this.model.get('name');
return {
name: name,
href: this.app.router.getRoute('route:menu', name),
className: this.model.get('active') ? 'active' : ''
};
}
});
<script id="menu-item-template" type="text/template">
<a href="<%= href %>" class="<%= className %>"><%= name %></a>
</script>
The ContentView
The ContentView
listens for changes to the active
property on its
collection
and displays the appropriate ContentItemView
.
ContentView = Giraffe.View.extend({
dataEvents: {
'change:active collection': 'onChangeActive'
},
onChangeActive: function(model, active) {
if (active) this.render();
},
afterRender: function() {
var activeMenuItem = this.collection.findWhere({active: true});
if (activeMenuItem) {
var itemView = this.getItemView(activeMenuItem);
itemView.attachTo(this, {method: 'html'}); // first detaches any view in this.$el
}
},
getItemView: function(menuItem) {
var view = _.find(this.children, function(child) {
return child.model === menuItem;
});
if (!view) {
view = new ContentItemView({
model: menuItem,
disposeOnDetach: false, // keeps the view cached when detached
saveScrollPosition: true
});
}
return view;
}
});
disposeOnDetach: false
is passed to the ContentItemView
because
by default, views are disposed when detached from the DOM, and by using
{method: 'html'}
to attach the view, all views in the target element are first
detached. In this example we want to keep views around so they can save their
scroll position, but there are other reasons to cache views beyond saving view
state, like speed.
The ContentItemView
The ContentItemView
displays the name of the content. Because these are
created with saveScrollPosition
set to true
, they save their scroll
position when detached and apply it when attached.
ContentItemView = Giraffe.View.extend({
className: 'content-item-view',
template: '#content-item-template',
serialize: function() {
var lines = [];
for (var i = 0; i < 50; i++)
lines.push(this.model.get('name'));
return {lines: lines};
}
});
<script id="content-item-template" type="text/template">
<% _.each(lines, function(line) { %>
<p>content for <%= line %></p>
<% }); %>
</script>
Loading the App
We'll now create the app with some routes
, which automatically creates an
instance of Giraffe.Router at app.router
. Next we'll attach the app, start
Backbone.history
, and then route to the first menu item.
var app = new App({
routes: {
'menu/:name': 'route:menu'
}
});
app.attachTo('body');
Backbone.history.start();
app.router.cause('route:menu', 'menu item 1');