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');