Source: backbone-x/source/view.js

/*jshint unused:false, bitwise:false */

(function () {
  'use strict';

  /**
   * MVP-E.
   * @author travis@xtuple.com
   *
   * Separate the concerns of the presenter from those of the view.
   */
  XM.View = Backbone.View.extend();

  /**
   * @static
   *
   * Presenters call this method to bind to a View. The Presenter must have a
   * property called 'view' that defines the name of the View to bind to.
   * @example
   * enyo.kind({
   *    name: 'XV.MyWorkspace',
   *    kind: 'XV.Workspace',
   *    view: 'XM.MyWorkspaceView'
   *    ...
   * });
   */
  XM.View.setPresenter = function (presenter, type) {
    if (!presenter.view) {
      //console.warn('No view defined for presenter: ' + presenter.kind + '#' + type);
      return;
    }
    return new XM[presenter.view.suffix()]({
      presenter: presenter,
      type: type
    });
  };

  XM.EnyoView = XM.View.extend(/** @lends XM.EnyoView# */{

    item: {
      /**
       * Layout DSL for List Item.
       *
       * @desc
       * A matrix that represents the template of the list item attributes.
       */
      template: { /** @example
        [
          // COLUMN 1          COLUMN 2
          [{ attr: 'name' }, { attr: 'description' }],  // ROW 1
          [{ attr: 'notes', colspan: 2             }]   // ROW 2
        ]
      **/
      },

      /**
       * Decorations DSL for List Item. Used by Decorators; feel free to
       * override or extend. If empty, the default is used as defined by the
       * optional Decorator which wraps this List Item, if present.
       *
       * @desc
       * Each key represents a style directive, which maps to a list of model
       * attributes, on each of which we apply the style if it exists in the
       * model.
       */
      decorations: { /** @example
        XXX TODO
        active:   [ 'isActive' ],
        italic:   [ 'name' ],
        hyperlink:[ 'primaryEmail', 'phone', 'webAddress' ],
      **/
      },

    },

    list: {
      /**
       * List which actions this list supports. 'method' and 'prerequisite'
       * are invoked on the model by default, unless 'isViewMethod' is set to
       * true.
       */
      actions: [ /** @example
        {name: 'deactivate'},
        {prerequisite: 'canDeactivate'},
        {method: 'deactivate'}
      **/
      ],

      /**
       * Describe the query used to populate/sort the list.
       */
      query: { /** @example
        orderBy: [
          {attribute: 'isActive', descending: true},
          {attribute: 'name}
        ]
      **/
      }
    },

    workspace: {

      template: {

      }

    },

    /**
     * @summary Map model events to Enyo events.
     * @desc The events block is employed slightly differently and more powerfully
     * in our XM.EnyoView than in a traditional Backbone.View.
     *
     * @example
     *  XM.FooView = XM.View.extends({
     *    events: {
     *      'change:bar': 'handleBarChange'
     *    }
     *  })
     *  enyo.kind({
     *    name: 'XV.FooWidget',
     *    handlers: {
     *      'onBarChange' : 'handleBarChange'
     *    },
     *    handleBarChange: function (inSender, inEvent) {
     *      ...
     *    }
     *  })
     *  // this will invoke presenter.handleBarChange
     *  model.set({ bar: 'MyBar' });
     */
    events: {/**
      @example
      'prefix:modelEvent': 'onPresenterEvent',
      'lock:acquire': 'onLockAcquire'
    */
    },

    /**
     * This view prefers no knowledge of any jquery-ness, and since
     * implemented an MVP-like design, will refer to its 'el' as 'presenter'
     * instead.
     */
    $:   undefined,
    $el: undefined,
    el:  undefined,

    /**
     * Initialize this View by binding bi-directionally to the Presenter. The
     * Presenter should rarely/never invoke any view methods or properties
     * directly.
     *
     *
     * @param {enyo.Component}
     */
    initialize: function (properties) {
      this.presenter = properties.presenter;
      this.type = properties.type;
      this.presenter.viewInstance = this;
      this.model = this.presenter.value;

      this._mixin();
    },

    /**
     * @override
     */
    render: function () {
      this.presenter.render();
      return this;
    },

    /**
     * @override
     */
    remove: function () {
      this.presenter.destroy();
      return this;
    },

    /**
     * @override
     * @summary Intercept events traveling between the enyo layer and the
     * model layer.
     *
     * @desc
     * The default implementation is logically congruent with this method, but
     * it is tailored for views built using jQuery. Specifically, it assumes our
     * UI components sport an .on() method, which they do not, and it is less
     * intrusive and truer to Backbone's design philosophy to override this
     * method than to sully the enyo event handler naming convention.
     *
     * @see http://backbonejs.org/docs/backbone.html#section-127
     * @see Backbone.Model#listenTo
     */
    delegateEvents: function () {
      if (!this.model) {
        return;
      }

      var that = this;

      _.each(this.events, function (presenterEvent, modelEvent) {

        // catch the event from the model
        that.listenTo(that.model, modelEvent, function (originator, params, options) {
          if (!that.presenter.value) {
            return;
          }

          // re-package and send the event to the enyo component
          that.presenter.bubble(presenterEvent, {
            model: originator,
            params: params,
            options: options
          });
        });
      });
    },

    /**
     * @override
     * @summary Alter semantics slightly to make this API believable in the
     * context of enyo.
     *
     * @param {enyo.Component}  enyo component to bind
     * @param {Boolean} verb form; whether to delegate events to the element
     *
     * @desc I diminish fidelity to the Backbone API on 'element' parameter;
     * for our purposes, it makes more sense for 'element' to be an enyo
     * component, and not a DOM element. This is because enyo generates a
     * DOM element, whereas jQuery wraps an existing one.
     */
    setElement: function (element, delegate) {
      this.presenter = element;
    },

    /**
     * @override
     * Re-implemented for Enyo.
     */
    _ensureElement: function () {
      return !!this.presenter;
    },

    /**
     * @override
     * Unbind presenter event handlers.
     */
    undelegateEvents: function () {
      _(this.events).each(function (presenterEvent, modelEvent) {
        this.stopListening(this[this.backing], modelEvent);
      });
    },

    /**
     * Mix some additional binding functionality into the Presenter, and
     * populate any published properties defined statically in the View.
     *
     * @param {String}  the type of Presenter
     */
    _mixin: function () {
      var view = this,
        presenter = view.presenter,
        _valueChanged = presenter.valueChanged;

      /**
       * Populate the Presenter with any statically-defined published fields
       * from the View. Bascally, a less forceful version of 'extend()'
       */
      _.each(view[view.type], function (value, key) {
        if (_.isObject(presenter[key]) && _.isObject(value)) {
          _.extend(presenter[key], value);
        }
        else if (_.isArray(presenter[key]) && _.isArray(value)) {
          _.union(presenter[key], value);
        }
        else {
          if (key === 'model') {
            // XXX can't setProperty on 'model' because we've broken the enyo API
            // by wrongly defining our own getFoo methods
            presenter[key] = value;
          }
          else {
            presenter.setProperty(key, value);
          }
        }
      });

      /**
        * @override
        * We want to know when the Presenter's backing model is changed
        * so we can bind to its events. The existing XV.Workspace, XV.List,
        * and XV.ListItem components all refer to the backing model using a
        * property called 'value'.
        */
      presenter.valueChanged = function () {
        view.model = presenter.value;
        view.delegateEvents();
        if (_.isFunction(_valueChanged)) {
          _valueChanged.call(presenter);
        }
      };
    }
  });
})();