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">&#9650;</button>
  <% } %>

  <% if (showMoveDownButton) { %>
    <button data-gf-click="onMoveDown">&#9660;</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>
The attribute 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'});
  },
In this example, siblings of a view reattached with {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">&#9650;</button>
  <% } %>

  <% if (showMoveDownButton) { %>
    <button data-gf-click="onMoveDown">&#9660;</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');