View.js

/**
 * The backbone View reference
 * @external Backbone-View
 * @extends external:Backbone-Events
 * @see {@link http://backbonejs.org/#View|Backbone.View}
 */
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['underscore', 'backbone', './templateRenderer', './Cell', './NestedCell', './registry'], factory);
  } else if (typeof exports === 'object') {
    module.exports = factory(require('underscore'), require('backbone'), require('./templateRenderer'), require('./Cell'), require('./NestedCell'), require('./registry'));
  } else {
    root.Torso = root.Torso || {};
    root.Torso.View = factory(root._, root.Backbone, root.Torso.Utils.templateRenderer, root.Torso.Cell, root.Torso.NestedCell, root.Torso.registry);
  }
}(this, function(_, Backbone, templateRenderer, Cell, NestedCell, registry) {
  'use strict';

  var $ = Backbone.$;

  /**
   * ViewStateCell is a NestedCell that holds view state data and can trigger
   * change events. These change events will propagate up and trigger on the view
   * as well.
   *
   * @class ViewStateCell
   * @extends {NestedCell}
   *
   * @param {Object} attrs the initial values to set on the cell - inherited from {@link NestedCell}.
   * @param {Object} opts options for the cell.
   *    @param {external:Backbone-View} opts.view the view that these options are tied to.
   *
   * @see <a href="../annotated/modules/View.html">View Annotated Source</a>
   */
  var ViewStateCell = NestedCell.extend(/** @lends ViewStateCell# */{
    /**
     * @constructs
     * @param {Object} attrs the initial values to set on the cell - inherited from {@link NestedCell}.
     * @param {Object} opts options for the cell.
     *    @param {external:Backbone-View} opts.view the view that these options are tied to.
     */
    initialize: function(attrs, opts) {
      opts = opts || {};
      this.view = opts.view;
    },

    /**
     * Retrigger view state change events on the view as well.
     * @override
     */
    trigger: function(name) {
      if (name === 'change' || name.indexOf('change:') === 0) {
        View.prototype.trigger.apply(this.view, arguments);
      }
      if (name.indexOf('change:hide:') === 0) {
        this.view.render();
      }
      NestedCell.prototype.trigger.apply(this, arguments);
    }
  });

  var View = Backbone.View.extend(/** @lends View# */{
    /**
     * Cell that can be used to save state for rendering the view.
     * @type {ViewStateCell}
     */
    viewState: null,
    template: undefined,
    feedback: null,
    feedbackCell: null,
    behaviors: null,
    templateRendererOptions: undefined,
    prepareFields: null,
    injectionSites: null,
    __behaviorInstances: null,
    __childViews: null,
    __sharedViews: null,
    __isActive: false,
    __isAttachedToParent: false,
    __isDisposed: false,
    __attachedCallbackInvoked: false,
    __feedbackOnEvents: null,
    __feedbackListenToEvents: null,
    /**
     * Array of feedback when-then-to's. Example:
     * [{
     *   when: {'@fullName': ['change']},
     *   then: function(event) { return {text: this.feedbackCell.get('fullName')};},
     *   to: 'fullName-feedback'
     * }]
     * @private
     * @property {Array} feedback
     */

    /**
     * Overrides constructor to create needed fields and invoke activate/render after initialization
     *
     * Generic View that deals with:
     * - Creation of private collections
     * - Lifecycle of a view
     *
     * @class View
     * @extends {external:Backbone-View}
     * @author ariel.wexler@vecna.com, kent.willis@vecna.com
     * @constructs
     *
     * @see <a href="../annotated/modules/View.html">View Annotated Source</a>
     */
    constructor: function(options) {
      options = options || {};
      this.viewState = new ViewStateCell({}, { view: this });
      this.feedbackCell = new Cell();
      this.__childViews = {};
      this.__sharedViews = {};
      this.__injectionSiteMap = {};
      this.__feedbackOnEvents = [];
      this.__feedbackListenToEvents = [];
      this.template = options.template || this.template;
      this.templateRendererOptions = options.templateRendererOptions || this.templateRendererOptions;
      this.__initializeBehaviors(options);
      this.trigger('initialize:begin');
      Backbone.View.apply(this, arguments);
      this.trigger('initialize:complete');
      if (!options.noActivate) {
        this.activate();
      }
      // Register by default.
      var shouldRegister = _.isUndefined(options.register) || _.isNull(options.register) || options.register;
      if (shouldRegister) {
        registry.viewInitialized(this);
      }
    },

    /**
     * Alias to this.viewState.get()
     */
    get: function() {
      return this.viewState.get.apply(this.viewState, arguments);
    },

    /**
     * Alias to this.viewState.set()
     */
    set: function() {
      return this.viewState.set.apply(this.viewState, arguments);
    },

    /**
     * Alias to this.viewState.has()
     */
    has: function() {
      return this.viewState.has.apply(this.viewState, arguments);
    },

    /**
     * Alias to this.viewState.unset()
     */
    unset: function() {
      return this.viewState.unset.apply(this.viewState, arguments);
    },

    /**
     * Alias to this.viewState.toJSON()
     */
    toJSON: function() {
      return this.viewState.toJSON();
    },

    /**
     * @param {string} alias the name/alias of the behavior
     * @return {Torso.Behavior} the behavior instance if one exists with that alias
     */
    getBehavior: function(alias) {
      if (this.__behaviorInstances) {
        return this.__behaviorInstances[alias];
      }
    },

    /**
     * prepareFields can be used to augment the default render method contents.
     * See __getPrepareFieldsContext() for more details on how to configure them.
     *
     * @return {Object} context for a render method. Defaults to:
     *    {view: this.viewState.toJSON(), model: this.model.toJSON()}
     */
    prepare: function() {
      return this.__getPrepareFieldsContext();
    },

    /**
     * Extension point to augment the template context with custom content.
     * @function
     * @param context the context you can modify
     * @return {Object} [Optional] If you return an object, it will be merged with the context
     */
    _prepare: function() {
      // do nothing, here for overrides and to properly inform jsdoc that this is a method.
    },

    /**
     * Rebuilds the html for this view's element. Should be able to be called at any time.
     * Defaults to using this.templateRender. Assumes that this.template is a javascript
     * function that accepted a single JSON context.
     * The render method returns a promise that resolves when rendering is complete. Typically render
     * is synchronous and the rendering is complete upon completion of the method. However, when utilizing
     * transitions/animations, the render process can be asynchronous and the promise is useful to know when it has finished.
     * @return {Promise} a promise that when resolved signifies that the rendering process is complete.
     */
    render: function() {
      if (this.isDisposed()) {
        throw new Error('Render called on a view that has already been disposed.');
      }
      var view = this;
      this.trigger('render:begin');
      if (this.prerender() === false) {
        this.trigger('render:aborted');
        return $.Deferred().resolve().promise();
      }
      this.__updateInjectionSiteMap();
      this.trigger('render:before-dom-update');
      this.detachTrackedViews();
      this.updateDOM();
      if (this.__pendingAttachInfo) {
        this.__performPendingAttach();
      }
      this.trigger('render:after-dom-update');
      this.delegateEvents();
      this.trigger('render:after-delegate-events');
      this.unregisterTrackedViews({ shared: true });
      this.trigger('render:before-attach-tracked-views');
      this.__attachViewsFromInjectionSites();
      var promises = this.attachTrackedViews();
      return $.when.apply($, _.flatten([promises])).done(function() {
        view.postrender();
        view.trigger('render:complete');
        view.__injectionSiteMap = {};
        view.__lastTrackedViews = {};
      });
    },

    /**
     * Hook during render that is invoked before any DOM rendering is performed.
     * This method can be overwritten as usual OR extended using <baseClass>.prototype.prerender.apply(this, arguments);
     * NOTE: if you require the view to be detached from the DOM, consider using _detach callback
     * @return {Promise|Array<Promise>} you can optionally return one or more promises that when all are resolved, prerender is finished. Note: render logic will not wait until promises are resolved.
     */
    prerender: function() {
      // do nothing, here for overrides and to properly inform jsdoc that this is a method.
    },

    /**
     * Produces and sets this view's elements DOM. Used during the rendering process. Override if you have custom DOM update logic.
     * Defaults to using the stanrdard: this.templateRender(this.$el, this.template, this.prepare(), templateRendererOptions);
     * this.templateRendererOptions is an object or function defined on the view that is passed into the renderer.
     * Examples include: views with no template or multiple templates, or if you wish to use a different rendering engine than the templateRenderer or wish to pass options to it.
     */
    updateDOM: function() {
      if (this.template) {
        var templateRendererOptions = _.result(this, 'templateRendererOptions');
        this.templateRender(this.$el, this.template, this.prepare(), templateRendererOptions);
      }
    },

    /**
     * Updates this view element's class attribute with the value provided.
     * If no value provided, removes the class attribute of this view element.
     * @param {string} newClassName the new value of the class attribute
     */
    updateClassName: function(newClassName) {
      if (newClassName === undefined) {
        this.$el.removeAttr('class');
      } else {
        this.$el.attr('class', newClassName);
      }
    },

    /**
     * Hook during render that is invoked after all DOM rendering is done and tracked views attached.
     * This method can be overwritten as usual OR extended using <baseClass>.prototype.postrender.apply(this, arguments);
     * NOTE: if you require the view to be attached to the DOM, consider using _attach callback
     * @return {Promise|Array<Promise>} you can optionally return one or more promises that when all are resolved, postrender is finished. Note: render logic will not wait until promises are resolved.
     */
    postrender: function() {
      // do nothing, here for overrides and to properly inform jsdoc that this is a method.
    },

    /**
     * Hotswap rendering system reroute method.
     * See Torso.templateRenderer#render for params
     */
    templateRender: function(el, template, context, opts) {
      opts = opts || {};
      if (_.isString(template)) {
        opts.newHTML = template;
      }
      templateRenderer.render(el, template, context, opts);
    },

    /**
     * Overrides the base delegateEvents
     * Binds DOM events with the view using events hash while also adding feedback event bindings
     */
    delegateEvents: function() {
      this.undelegateEvents(); // always undelegate events - backbone sometimes doesn't.
      Backbone.View.prototype.delegateEvents.call(this);
      this.__generateFeedbackBindings();
      this.__generateFeedbackCellCallbacks();
      _.each(this.getTrackedViews(), function(view) {
        if (view.isAttachedToParent()) {
          view.delegateEvents();
        }
      });
    },

    /**
     * Overrides undelegateEvents
     * Unbinds DOM events from the view.
     */
    undelegateEvents: function() {
      Backbone.View.prototype.undelegateEvents.call(this);
      _.each(this.getTrackedViews(), function(view) {
        view.undelegateEvents();
      });
    },

    /**
     * If detached, will replace the element passed in with this view's element and activate the view.
     * @param {external:jQuery} [$el] the element to attach to. This element will be replaced with this view.
     *                       If options.replaceMethod is provided, then this parameter is ignored.
     * @param {Object} [options] optional options
     * @param   {Fucntion} [options.replaceMethod] if given, this view will invoke replaceMethod function
     *                                             in order to attach the view's DOM to the parent instead of calling $el.replaceWith
     * @param   {Booleon} [options.discardInjectionSite=false] if set to true, the injection site is not saved.
     * @return {Promise} promise that when resolved, the attach process is complete. Normally this method is synchronous. Transition effects can
     *                   make it asynchronous.
     */
    attachTo: function($el, options) {
      options = options || {};
      var view = this;
      if (!this.isAttachedToParent()) {
        this.__pendingAttachInfo = {
          $el: $el,
          options: options
        };
        return this.render().done(function() {
          if (!view.__attachedCallbackInvoked && view.isAttached()) {
            view.__invokeAttached();
          }
          view.__isAttachedToParent = true;
        });
      }
      return $.Deferred().resolve().promise();
    },

    /**
     * Registers the view as a tracked view (defaulting as a child view), then calls view.attachTo with the element argument
     * The element argument can be a String that references an element with the corresponding "inject" attribute.
     * When using attachView with options.useTransition:
     *   Will inject a new view into an injection site by using the new view's transitionIn method. If the parent view
     *   previously had another view at this injections site, this previous view will be removed with that view's transitionOut.
     *   If this method is used within a render, the current views' injection sites will be cached so they can be transitioned out even
     *   though they are detached in the process of re-rendering. If no previous view is given and none can be found, the new view is transitioned in regardless.
     *   If the previous view is the same as the new view, it is injected normally without transitioning in.
     *   The previous view must has used an injection site with the standard "inject=<name of injection site>" attribute to be found.
     *   When the transitionIn and transitionOut methods are invoked on the new and previous views, the options parameter will be passed on to them. Other fields
     *   will be added to the options parameter to allow better handling of the transitions. These include:
     *   {
     *     newView: the new view
     *     previousView: the previous view (can be undefined)
     *     parentView: the parent view transitioning in or out the tracked view
     *   }
     * @param {(external:jQuery|string)} $el the element to attach to OR the name of the injection site. The element with the attribute "inject=<name of injection site>" will be used.
     * @param {View}   view   The instantiated view object to be attached
     * @param {Object} [options] optionals options object. If using transitions, this options object will be passed on to the transitionIn and transitionOut methods as well.
     * @param   {boolean} [options.noActivate=false] if set to true, the view will not be activated upon attaching.
     * @param   {boolean} [options.shared=false] if set to true, the view will be treated as a shared view and not disposed during parent view disposing.
     * @param   {boolean} [options.useTransition=false] if set to true, this method will delegate attach logic to this.__transitionNewViewIntoSite
     * @param   {boolean} [options.addBefore=false] if true, and options.useTransition is true, the new view's element will be added before the previous view's element. Defaults to after.
     * @param   {View} [options.previousView] if using options.useTransition, then you can explicitly define the view that should be transitioned out.
     *                                        If using transitions and no previousView is provided, it will look to see if a view already is at this injection site and uses that by default.
     * @return {Promise} resolved when all transitions are complete. No payload is provided upon resolution. If no transitions, then returns a resolved promise.
     */
    attachView: function($el, view, options) {
      options = options || {};
      var injectionSite, injectionSiteName,
        usesInjectionSiteName = _.isString($el);
      if (usesInjectionSiteName) {
        injectionSiteName = $el;
        injectionSite = this.$('[inject=' + injectionSiteName + ']');
        if (!injectionSite) {
          throw 'View.attachView: No injection site found with which to attach this view. View.cid=' + this.cid;
        }
      } else {
        injectionSite = $el;
      }
      if (options.useTransition) {
        return this.__transitionNewViewIntoSite(injectionSiteName, view, options);
      }
      view.detach();
      this.registerTrackedView(view, options);
      view.attachTo(injectionSite, options);
      if (!options.noActivate) {
        view.activate();
      }
      return $.Deferred().resolve().promise();
    },

    /**
     * Hook to attach all your tracked views. This hook will be called after all DOM rendering is done so injection sites should be available.
     * This method can be overwritten as usual OR extended using <baseClass>.prototype.attachTrackedViews.apply(this, arguments);
     * @return {Promise|Array<Promise>} you can optionally return one or more promises that when all are resolved, all tracked views are attached. Useful when using this.attachView with useTransition=true.
     */
    attachTrackedViews: function() {
      // do nothing, here for overrides and to properly inform jsdoc that this is a method.
    },

    /**
     * Method to be invoked when the view is fully attached to the DOM (NOT just the parent). Use this method to manipulate the view
     * after the DOM has been attached to the document. The default implementation is a no-op.
     */
    _attached: function() {
      // do nothing, here for overrides and to properly inform jsdoc that this is a method.
    },

    /**
     * @return {boolean} true if the view is attached to a parent
     */
    isAttachedToParent: function() {
      return this.__isAttachedToParent;
    },

    /**
     * NOTE: depends on a global variable "document"
     * @return {boolean} true if the view is attached to the DOM
     */
    isAttached: function() {
      return this.$el && $.contains(document, this.$el[0]);
    },

    /**
     * If attached, will detach the view from the DOM.
     * This method will only separate this view from the DOM it was attached to, but it WILL invoke the _detach
     * callback on each tracked view recursively.
     */
    detach: function() {
      var wasAttached;
      if (this.isAttachedToParent()) {
         wasAttached = this.isAttached();
        // Detach view from DOM
        this.trigger('before-dom-detach');
        if (this.injectionSite) {
          this.$el.replaceWith(this.injectionSite);
          this.injectionSite = undefined;
        } else {
          this.$el.detach();
        }
        if (wasAttached) {
          this.__invokeDetached();
        }
        this.undelegateEvents();
        this.__isAttachedToParent = false;
      }
    },

    /**
     * Detach all tracked views or a subset of them based on the options parameter.
     * NOTE: this is not recursive - it will not separate the entire view tree.
     * @param {Object} [options={}]  Optional options.
     *   @param {boolean} [options.shared=false] If true, detach only the shared views. These are views not owned by this parent. As compared to a child view
     *                                           which are disposed when the parent is disposed.
     *   @param {boolean} [options.child=false] If true, detach only child views. These are views that are owned by the parent and dispose of them if the parent is disposed.
     */
    detachTrackedViews: function(options) {
      var trackedViewsHash = this.getTrackedViews(options);
      _.each(trackedViewsHash, function(view) {
        view.detach();
      });
    },

    /**
     * Method to be invoked when the view is detached from the DOM (NOT just the parent). Use this method to clean up state
     * after the view has been removed from the document. The default implementation is a no-op.
     */
    _detached: function() {
      // do nothing, here for overrides and to properly inform jsdoc that this is a method.
    },

    /**
     * Resets listeners and events in order for the view to be reattached to the visible DOM
     */
    activate: function() {
      this.__activateTrackedViews();
      if (!this.isActive()) {
        this.trigger('before-activate-callback');
        this._activate();
        this.__isActive = true;
      }
    },

    /**
     * Method to be invoked when activate is called. Use this method to turn on any
     * custom timers, listenTo's or on's that should be activatable. The default implementation is a no-op.
     */
    _activate: function() {
      // do nothing, here for overrides and to properly inform jsdoc that this is a method.
    },

    /**
     * @return {boolean} true if the view is active
     */
    isActive: function() {
      return this.__isActive;
    },

    /**
     * Maintains view state and DOM but prevents view from becoming a zombie by removing listeners
     * and events that may affect user experience. Recursively invokes deactivate on child views
     */
    deactivate: function() {
      this.__deactivateTrackedViews();
      if (this.isActive()) {
        this.trigger('before-deactivate-callback');
        this._deactivate();
        this.__isActive = false;
      }
    },

    /**
     * Method to be invoked when deactivate is called. Use this method to turn off any
     * custom timers, listenTo's or on's that should be deactivatable. The default implementation is a no-op.
     */
    _deactivate: function() {
      // do nothing, here for overrides and to properly inform jsdoc that this is a method.
    },

    /**
     * Removes all listeners, disposes children views, stops listening to events, removes DOM.
     * After dispose is called, the view can be safely garbage collected. Called while
     * recursively removing views from the hierarchy.
     */
    dispose: function() {
      this.trigger('before-dispose');
      this.trigger('before-dispose-callback');
      this._dispose();

      // Detach DOM and deactivate the view
      this.detach();
      this.deactivate();

      // Clean up child views first
      this.__disposeChildViews();

      // Remove view from DOM
      if (this.$el) {
        this.remove();
      }

      // Unbind all local event bindings
      this.off();
      this.stopListening();
      if (this.viewState) {
        this.viewState.off();
        this.viewState.stopListening();
      }
      if (this.feedbackCell) {
        this.feedbackCell.off();
        this.feedbackCell.stopListening();
      }
      // Delete the dom references
      delete this.$el;
      delete this.el;

      this.__isDisposed = true;
      this.trigger('after-dispose');
    },

    /**
     * Method to be invoked when dispose is called. By default calling dispose will remove the
     * view's element, its on's, listenTo's, and any registered children.
     * Override this method to destruct any extra
     */
    _dispose: function() {
      // do nothing, here for overrides and to properly inform jsdoc that this is a method.
    },

    /**
     * @return {boolean} true if the view was disposed
     */
    isDisposed: function() {
      return this.__isDisposed;
    },

    /**
     * @return {boolean} true if this view has tracked views (limited by the options parameter)
     * @param {Object} [options={}]  Optional options.
     *   @param {boolean} [options.shared=false] If true, only check the shared views. These are views not owned by this parent. As compared to a child view
     *                                           which are disposed when the parent is disposed.
     *   @param {boolean} [options.child=false] If true, only check the child views. These are views that are owned by the parent and dispose of them if the parent is disposed.
     */
    hasTrackedViews: function(options) {
      return !_.isEmpty(this.getTrackedViews(options));
    },

    /**
     * Returns all tracked views, both child views and shared views.
     * @param {Object} [options={}]  Optional options.
     *   @param {boolean} [options.shared=false] If true, get only the shared views. These are views not owned by this parent. As compared to a child view
     *                                           which are disposed when the parent is disposed.
     *   @param {boolean} [options.child=false] If true, get only child views. These are views that are owned by the parent and dispose of them if the parent is disposed.
     * @return {List<View>} all tracked views (filtered by options parameter)
     */
    getTrackedViews: function(options) {
      return _.values(this.__getTrackedViewsHash(options));
    },

    /**
     * @return the view with the given cid.  Will look in both shared and child views.
     * @param {string} viewCID the cid of the view
     */
    getTrackedView: function(viewCID) {
      var childView = this.__childViews[viewCID],
          sharedView = this.__sharedViews[viewCID];
      return childView || sharedView;
    },

    /**
     * Binds the view as a tracked view - any recursive calls like activate, deactivate, or dispose will
     * be done to the tracked view as well.  Except dispose for shared views. This method defaults to register the
     * view as a child view unless specified by options.shared.
     * @param {View} view the tracked view
     * @param {Object} [options={}]  Optional options.
     *   @param {boolean} [options.shared=false] If true, registers view as a shared view. These are views not owned by this parent. As compared to a child view
     *                                           which are disposed when the parent is disposed. If false, registers view as a child view which are disposed when the parent is disposed.
     * @return {View} the tracked view
     */
    registerTrackedView: function(view, options) {
      options = options || {};
      this.unregisterTrackedView(view);
      if (options.child || !options.shared) {
        this.__childViews[view.cid] = view;
      } else {
        this.__sharedViews[view.cid] = view;
      }
      return view;
    },

    /**
     * Unbinds the tracked view - no recursive calls will be made to this shared view
     * @param {View} view the shared view
     * @return {View} the tracked view
     */
    unregisterTrackedView: function(view) {
      delete this.__childViews[view.cid];
      delete this.__sharedViews[view.cid];
      return view;
    },

    /**
     * Unbinds all tracked view - no recursive calls will be made to this shared view
     * You can limit the types of views that will be unregistered by using the options parameter.
     * @param {Object} [options={}]  Optional options.
     *   @param {boolean} [options.shared=false] If true, unregister only the shared views. These are views not owned by this parent. As compared to a child view
     *                                           which are disposed when the parent is disposed.
     *   @param {boolean} [options.child=false] If true, unregister only child views. These are views that are owned by the parent and dispose of them if the parent is disposed.
     * @return {View} the tracked view
     */
    unregisterTrackedViews: function(options) {
      var trackedViewsHash = this.getTrackedViews(options);
      _.each(trackedViewsHash, function(view) {
        this.unregisterTrackedView(view, options);
      }, this);
    },

    /**
     * Override to provide your own transition out logic. Default logic is to just detach from the page.
     * The method is passed a callback that should be invoked when the transition out has fully completed.
     * @param {Function} done callback that MUST be invoked when the transition is complete.
     * @param options optionals options object
     * @param   {View} options.currentView the view that is being transitioned in.
     * @param   {View} options.previousView the view that is being transitioned out. Typically this view.
     * @param   {View} options.parentView the view that is invoking the transition.
     */
    transitionOut: function(done, options) {
      this.detach();
      done();
    },

    /**
     * Override to provide your own transition in logic. Default logic is to just attach to the page.
     * The method is passed a callback that should be invoked when the transition in has fully completed.
     * @param {Function} attach callback to be invoked when you want this view to be attached to the dom.
                                If you are trying to transition in a tracked view, consider using this.transitionInView()
     * @param {Function} done callback that MUST be invoked when the transition is complete.
     * @param options optionals options object
     * @param   {View} options.currentView the view that is being transitioned in.
     * @param   {View} options.previousView the view that is being transitioned out. Typically this view.
     * @param   {View} options.parentView the view that is invoking the transition.
     */
    transitionIn: function(attach, done, options) {
      attach();
      done();
    },

    /**
     * Invokes a feedback entry's "then" method
     * @param {string} to the "to" field corresponding to the feedback entry to be invoked.
     * @param {Event} [evt] the event to be passed to the "then" method
     * @param {Object} [indexMap] a map from index variable name to index value. Needed for "to" fields with array notation.
     */
    invokeFeedback: function(to, evt, indexMap) {
      var result,
        feedbackToInvoke = _.find(this.feedback, function(feedback) {
          var toToCheck = feedback.to;
          if (_.isArray(toToCheck)) {
            return _.contains(toToCheck, to);
          } else {
            return to === toToCheck;
          }
        }),
        feedbackCellField = to;
      if (feedbackToInvoke) {
        if (indexMap) {
          feedbackCellField = this.__substituteIndicesUsingMap(to, indexMap);
        }
        result = feedbackToInvoke.then.call(this, evt, indexMap);
        this.__processFeedbackThenResult(result, feedbackCellField);
      }
    },

    //************** Private methods **************//

    /**
     * Attaches views using this.injectionSites. The API for injectionSites looks like:
     * injectionSites: {
     *   foo: fooView,  // foo is injectionSite, fooView is the view
         bar: 'barView',  // bar is injectionSite, 'barView' is a field on the view (view.barView)
         baz: function() {  // baz is injectionSite
           return this.bazView;  // the context 'this' is the view
         },
         taz: {  // if you want to pass in options, use a config object with 'view' and 'options'
           view: (same as the three above: direct reference, string of view field, or function that return view),
           options: {} // optional options
         }
     * }
     * To create dynamic show/hide logic, perform the logic in a function that returns the correct view, or you can
     * call this.set('hide:foo', true) or this.set('hide:foo', false)
     * @private
     */
    __attachViewsFromInjectionSites: function() {
      var injectionSites = _.result(this, 'injectionSites');
      _.each(injectionSites, function(config, injectionSiteName) {
        if (!this.get('hide:' + injectionSiteName)) {
          var options = {};
          var trackedView;
          if (_.isFunction(config)) {
            config = config.call(this);
          }
          if (config instanceof Backbone.View) {
            trackedView = config;
          } else if (_.isObject(config)) {
            options = config.options;
            config = config.view;
          }
          if (!trackedView) {
            if (_.isString(config)) {
              trackedView = _.result(this, config);
            } else if (config instanceof Backbone.View) {
              trackedView = config;
            } else if (_.isFunction(config)) {
              trackedView = config.call(this);
            }
          }
          if (trackedView) {
            this.attachView(injectionSiteName, trackedView, options);
          }
        }
      }, this);
    },

    /**
     * Parses the combined arrays from the defaultPrepareFields array and the prepareFields array (or function
     * returning an array).
     *
     * The default prepared fields are: [ { name: 'view', value: 'viewState' }, 'model' ]
     *
     * Prepared fields can be defined in a couple of ways:
     *   preparedFields = [
     *     'model',
     *     { name: 'app', value: someGlobalCell },
     *     'a value that does not exist on the view',
     *     { name: 'view', value: 'viewState' },
     *     { name: 'patientId', value: '_patientId' },
     *     { name: 'calculatedValue', value: function() { return 'calculated: ' + this.viewProperty },
     *     'objectWithoutToJSON'
     *   ]
     *
     * Will result in the following context (where this === this view and it assumes all the properties on the view
     * that are referenced are defined):
     *   {
     *     model: this.model.toJSON(),
     *     app: someGlobalCell.toJSON(),
     *     view: this.viewState.toJSON(),
     *     patientId: this._patientId,
     *     calculatedValue: 'calculated: ' + this.viewProperty,
     *     objectWithoutToJSON: this.objectWithoutToJSON
     *   }
     *
     * Note: alternatively, you can define your prepareFields as an object that will be mapped to an array of { name: key, value: value }
     *
     * Things to be careful of:
     *   * If the view already has a field named 'someGlobalCell' then the property on the view will be used instead of the global value.
     *   * if the prepared field item is not a string or object containing 'name' and 'value' properties, then an exception
     *     will be thrown.
     *   * 'model' and 'view' are reserved field names and cannot be reused.
     *
     * @return {Object} context composed of { modelName: model.toJSON() } for every model identified.
     * @private
     */
    __getPrepareFieldsContext: function() {
      var prepareFieldsContext = {};
      var prepareFields = _.result(this, 'prepareFields');
      if (prepareFields && _.isObject(prepareFields) && !_.isArray(prepareFields)) {
        var keys = _.keys(prepareFields);
        prepareFields = _.map(keys, function(key) {
          return { name: key, value: prepareFields[key] };
        });
      }
      var defaultPrepareFields = [ { name: 'view', value: 'viewState' }, 'model' ];
      prepareFields = _.union(prepareFields, defaultPrepareFields);
      if (prepareFields && prepareFields.length > 0) {
        for (var fieldIndex = 0; fieldIndex < prepareFields.length; fieldIndex++) {
          var prepareField = prepareFields[fieldIndex];
          var prepareFieldIsSimpleString = _.isString(prepareField);

          var prepareFieldName = prepareField;
          var prepareFieldValue = prepareField;

          if (!prepareFieldIsSimpleString) {
            if (!_.isString(prepareField.name)) {
              throw "prepareFields items need to either be a string or define a .name property that is a simple string to use for the key in the template context.";
            }

            if (_.isUndefined(prepareField.value)) {
              throw "prepareFields items need a value property if it is not a string.";
            }

            prepareFieldName = prepareField.name;
            prepareFieldValue = prepareField.value;
          }
          if (!_.isUndefined(prepareFieldsContext[prepareFieldName])) {
            throw "duplicate prepareFields name (" + prepareFieldName + ").  Note 'view' and 'model' are reserved names.";
          }

          var prepareFieldValueIsDefinedOnView = false;
          if (_.isFunction(prepareFieldValue)) {
            prepareFieldValue = prepareFieldValue.call(this);
          } else {
            // Note _.result() also returns undefined if the 2nd argument is not a string.
            var prepareFieldValueFromView = _.result(this, prepareFieldValue);
            prepareFieldValueIsDefinedOnView = !_.isUndefined(prepareFieldValueFromView);
            if (prepareFieldValueIsDefinedOnView) {
              prepareFieldValue = prepareFieldValueFromView;
            }
          }

          if (prepareFieldValue && _.isFunction(prepareFieldValue.toJSON)) {
            prepareFieldsContext[prepareFieldName] = prepareFieldValue.toJSON();
          } else if (!prepareFieldIsSimpleString || prepareFieldValueIsDefinedOnView) {
            prepareFieldsContext[prepareFieldName] = prepareFieldValue;
          }
        }
      }
      var context = this._prepare(prepareFieldsContext);
      if (_.isUndefined(context)) {
        context = prepareFieldsContext;
      } else {
        context = _.extend(prepareFieldsContext, context);
      }
      return context;
    },

    /**
     * Initializes the behaviors
     * @private
     */
    __initializeBehaviors: function(viewOptions) {
      var view = this;
      if (!_.isEmpty(this.behaviors)) {
        view.__behaviorInstances = {};
        _.each(this.behaviors, function(behaviorDefinition, alias) {
          if (!_.has(behaviorDefinition, 'behavior')) {
            behaviorDefinition = {behavior: behaviorDefinition};
          }
          var BehaviorClass = behaviorDefinition.behavior;
          if (!(BehaviorClass && _.isFunction(BehaviorClass))) {
            throw new Error('Incorrect behavior definition. Expected key "behavior" to be a class but instead got ' +
              String(BehaviorClass));
          }

          var behaviorOptions = _.pick(behaviorDefinition, function(value, key) {
            return key !== 'behavior';
          });
          behaviorOptions.view = view;
          behaviorOptions.alias = alias;
          var behaviorAttributes = behaviorDefinition.attributes || {};
          var behaviorInstance = view.__behaviorInstances[alias] = new BehaviorClass(behaviorAttributes, behaviorOptions, viewOptions);
          // Add the behavior's mixin fields to the view's public API
          if (behaviorInstance.mixin) {
            var mixin = _.result(behaviorInstance, 'mixin');
            _.each(mixin, function(field, fieldName) {
              // Default to a view's field over a behavior mixin
              if (_.isUndefined(view[fieldName])) {
                if (_.isFunction(field)) {
                  // Behavior mixin functions will be behavior-scoped - the context will be the behavior.
                  view[fieldName] = _.bind(field, behaviorInstance);
                } else {
                  view[fieldName] = field;
                }
              }
            });
          }
        });
      }
    },

    /**
     * If the view is attaching during the render process, then it replaces the injection site
     * with the view's element after the view has generated its DOM.
     * @private
     */
    __performPendingAttach: function() {
      this.trigger('before-dom-attach');
      this.__replaceInjectionSite(this.__pendingAttachInfo.$el, this.__pendingAttachInfo.options);
      delete this.__pendingAttachInfo;
    },

    /**
     * Deactivates all tracked views or a subset of them based on the options parameter.
     * @param {Object} [options={}]  Optional options.
     *   @param {boolean} [options.shared=false] If true, deactivate only the shared views. These are views not owned by this parent. As compared to a child view
     *                                           which are disposed when the parent is disposed.
     *   @param {boolean} [options.child=false] If true, deactivate only child views. These are views that are owned by the parent and dispose of them if the parent is disposed.
     * @private
     */
    __deactivateTrackedViews: function(options) {
      _.each(this.getTrackedViews(options), function(view) {
        view.deactivate();
      });
    },

    /**
     * Activates all tracked views or a subset of them based on the options parameter.
     * @param {Object} [options={}]  Optional options.
     *   @param {boolean} [options.shared=false] If true, activate only the shared views. These are views not owned by this parent. As compared to a child view
     *                                           which are disposed when the parent is disposed.
     *   @param {boolean} [options.child=false] If true, activate only child views. These are views that are owned by the parent and dispose of them if the parent is disposed.
     * @private
     */
    __activateTrackedViews: function(options) {
      _.each(this.getTrackedViews(options), function(view) {
        view.activate();
      });
    },

    /**
     * Disposes all child views recursively
     * @private
     */
    __disposeChildViews: function() {
      _.each(this.__childViews, function(view) {
        view.dispose();
      });
    },

    /**
     * Will inject a new view into an injection site by using the new view's transitionIn method. If the parent view
     * previously had another view at this injections site, this previous view will be removed with that view's transitionOut.
     * If this method is used within a render, the current views' injection sites will be cached so they can be transitioned out even
     * though they are detached in the process of re-rendering. If no previous view is given and none can be found, the new view is transitioned in regardless.
     * If the previous view is the same as the new view, it is injected normally without transitioning in.
     * The previous view must has used an injection site with the standard "inject=<name of injection site>" attribute to be found.
     * @private
     * @param {string} injectionSiteName The name of the injection site in the template. This is the value corresponding to the attribute "inject".
     * @param {View} newView The instantiated view object to be transitioned into the injection site
     * @param {Object} [options] optional options object. This options object will be passed on to the transitionIn and transitionOut methods as well.
     * @param   {View} [options.previousView] the view that should be transitioned out. If none is provided, it will look to see if a view already
     *                                 is at this injection site and uses that by default.
     * @param   {boolean} [options.addBefore=false] if true, the new view's element will be added before the previous view's element. Defaults to after.
     * @param   {boolean} [options.shared=false] if set to true, the view will be treated as a shared view and not disposed during parent view disposing.
     * @return {Promise} resolved when all transitions are complete. No payload is provided upon resolution.
     * When the transitionIn and transitionOut methods are invoked on the new and previous views, the options parameter will be passed on to them. Other fields
     * will be added to the options parameter to allow better handling of the transitions. These include:
     * {
     *   newView: the new view
     *   previousView: the previous view (can be undefined)
     *   parentView: the parent view transitioning in or out the tracked view
     * }
     */
    __transitionNewViewIntoSite: function(injectionSiteName, newView, options) {
      var previousView, injectionSite;
      options = options || {};
      // find previous view that used this injection site.
      previousView = options.previousView;
      if (!previousView) {
        previousView = this.__injectionSiteMap[injectionSiteName];
      }
      _.defaults(options, {
        parentView: this,
        newView: newView,
        previousView: previousView
      });
      options.useTransition = false;
      if (previousView == newView) {
        // Inject this view like normal if it's already the last one there
        return this.attachView(injectionSiteName, newView, options);
      }
      if (!previousView) {
        // Only transition in the new current view and find the injection site.
        injectionSite = this.$('[inject=' + injectionSiteName + ']');
        return this.__transitionInView(injectionSite, newView, options);
      }
      return this.__performTwoWayTransition(injectionSiteName, previousView, newView, options);
    },

    /**
     * Will transition out previousView at the same time as transitioning in newView.
     * @param {string} injectionSiteName The name of the injection site in the template. This is the value corresponding to the attribute "inject".
     * @param {View} previousView the view that should be transitioned out.
     * @param {View} newView The view that should be transitioned into the injection site
     * @param {Object} [options] optional options object. This options object will be passed on to the transitionIn and transitionOut methods as well.
     * @param   {boolean} [options.addBefore=false] if true, the new view's element will be added before the previous view's element. Defaults to after.
     * @return {Promise} resolved when all transitions are complete. No payload is provided upon resolution.
     * @private
     */
    __performTwoWayTransition: function(injectionSiteName, previousView, newView, options) {
      var newInjectionSite, currentPromise,
        previousDeferred = $.Deferred();
      this.attachView(injectionSiteName, previousView, options);
      options.cachedInjectionSite = previousView.injectionSite;
      newInjectionSite = options.newInjectionSite = $('<span inject="' + injectionSiteName + '">');
      if (options.addBefore) {
        previousView.$el.before(newInjectionSite);
      } else {
        previousView.$el.after(newInjectionSite);
      }

      // clear the injections site so it isn't replaced back into the dom.
      previousView.injectionSite = undefined;

      // transition previous view out
      previousView.transitionOut(previousDeferred.resolve, options);
      // transition new current view in
      currentPromise = this.__transitionInView(newInjectionSite, newView, options);

      // return a combined promise
      return $.when(previousDeferred.promise(), currentPromise);
    },

    /**
     * Simliar to this.attachView except it utilizes the new view's transitionIn method instead of just attaching the view.
     * This method is invoked on the parent view to attach a tracked view where the transitionIn method defines how a tracked view is brought onto the page.
     * @param {external:jQuery} $el the element to attach to.
     * @param {View} newView the view to be transitioned in.
     * @param {Object} [options] optional options object
     * @param   {boolean} [options.noActivate=false] if set to true, the view will not be activated upon attaching.
     * @param   {boolean} [options.shared=false] if set to true, the view will be treated as a shared view and not disposed during parent view disposing.
     * @return {Promise} resolved when transition is complete. No payload is provided upon resolution.
     * @private
     */
    __transitionInView: function($el, newView, options) {
      var currentDeferred = $.Deferred(),
        parentView = this;
      options = _.extend({}, options);
      _.defaults(options, {
        parentView: this,
        newView: newView
      });
      newView.transitionIn(function() {
        parentView.attachView($el, newView, options);
      }, currentDeferred.resolve, options);
      return currentDeferred.promise();
    },

    /**
     * Gets the hash from id to tracked views. You can limit the subset of views returned based on the options passed in.
     * NOTE: returns READ-ONLY snapshots. Updates to the returned cid->view map will not be saved nor will updates to the underlying maps be reflected later in returned objects.
     * This means that you can add "add" or "remove" tracked view using this method, however you can interact with the views inside the map completely.
     * @param {Object} [options={}]  Optional options.
     *   @param {boolean} [options.shared=false] If true, will add the shared views. These are views not owned by this parent. As compared to a child view
     *                                           which are disposed when the parent is disposed.
     *   @param {boolean} [options.child=false] If true, will add child views. These are views that are owned by the parent and dispose of them if the parent is disposed.
     * @return READ-ONLY snapshot of the object maps cotaining tracked views keyed by their cid (filtered by optional options parameter).
     * @private
     */
    __getTrackedViewsHash: function(options) {
      var views = {};
      options = options || {};
      if (options.shared) {
        views = _.extend(views, this.__sharedViews);
      }
      if (options.child) {
        views = _.extend(views, this.__childViews);
      }
      if (!options.child && !options.shared) {
        views = _.extend(views, this.__sharedViews, this.__childViews);
      }
      return views;
    },

    /**
     * Used internally by Torso.View to keep a cache of tracked views and their current injection sites before detaching during render logic.
     * @private
     */
    __updateInjectionSiteMap: function() {
      var parentView = this;
      this.__injectionSiteMap = {};
      this.__lastTrackedViews = {};
      _.each(this.getTrackedViews(), function(view) {
        if (view.isAttachedToParent() && view.injectionSite) {
          parentView.__injectionSiteMap[view.injectionSite.attr('inject')] = view;
        }
        parentView.__lastTrackedViews[view.cid] = view;
      });
    },

    /**
     * Replaces the injection site element passed in using $el.replaceWith OR you can use your own replace method
     * @param {external:jQuery} $el the injection site element to be replaced
     * @param {Object} [options] Optional options
     * @param   {Function} [options.replaceMethod] use an alternative replace method. Invoked with the view's element as the argument.
     * @param   {boolean} [options.discardInjectionSite=false] if true, the view will not save a reference to the injection site after replacement.
     * @private
     */
    __replaceInjectionSite: function($el, options) {
      options = options || {};
      this.injectionSite = options.replaceMethod ? options.replaceMethod(this.$el) : $el.replaceWith(this.$el);
      if (options.discardInjectionSite) {
        this.injectionSite = undefined;
      }
    },

    /**
     * Call this method when a view is attached to the DOM. It is recursive to child views, but checks whether each child view is attached.
     * @private
     */
    __invokeAttached: function() {
      // Need to check if each view is attached because there is no guarentee that if parent is attached, child is attached.
      if (!this.__attachedCallbackInvoked) {
        this.trigger('before-attached-callback');
        this._attached();
        this.__attachedCallbackInvoked = true;
        _.each(this.getTrackedViews(), function(view) {
          if (view.isAttachedToParent()) {
            view.__invokeAttached();
          }
        });
      }
    },

    /**
     * Call this method when a view is detached from the DOM. It is recursive to child views.
     * @private
     */
    __invokeDetached: function() {
      if (this.__attachedCallbackInvoked) {
        this.trigger('before-detached-callback');
        this._detached();
        this.__attachedCallbackInvoked = false;
      }
      _.each(this.getTrackedViews(), function(view) {
        // If the tracked view is currently attached to the parent, then invoke detatched on it.
        if (view.isAttachedToParent()) {
          view.__invokeDetached();
        }
      });
    },

    /**
     * Generates callbacks for changes in feedback cell fields
     * 'change fullName' -> invokes all the jQuery (or $) methods on the element as stored by the feedback cell
     * If feedbackCell.get('fullName') returns:
     * { text: 'my text',
     *   attr: {class: 'newClass'}
     *   hide: [100, function() {...}]
     * ...}
     * Then it will invoke $element.text('my text'), $element.attr({class: 'newClass'}), etc.
     * @private
     */
    __generateFeedbackCellCallbacks: function() {
      var self = this;
      // Feedback one-way bindings
      self.feedbackCell.off();
      _.each(this.$('[data-feedback]'), function(element) {
        var attr = $(element).data('feedback');
        self.feedbackCell.on('change:' + attr, (function(field) {
          return function() {
            var $element,
              state = self.feedbackCell.get(field);
            if (!state) {
              return;
            }
            $element = self.$el.find('[data-feedback="' + field + '"]');
            _.each(state, function(value, key) {
              var target;
              if (_.first(key) === '_') {
                target = self[key.slice(1)];
              } else {
                target = $element[key];
              }
              if (_.isArray(value)) {
                target.apply($element, value);
              } else if (value !== undefined) {
                target.call($element, value);
              }
            });
          };
        })(attr));
      });
      _.each(self.feedbackCell.attributes, function(value, attr) {
        self.feedbackCell.trigger('change:' + attr);
      });
    },

    /**
     * Processes the result of the then method. Adds to the feedback cell.
     * @param result the result of the then method
     * @param feedbackCellField the name of the feedbackCellField, typically the "to" value.
     * @private
     */
    __processFeedbackThenResult: function(result, feedbackCellField) {
      var newState = $.extend({}, result);
      this.feedbackCell.set(feedbackCellField, newState, {silent: true});
      this.feedbackCell.trigger('change:' + feedbackCellField);
    },

    /**
     * Creates the "when" bindings, and collates and invokes the "then" methods for all feedbacks
     * Finds all feedback zones that match the "to" field, and binds the "when" events to invoke the "then" method
     * @private
     */
    __generateFeedbackBindings: function() {
      var i,
          self = this;

      // Cleanup previous "on" and "listenTo" events
      for (i = 0; i < this.__feedbackOnEvents.length; i++) {
        this.off(null, this.__feedbackOnEvents[i]);
      }
      for (i = 0; i < this.__feedbackListenToEvents.length; i++) {
        var feedbackListenToConfig = this.__feedbackListenToEvents[i];
        this.stopListening(feedbackListenToConfig.obj, feedbackListenToConfig.name, feedbackListenToConfig.callback);
      }
      this.__feedbackOnEvents = [];
      this.__feedbackListenToEvents = [];

      // For each feedback configuration
      _.each(this.feedback, function(declaration) {
        var toEntries = [declaration.to];
        if (_.isArray(declaration.to)) {
          toEntries = declaration.to;
        }
        _.each(toEntries, function(to) {
          var destinations = self.__getFeedbackDestinations(to),
            destIndexTokens = self.__getAllIndexTokens(to);

          // Iterate over all destinations
          _.each(destinations, function(dest) {
            var fieldName, indices, indexMap, then, args, method, whenEvents, bindInfo;
            dest = $(dest);
            fieldName = dest.data('feedback');
            indices = self.__getAllIndexTokens(fieldName);
            indexMap = {};
            // Generates a mapping from variable name to value:
            // If the destination "to" mapping is: my-feedback-element[x][y] and this particular destination is: my-feedback-element[1][4]
            // then the map would look like: {x: 1, y: 4}
            _.each(destIndexTokens, function(indexToken, i) {
              indexMap[indexToken] = indices[i];
            });
            then = declaration.then;

            // If the "then" clause is a string, assume it's a view method
            if (_.isString(then)) {
              then = self[then];
            } else if (_.isArray(then)) {
              // If the "then" clause is an array, assume it's [viewMethod, arg[0], arg[1], ...]
              args = then.slice();
              method = args[0];
              args.shift();
              then = self[method].apply(self, args);
            }

            // track the indices for binding
            bindInfo = {
              feedbackCellField: fieldName,
              fn: then,
              indices: indexMap
            };
            // Iterate over all "when" clauses
            whenEvents = self.__generateWhenEvents(declaration.when, indexMap);
            _.each(whenEvents, function(eventKey) {
              var match, delegateEventSplitter,
                invokeThen = function(evt) {
                  var i, args, result, newState;
                  args = [evt];
                  newState = {};
                  args.push(bindInfo.indices);
                  result = bindInfo.fn.apply(self, args);
                  self.__processFeedbackThenResult(result, bindInfo.feedbackCellField);
                };
              delegateEventSplitter = /^(\S+)\s*(.*)$/;
              match = eventKey.match(delegateEventSplitter);
              self.$el.on(match[1] + '.delegateEvents' + self.cid, match[2], _.bind(invokeThen, self));
            });
            // Special "on" listeners
            _.each(declaration.when.on, function(eventKey) {
              var invokeThen = self.__generateThenCallback(bindInfo, eventKey);
              self.on(eventKey, invokeThen, self);
              self.__feedbackOnEvents.push(invokeThen);
            });
            // Special "listenTo" listeners
            _.each(declaration.when.listenTo, function(listenToConfig) {
              var obj = listenToConfig.object;
              if (_.isFunction(obj)) {
                obj = _.bind(listenToConfig.object, self)();
              } else if (_.isString(obj)) {
                obj = _.result(self, listenToConfig.object);
              }
              if (obj) {
                var invokeThen = _.bind(self.__generateThenCallback(bindInfo, listenToConfig.events), self);
                self.listenTo(obj, listenToConfig.events, invokeThen);
                self.__feedbackListenToEvents.push({
                  object: obj,
                  name: listenToConfig.events,
                  callback: invokeThen
                });
              }
            });
          });
        });
      });
    },


    /**
     * Returns a properly wrapped "then" using a configuration object "bindInfo" and an "eventKey" that will be passed as the type
     * @param bindInfo
     * @param   bindInfo.feedbackCellField the property name of the feedback cell to store the "then" instructions
     * @param   bindInfo.fn the original "then" function
     * @param   [bindInfo.indices] the index map
     * @return {Function} the properly wrapped "then" function
     * @private
     */
    __generateThenCallback: function(bindInfo, eventKey) {
      return function() {
        var result,
            args = [{
              args: arguments,
              type: eventKey
            }];
        args.push(bindInfo.indices);
        result = bindInfo.fn.apply(this, args);
        this.__processFeedbackThenResult(result, bindInfo.feedbackCellField);
      };
    },

    /**
     * Returns all elements on the page that match the feedback mapping
     * If dest is: my-feedback-foo[x][y] then it will find all elements that match: data-feedback="my-feedback-foo[*][*]"
     * @param {string} dest the string of the data-feedback
     * @return {external:jQuery} all elements on the page that match the feedback mapping
     * @private
     */
    __getFeedbackDestinations: function(dest) {
      var self = this,
          strippedField = this.__stripAllAttribute(dest),
          destPrefix = dest,
          firstArrayIndex = dest.indexOf('[');
      if (firstArrayIndex > 0) {
        destPrefix = dest.substring(0, firstArrayIndex);
      }
      // Tries to match as much as possible by using a prefix (the string before the array notation)
      return this.$('[data-feedback^="' + destPrefix + '"]').filter(function() {
        // Only take the elements that actually match after the array notation is converted to open notation ([x] -> [])
        return self.__stripAllAttribute($(this).data('feedback')) === strippedField;
      });
    },

    /**
     * Generates the events needed to listen to the feedback's when methods. A when event is only created
     * if the appropriate element exist on the page
     * @param whenMap the collection of "when"'s for a given feedback
     * @param indexMap map from variable names to values when substituting array notation
     * @return the events that were generated
     * @private
     */
    __generateWhenEvents: function(whenMap, indexMap) {
      var self = this,
          events = [];
      _.each(whenMap, function(whenEvents, whenField) {
        var substitutedWhenField,
            qualifiedFields = [whenField],
            useAtNotation = (whenField.charAt(0) === '@');

        if (whenField !== 'on' && whenField !== 'listenTo') {
          if (useAtNotation) {
            whenField = whenField.substring(1);
            // substitute indices in to "when" placeholders
            // [] -> to all, [0] -> to specific, [x] -> [x's value]
            substitutedWhenField = self.__substituteIndicesUsingMap(whenField, indexMap);
            qualifiedFields = _.flatten(self.__generateSubAttributes(substitutedWhenField, self.model));
          }
          // For each qualified field
          _.each(qualifiedFields, function(qualifiedField) {
            _.each(whenEvents, function(eventType) {
              var backboneEvent = eventType + ' ' + qualifiedField;
              if (useAtNotation) {
                backboneEvent = eventType + ' [data-model="' + qualifiedField + '"]';
              }
              events.push(backboneEvent);
            });
          });
        }
      });
      return events;
    },

    /**
     * Returns an array of all the values and variables used within the array notations in a string
     * Example: foo.bar[x].baz[0][1].taz[y] will return ['x', 0, 1, 'y']. It will parse integers if they are numbers
     * This does not handle or return any "open" array notations: []
     * @private
     */
    __getAllIndexTokens: function(attr) {
      return _.reduce(attr.match(/\[.+?\]/g), function(result, arrayNotation) {
        var token = arrayNotation.substring(1, arrayNotation.length - 1);
        if (!isNaN(token)) {
          result.push(parseInt(token, 10));
        } else {
          result.push(token);
        }
        return result;
      }, []);
    },

    /**
     * Replaces all array notations with open array notations.
     * Example: foo.bar[x].baz[0][1].taz[y] will return as foo.bar[].baz[][].taz[]
     * @private
     */
    __stripAllAttribute: function(attr) {
      attr = attr.replace(/\[.+?\]/g, function() {
        return '[]';
      });
      return attr;
    },

    /**
     * Takes a map from variable name to value to be replaced and processes a string with them.
     * Example: foo.bar[x].baz[0][1].taz[y] and {x: 5, y: 9} will return as foo.bar[5].baz[0][1].taz[9]
     * Also supports objects:
     * Example: foo.bar and {bar: someString} will return as foo.someString
     * @private
     */
    __substituteIndicesUsingMap : function(dest, indexMap) {
      var newIndex;
      return dest.replace(/\[[^\]]*\]/g, function(arrayNotation) {
        if (arrayNotation.match(/\[\d+\]/g) || arrayNotation.match(/\[\]/g)) {
          return arrayNotation;
        } else {
          newIndex = indexMap[arrayNotation.substring(1, arrayNotation.length - 1)];
          if (_.isString(newIndex)) {
            return '.' + newIndex;
          } else {
            return '[' + (newIndex === undefined ? '' : newIndex) + ']';
          }
        }
      });
    },

    /**
     * Generates an array of all the possible field accessors and their indices when using
     * the "open" array notation:
     *    foo[] -> ['foo[0]', 'foo[1]'].
     * Will also perform nested arrays:
     *    foo[][] -> ['foo[0][0]', foo[1][0]']
     * Supports both foo[x] and foo.bar
     * @private
     * @param {string} attr The name of the attribute to expand according to the bound model
     * @return {Array<string>} The fully expanded subattribute names
     */
    __generateSubAttributes: function(attr, model) {
      var firstBracket = attr.indexOf('[]');
      if (firstBracket === -1) {
        return [attr];
      } else {
        var attrName = attr.substring(0, firstBracket);
        var remainder = attr.substring(firstBracket + 2);
        var subAttrs = [];
        var values = model.get(attrName);
        if (!values) {
          return [attr];
        }
        var indexes;
        if (_.isArray(values)) {
          indexes = _.range(values.length);
        } else {
          indexes = _.keys(values);
        }
        _.each(indexes, function(index) {
          var indexToken = '[' + index + ']';
          if (_.isString(index)) {
            indexToken = '.' + index;
          }
          subAttrs.push(this.__generateSubAttributes(attrName + indexToken + remainder, model));
        }, this);
        return subAttrs;
      }
    }

    //************** End Private methods **************//
  });

  return View;
}));