lateralus.component.view.js

import $ from 'jquery';
import _ from 'lodash-compat';
import Backbone from 'backbone';
import Mustache from 'mustache';
import mixins from './lateralus.mixins';

const fn = {};

/**
 * The DOM template to be used with this View.
 * @type {string|null}
 * @member Lateralus.Component.View#template
 * @default {null}
 */
fn.template = null;

// jshint maxlen:100
/**
 * The constructor for this class should not be called by application code,
 * it is used by the `{@link Lateralus.Component}`
 * constructor.
 * @private
 * @param {Lateralus} lateralus
 * @param {Lateralus.Component} component
 * @param {Object} [options] Gets passed to
 * [Backbone.View#initialize](http://backbonejs.org/#Collection-constructor).
 * @param {Lateralus.Component.View} [opt_parentView]
 * @mixes Lateralus.mixins
 * @constructs Lateralus.Component.View
 */
fn.constructor = function (lateralus, component, options, opt_parentView) {
  /**
   * A reference to the central {@link Lateralus} instance.
   * @member Lateralus.Component.View#lateralus
   * @type {Lateralus}
   * @final
   */
  this.lateralus = lateralus;

  /**
   * If this is a subview of another `{@link Lateralus.Component.View}`, this
   * property is a reference to the parent `{@link Lateralus.Component.View}`.
   * @property parentView
   * @type {Lateralus.Component.View|null}
   * @default null
   */
  this.parentView = opt_parentView || null;

  /**
   * A reference to the `{@link Lateralus.Component}` to which this `{@link
   * Lateralus.Component.View}` belongs.
   * @member Lateralus.Component.View#component
   * @type {Lateralus.Component}
   * @final
   */
  this.component = component;

  if (options.model) {
    // Attach the model a bit early here so that the modelEvents map is
    // properly bound in the delegateLateralusEvents call below.
    this.model = options.model;
  }

  this.delegateLateralusEvents();
  Backbone.View.call(this, options);
};

/**
 * This is called when a `{@link Lateralus.Component.View}` is initialized, it
 * is not called directly.
 *
 * `{@link Lateralus.Component.View}` subclasses that
 * override `initialize` must call this base method:
 *
 *     const Base = Lateralus.Component.View;
 *     const baseProto = Base.prototype;
 *
 *     const ExtendedComponentView = Base.extend({
 *       initialize: function () {
 *         baseProto.initialize.apply(this, arguments);
 *         // Other logic...
 *       }
 *     });
 * @method Lateralus.Component.View#initialize
 * @param {Object} [opts] Any properties or methods to attach to this
 * `{@link Lateralus.Component.View}` instance.
 */
fn.initialize = function (opts) {
  // this.toString references the central Component constructor, so don't
  // attach the class for it here.
  if (!this.parentView) {
    this.$el.addClass(this.toString());
  }

  /**
   * The CSS class names specified by this property will be attached to `$el`
   * when this `{@link Lateralus.Component.View}` is
   * initialized.
   * @property className
   * @type {string|undefined}
   * @default undefined
   */
  if (this.className) {
    this.$el.addClass(this.className);
  }

  _.extend(this, _.defaults(_.clone(opts), this.attachDefaultOptions));
  this.renderTemplate();

  /**
   * A function to be called in the next JavaScript thread.  This can be
   * necessary for situations where setup logic needs to happen after a View
   * has been rendered.
   *
   * In other words, `{@link Lateralus.Component.View#initialize}` runs before
   * the View has been rendered to the DOM, and `deferredInitialize` runs
   * immediately after it has been rendered.
   * @method Lateralus.Component.View#deferredInitialize
   */
  if (this.deferredInitialize) {
    _.defer(_.bind(this.deferredInitialize, this));
  }
};

/**
 * Meant to be overridden in subclasses.  With `attachDefaultOptions`, you
 * can provide an object of default parameters that should be attached to the
 * View instance when the base `initialize` method is called.  These values
 * can be overridden by the [`options` values that are provided to the View
 * constructor](http://backbonejs.org/#View-constructor).
 * @property attachDefaultOptions
 * @type {Object}
 */
fn.attachDefaultOptions = {};

/**
 * Adds a subview.  Subviews are lighter than subcomponents.  It is
 * preferable to use a subview rather than a subcomponent when there is clear
 * interdependency between two Views.  This pattern is useful when you want
 * to keep display logic well-organized into several Views, but have it
 * compartmentalized within a single component.
 * @method Lateralus.Component.View#addSubview
 * @param {Lateralus.Component.View} Subview A constructor, not an instance.
 * @param {Object} [subviewOptions] Backbone.View [constructor
 * options](http://backbonejs.org/#View-constructor) to pass along to the
 * subview when it is instantiated.
 * @return {Lateralus.Component.View} The instantiated subview.
 */
fn.addSubview = function (Subview, subviewOptions) {
  if (!this.subviews) {
    /**
     * The subviews of this object.  Do not modify this property directly, it
     * is managed by Lateralus.
     * @property subviews
     * @type {Array(Lateralus.Component.View)}
     */
    this.subviews = [];
  }

  const subview = new Subview(
    this.lateralus,
    this.component,
    subviewOptions,
    this
  );

  this.subviews.push(subview);

  return subview;
};

/**
 * This method returns the object whose properties are used as render
 * variables in `{@link Lateralus.Component.View#renderTemplate}`.  The method
 * can be overridden.
 * @method Lateralus.Component.View#getTemplateRenderData
 * @return {Object} The [raw `Backbone.Model`
 * data](http://backbonejs.org/#Model-toJSON), if this View has a Model.
 * Otherwise, an empty object is returned.
 */
fn.getTemplateRenderData = function () {
  const renderData = {};

  if (this.model) {
    _.extend(renderData, this.model.toJSON());
  }

  _.extend(renderData, this.lateralus.globalRenderData);

  return renderData;
};

fn.getTemplatePartials = function () {
  /**
   * An optional map of template partials to be passed to the
   * `Mustache.render` call for this View.
   *
   *     Lateralus.Component.View.extend({
   *       templatePartials: {
   *         myNamePartial: 'Hello my name is {{name}}.'
   *       }
   *     });
   *
   * @property templatePartials
   * @type {Object<String>|undefined}
   * @default undefined
   */
  return _.extend(this.templatePartials || {}, this.lateralus.globalPartials);
};

/**
 * Meant to be called by `{@link Lateralus.Component.View#initialize}` and
 * infrequently thereafter, this method empties out
 * [`$el`](http://backbonejs.org/#View-$el) and does a full re-render.
 * [`render`](http://backbonejs.org/#View-render) should only be used for
 * partial renders.
 * @method Lateralus.Component.View#renderTemplate
 */
fn.renderTemplate = function () {
  if (!this.template) {
    return;
  }

  this.$el.children().remove();
  this.$el.html(
    Mustache.render(
      this.template,
      this.getTemplateRenderData(),
      this.getTemplatePartials()
    )
  );

  this.bindToDOM();
};

/**
 * Look for any DOM elements within [`$el`](http://backbonejs.org/#View-$el)
 * that have a class that looks like _`$this`_ and create a property on this
 * instance with the same name.  The attached property is a jQuery object
 * that references the corresponding DOM element.
 * @method Lateralus.Component.View#bindToDOM
 * @private
 */
fn.bindToDOM = function () {
  this.$el.find('[class^="$"]').each((i, el) => {
    const $el = $(el);
    this[$el.attr('class').split(/\s+/)[0]] = $el;
  });
};

/**
 * Remove this `{@link Lateralus.Component.View}` from
 * the DOM and cleanly dispose of any references.
 * @method Lateralus.Component.View#dispose
 * @chainable
 */
fn.dispose = function () {
  this.remove();

  const parentView = this.parentView;
  if (parentView) {
    parentView.subviews = _.without(parentView.subviews, this);
  }

  return this;
};

_.extend(fn, mixins);

/**
 * This class builds on the ideas and APIs of
 * [`Backbone.View`](http://backbonejs.org/#View).
 * @class Lateralus.Component.View
 * @extends {Backbone.View}
 */
const ComponentView = Backbone.View.extend(fn);

/**
 * @method Lateralus.Component.View#toString
 * @return {string} The name of this View.  This is used internally by
 * Lateralus.
 */
ComponentView.prototype.toString = function () {
  return this.component.toString() + '-view';
};

export default ComponentView;