View Flexibility
This advanced example demonstrates the flexibility of Giraffe.View. The design goal is to create a single view class that nests, manages memory, and moves around the DOM with ease.
The Final Result
There is a lot of information in this example, so let's begin by playing around with the result. If something doesn't make sense to you, read on!
var ParentApp = Giraffe.App.extend({
template: '#parent-app-template',
afterRender: function() {
this.attach(new ChildView());
}
});
var ChildView = Giraffe.View.extend({
className: 'child-view',
colors: ['#ebb', '#eeb', '#beb', '#bee', '#bbe', '#ebe'],
colorIndex: -1,
initialize: function() {
var proto = ChildView.prototype;
proto.colorIndex += 1;
if (proto.colorIndex >= proto.colors.length)
proto.colorIndex = 0;
var color = proto.colors[proto.colorIndex];
this.$el.css('background-color', color);
},
onAddChild: function() {
this.attach(new ChildView(), {
el: this.$('.child-views:first')
});
},
onToggleCache: function(e) {
this.disposeOnDetach = !$(e.target).is(':checked');
},
afterRender: function() {
for (var i = 0; i < this.children.length; i++) {
this.attach(this.children[i], {
el: '.child-views:first'
});
}
},
beforeRender: function() {
this.renderCount = this.renderCount || 0;
this.renderCount += 1;
},
onMoveUp: function() {
var previousView = this.getPreviousView();
this.attachTo(previousView, {
method: 'before',
forceRender: true,
preserve: true
});
previousView.render({
preserve: true
});
},
getPreviousView: function() {
var $parentChildren = this.$el.parent().find('> .child-view');
var index = $parentChildren.index(this.$el);
if (index > 0)
return Giraffe.View.getClosestView($parentChildren[index - 1]);
else
return this.parent;
},
onMoveDown: function() {
var nextView = this.getNextView();
this.attachTo(nextView, {
method: 'after',
forceRender: true,
preserve: true
});
nextView.render({
preserve: true
});
},
getNextView: function() {
var $parentChildren = this.$el.parent().find('> .child-view');
var index = $parentChildren.index(this.$el);
if (index < $parentChildren.length - 1)
return Giraffe.View.getClosestView($parentChildren[index + 1]);
else
return this.parent;
},
onAttachUsingHTML: function() {
this.attachTo(this.$el.parent(), {
method: 'html'
});
},
dispose: function() {
Giraffe.dispose.call(this);
console.log('Disposing of ' + this.cid);
},
template: '#child-template',
serialize: function() {
var $parentChildren = this.$el.parent().find('> .child-view');
var index = $parentChildren.index(this.$el);
var parentIsChildView = this.parent instanceof ChildView;
return {
parentIsChildView: parentIsChildView,
showMoveUpButton: parentIsChildView || index !== 0,
showMoveDownButton: parentIsChildView || index !== $parentChildren.length - 1,
checkedAttr: this.disposeOnDetach ? '' : "checked='checked'",
renderCount: this.renderCount,
cid: this.cid
};
}
});
var app1 = new ParentApp({
name: 'app1'
});
app1.attachTo('body');
var app2 = new ParentApp({
name: 'app2'
});
app2.attachTo('body');
<!DOCTYPE html>
<html>
<head>
<link rel='stylesheet' type='text/css' href='../css/reset.css' />
<link rel='stylesheet' type='text/css' href='viewflexibility0-style.css' />
</head>
<body>
<script id="parent-app-template" type="text/template">
<h2><%= name %></h2>
<h3><%= cid %></h3>
<button data-gf-click="render">Reset <%= name %></button>
</script>
<script id="child-template" type="text/template">
<h3><%= cid %></h3>
<% if (showMoveUpButton) { %>
<button data-gf-click="onMoveUp">▲</button>
<% } %>
<% if (showMoveDownButton) { %>
<button data-gf-click="onMoveDown">▼</button>
<% } %>
<button data-gf-click="onAddChild">Add a child</button>
<button data-gf-click="render">Render count: <%= renderCount%></button>
<button data-gf-click="dispose">Dispose</button>
<% if (parentIsChildView) { %>
<label>
<input type="checkbox" data-gf-change="onToggleCache" <%= checkedAttr %>>
Cache this view
</label>
<button data-gf-click="onAttachUsingHTML">
Reattach to parent using jQuery method 'html'
</button>
<% } %>
<div class="child-views"></div>
</script>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min.js"></script>
<script src="../backbone.giraffe.js" type="text/javascript"></script>
<script type='text/javascript' src='viewflexibility0-script.js'></script>
</body>
</html>
body {
padding: 20px;
}
h2 {
font-size: 24px;
margin-bottom: 20px;
display: inline;
margin-right: 10px;
}
h3 {
font-size: 18px;
display: inline;
margin-right: 10px;
}
.child-view {
position: relative;
padding: 20px;
margin: 20px;
border: 1px dashed #999;
}
[data-gf-click="onMoveUp"] {
position: absolute;
left: -17px;
top: 0;
}
[data-gf-click="onMoveDown"] {
position: absolute;
left: -17px;
bottom: 0;
}
The Parent App
Giraffe.App is a Giraffe.View that encapsulates an app. This example has
an app with a ChildView that has buttons to move around the DOM and create
more child views.
var ParentApp = Giraffe.App.extend({
template: '#parent-app-template',
afterRender: function() {
this.attach(new ChildView());
}
});
Here's the app's template:
<script id="parent-app-template" type="text/template">
<h2><%= name %></h2>
<h3><%= cid %></h3>
<button data-gf-click="render">Reset <%= name %></button>
</script>
data-gf-click is an convenient way to assign a view method as
the click event handler for the DOM element. We recommend prefixing the name of
the handler with on to make it clear an event triggers the method. See the
Document Events example for more.
The Child View
var ChildView = Giraffe.View.extend({
className: 'child-view',
In this example, each ChildView has a button that adds another ChildView to
its children via the onAddChild method.
onAddChild: function() {
this.attach(new ChildView(), {el: this.$('.child-views:first')});
},
By default, Giraffe recreates child views every render, but this is often
not desired. disposeOnDetach tells Giraffe whether or not to cache
a view. By default, disposeOnDetach is true, and child views are disposed when
their parent detaches them before a render. If you set a view's
disposeOnDetach option to false, it is preserved when its parent renders.
In this example, the ChildView has a checkbox to toggle this caching behavior.
onToggleCache: function(e) {
this.disposeOnDetach = !$(e.target).is(':checked');
},
Cached child views will be in children after rendering the parent. Uncached
child views have already been disposed of by this point which removes them from
children. Giraffe does not automatically reattach child views, so you
retain full control over what happens each render.
afterRender: function() {
for (var i = 0; i < this.children.length; i++) {
this.attach(this.children[i], {el: '.child-views:first'});
}
},
Let's track and display the number of renders so we can see what's happening.
beforeRender: function() {
this.renderCount = this.renderCount || 0;
this.renderCount += 1;
},
Giraffe views can move freely around the DOM using the function attachTo,
which automatically sets up parent-child relationships between views. attachTo
takes an optional method option, which is a jQuery insertion method
defaulting to 'append'. The methods are 'append', 'prepend', 'before',
'after', and 'html'. The function attachTo is an inverted way to call
attach, the difference being attachTo doesn't require a parent view - any
DOM element, selector, or view will do.
In this example, we have buttons to move the views around, but we don't want to
display an up or down button when that's an invalid move. To display the correct
buttons, we need to render a view when it moves, so we forceRender on
attachTo and we use preserve to prevent render from disposing of uncached
child views. When a view is attached, Giraffe automatically calls render
on the view if it hasn't yet been rendered, but passing the option forceRender
will cause attachTo to always render the view. The option preserve
prevents child view disposal, even if disposeOnDetach is true, and is used
because we don't want to dispose of uncached views just to update the arrows.
onMoveUp: function() {
var previousView = this.getPreviousView();
this.attachTo(previousView, {
method: 'before',
forceRender: true,
preserve: true
});
previousView.render({preserve: true});
},
getPreviousView: function() {
var $parentChildren = this.$el.parent().find('> .child-view');
var index = $parentChildren.index(this.$el);
if (index > 0)
return Giraffe.View.getClosestView($parentChildren[index - 1]);
else
return this.parent;
},
onMoveDown: function() {
var nextView = this.getNextView();
this.attachTo(nextView, {
method: 'after',
forceRender: true,
preserve: true
});
nextView.render({preserve: true});
},
getNextView: function() {
var $parentChildren = this.$el.parent().find('> .child-view');
var index = $parentChildren.index(this.$el);
if (index < $parentChildren.length - 1)
return Giraffe.View.getClosestView($parentChildren[index + 1]);
else
return this.parent;
},
The 'html' jQuery method replaces existing content. Giraffe
automatically detaches any views that get in the way when it's used. We'll add a
button to see how this behavior works with sibling views.
onAttachUsingHTML: function() {
this.attachTo(this.$el.parent(), {method: 'html'});
},
{method: 'html'} will
be automatically detached. If the detached views are cached, they will remain in
children and will be reattached when the parent renders since afterRender
attaches all child views.
Let's use the console to see when views get disposed.
dispose: function() {
Giraffe.dispose.call(this);
console.log('Disposing of ' + this.cid);
},
Here's the child view's serialize function and template:
template: '#child-template',
serialize: function() {
var $parentChildren = this.$el.parent().find('> .child-view');
var index = $parentChildren.index(this.$el);
var parentIsChildView = this.parent instanceof ChildView;
return {
parentIsChildView: parentIsChildView,
showMoveUpButton: parentIsChildView || index !== 0,
showMoveDownButton: parentIsChildView || index !== $parentChildren.length - 1,
checkedAttr: this.disposeOnDetach ? '' : "checked='checked'",
renderCount: this.renderCount,
cid: this.cid
};
}
});
<script id="child-template" type="text/template">
<h3><%= cid %></h3>
<% if (showMoveUpButton) { %>
<button data-gf-click="onMoveUp">▲</button>
<% } %>
<% if (showMoveDownButton) { %>
<button data-gf-click="onMoveDown">▼</button>
<% } %>
<button data-gf-click="onAddChild">Add a child</button>
<button data-gf-click="render">Render count: <%= renderCount%></button>
<button data-gf-click="dispose">Dispose</button>
<% if (parentIsChildView) { %>
<label>
<input type="checkbox" data-gf-change="onToggleCache" <%= checkedAttr %>>
Cache this view
</label>
<button data-gf-click="onAttachUsingHTML">
Reattach to parent using jQuery method 'html'
</button>
<% } %>
<div class="child-views"></div>
</script>
Creating the App(s)
Phew, that's it! Let's create and attach the app.
The name property is only used for display purposes.
var app1 = new ParentApp({name: 'app1'});
app1.attachTo('body');
Let's make two parent apps. Why? Because we can!
var app2 = new ParentApp({name: 'app2'});
app2.attachTo('body');