// Emerson View // // A view... (function(ns) { // Emerson Extension // -------------------------------------------------------------------------- // ### Emerson.view module // Entry point for defining a new View. var view = ns.view = function(name, setup) { return (views[name] = construct(name, setup || {})); }; // ### Emerson.trait module // Entry point for defining a new "trait". // // A trait can be thought of as a set of related behaviors and is essentially // a special form of view. // // Traits have the additional capability of responding to an optional "mode" // at intialization. var trait = ns.trait = function(name, setup) { return (traits[name] = construct(name, setup || {})); }; // ### Module API // * `ns` is a reference to the namespace. // * `init` is a hook for initializing the module. _.extend(view, { ns : ns, init : function init() { configure(); $('body').view(); } }); // "Base" Libary Extension // -------------------------------------------------------------------------- // Make a local copy of Emerson.base. e.g., one of jQuery, Zepto or Ender. var $ = ns.base; // ### $.view // // $.view(key); // // Accessor for defined Views. $.view = function(key) { return views[key]; }; // ### $.trait // // $.trait(key); // // Accessor for defined Traits. See `$.view`. $.trait = function(key) { return traits[key]; }; // ### $.fn.view // // $(target).view() // // Initializes a transiently-decorated object. In the call to #initialized, // and any additional methods called from there, `this` will be wrapped with // the View definition. The returned object is stock: no longer decorated. // // This method will apply, both, "view" and "trait" behaviors. Additionally, // it works on the surrounding DOM match, as well as nested matches. $.fn.view = function() { _.each(this, function(e) { var keys = []; var element = $(e); var as_view = element.add(element.find(selectors.view)).filter(selectors.view); var as_trait = element.add(element.find(selectors.traits)).filter(selectors.traits); _.each(as_view, function(html) { var element = $(html); // why jQuery here, if $sub() in a moment? attach.call(element, views, [element.data(attrs.view)]); }); _.each(as_trait, function(html) { var element = $(html); // why jQuery here, if $sub() in a moment? attach.call(element, traits, element.data(attrs.traits).split(/\s+/), true); }); }); return this; }; // Internal Objects // -------------------------------------------------------------------------- // ### View constructor. // View instances are "subclasses" of the base lib object, decorated with our // View.prototype and the provided definition. function View() {} // ### View setup definition. _.extend(View, { setup : { initialize : function() {}, connected : false, subscribe : {} } }); // ### View prototype definition. // **TODO**: Add specs for #connect & #outlet. // // * `connect` specifies which, if any, `outlets` should be registered as // methods on the instance. // // false - do not connect any outlets (default) // true - connect all outlets // [outlet(s)] - connect the named outlets. // // * `outlet` returns descendent element(s) with matching a `data-outlet` // attribute. This may be used for a given outlet whether or not it has // been "connected". Outlets are useful as a means of allowing for View // and Trait definitions with somewhat flexible DOM. Rather than using // specific selectors (or polluting the `class` attribute), the DOM for // a given instance indicates which nodes should match for the View. // // thanks to Nathan Sobo for the idea: https://github.com/nathansobo/space-pen _.extend(View.prototype, { connect : function(config) { var self = this; var outlets = this.find('[data-outlet]'); if(config === true) { _.each(outlets, function(subject) { var outlet = $(subject); var key = outlet.data('outlet'); self[outlet.data('outlet')] = function() { return self.outlet(key); }; }); } else if($.isArray(config)) { _.each(config, function(key) { var outlet = self.find('[data-outlet="' + key + '"]'); if(outlet.length) { self[key] = function() { return self.outlet(key); }; } }); } return this; }, outlet : function(key) { return this.find('[data-outlet="' + key + '"]'); } }); // Internal Implementation // -------------------------------------------------------------------------- // Attr definitions. // @private var attrs = {}; // Selector definitions. // @private var selectors = {}; // Storage place for the defined Views. // @private var views = {}; // Storage place for the defined Traits. // @private var traits = {}; // Storage place for attachments made. // @private var attachments = {}; // emerson id, for tracking attachments. // @private var _eid = 0; // configure // @private function configure() { var config = ns.config('attrs'); _.extend(attrs, config); _.each(config, function(value, key) { selectors[key] = '[data-' + value + ']'; }); } // ### eid // Retrieves a unique and persistent ID for the given DOM element. // @private function eid(element) { return element._emerson || (element._emerson = (_eid += 1)); } // ### construct // Construct a definition made up of: // // 1. The provided setup block. // 2. A "subclass", extended with the View prototype. function construct(name, setup) { var sub = _.extend($sub(), { constructor : View, setup : _.extend({ initialize : function() {}, connected : false, subscribe : {}, }, setup), // experimental... allows for easier spec'ing of a view/trait build : function build(element, mode) { return instance(this, element, mode); } }); _.extend(sub.fn, View.prototype); return sub; } // ### instance // experimental (see `build` definition in `construct`). function instance(def, element, mode) { var setup = def.setup; var built = def(element, element.context); built.connect(setup.connected); _.each(setup.subscribe, function(handler, key) { bind(built, key, handler); }); setup.initialize.call(built, mode); return built; } // ### attach // Given a (complex) list of keys, search the libraries for applicable View // and Trait definitions to apply to the object. // // Keeps track of which definitions have been applied, and does not re-apply. // // NOTE: `this`, in this call, should be a baselib-extended object containing // a single element. e.g., // // _.each($(selector), function(element) { // attach.apply($(element), views, [key, [subkey]]); // }); // // The `mode_p` predicate argument indicates whether the view type may accept // a "mode", which should be true for traits (not for views). function attach(library, keys, mode_p) { var self = this, def; var id = eid(this[0]); _.each(_.flatten(keys), function(key) { var mode, match, built, init, events, set, setup; if(mode_p && (match = /^([^(]+)\((.+)\)/.exec(key))) { key = match[1]; mode = match[2]; } set = (attachments[key] || (attachments[key] = [])); if(_.include(set, id)) { return; // do not re-apply. } // Build an instance, connect outlets, bind events, init and record. if(def = library[key]) { // experimental... instance(def, self, mode); // setup = def.setup; // built = def(self, self.context); // built.connect(setup.connected); // // _.each(setup.subscribe, function(handler, key) { // bind(built, key, handler); // }); // // setup.initialize.call(built, mode); set.push(id); } }); return this; } // ### bind // Attach event handler(s). // // Emerson.view(key, { // subscribe : { // 'click' : handler, // simple // 'click focus' : handler, // multiple event types // 'selector' : { // specific child target // 'click' : handler, // 'focus' : handler // }, // document : { // bind document, for events // 'click' : handler // fired outside of the view // 'selector' : { // 'click' : handler // } // }, // window : { // bind window, for events // 'scroll' : handler // fired outside of the view // }, // 'outlet:name' : { // a custom key to specify a // 'click' : handler // defined outlet as the scope // } // } // }); // // Emerson event handling differs from that of, say, stock jQuery in that // `this` within the context of the handler will be a view instance. The // event argument is unadultered, allowing access to the full set of targets // as defined by the baselib (e.g., jQuery). // // **Special notes regarding document-/window-bound handlers**: // // Binding an event like `click` (without selector scope) to document/window // would likely be a bad idea. A more useful (and less costly) use case // would be a sort of pub/sub. For example, view "A" could trigger an event // indicating that it has rendered a new instance, to which "B" (elsewhere) // would listen in order to update, say, a count of instances of "A". // // Additionally, event handlers bound to the document will not be cleaned // up when the associated view instance is removed. function bind(instance, key, handler, selector, binder) { if($.isPlainObject(handler)) { if(key === 'document') { _.each(handler, function(subhandler, subkey) { bind(instance, subkey, subhandler, undefined, $(document)); }); } else if(key === 'window') { _.each(handler, function(subhandler, subkey) { bind(instance, subkey, subhandler, undefined, $(window)); }); } else { _.each(handler, function(subhandler, subkey) { bind(instance, subkey, subhandler, key, binder); }); } } else { if(/^outlet/.test(selector)) { selector = '[data-outlet="' + selector.split(':')[1] + '"]'; } (binder || instance).on(key, selector, function() { // selector may be undefined return handler.apply(instance, arguments); }); } } // ### $sub // Basically a copy of jQuery.sub, but more generic and with changes to: // // 1. `Sub.extend` // to ensure proper object context is maintained. // 2. `Sub.fn.extend` // to ensure proper object context is maintained. function $sub() { var root; function Sub(selector, context) { return new Sub.fn.init(selector, context); } _.extend(true, Sub, $); Sub.fn = Sub.prototype = $(); Sub.fn.init = function init(selector, context) { return $.fn.init.call(this, selector, context, root); }; Sub.fn.constructor = Sub; Sub.fn.init.prototype = Sub.fn; Sub.fn.extend = function extend() { this.constructor.extend.apply(this.constructor, arguments); return this; }; Sub.extend = function extend() { var self = this; var keep = { constructor : this.fn.constructor, init : this.fn.init }; _.each(arguments, function(arg) { if(arg.fn) { $.extend(self.fn, arg.fn, keep); } else { $.extend(self.fn, arg, keep); } }); return self; }; root = Sub(document); return Sub; } })(Emerson);