Behold

Option name Type Description
root object Where to attach the Behold object.
$ object jQuery lib, or api-compatible replacement (such as Zepto).
_ object underscore/lodash lib.

Copyright Christopher Keefer, 2014.

(function(root, $, _){

Behold

constructor
Behold()

Our container function for the various elements of Behold, and static functions such as extend.

function Behold(){}

extend

method
Behold.extend()

Recreate javascript inheritance (working similarly to backbone extend) to allow our views to be
extendable/inheritable.

Behold.extend = function(onProto, onStatic){
    onProto = onProto || {};
    onStatic = onStatic || {};
    var parent = this,
        child,
        proxy;

For legacy support reasons, if the context (this) is Behold, then we instead recall extend
with the context set to Behold.View, allowing applications to continue using Behold.extend when
they want to extend Behold Views.

if (this === Behold) return Behold.extend.apply(Behold.View, arguments);

child = (onProto && onProto.hasOwnProperty('constructor')) ?
    onProto.constructor : function(){ return parent.apply(this, arguments); };

_.extend(child, parent, onStatic);

proxy = function(){ this.constructor = child; };
proxy.prototype = parent.prototype;
child.prototype = new proxy;

_.extend(child.prototype, onProto);
child.__super__ = parent.prototype;
return child;
    };

View

constructor
Behold.View()

Option name Type Description
options object= Options to be passed to the view. Those with certain names will be set directly on the object for easy access - the others will live in this.options in the view.

Our view function.

Behold.View = function(options){
    this.cid = _.uniqueId('view');
    this.options = options || {};
    this.$ = $;
    this._ = _;

    this._setOptions();
    this._setEl();
    this.bindUI();
    this.attachEvents();
    this.initialize.apply(this, arguments);
};

attachElContent

method
Behold.View.prototype.attachElContent()

Called if we're generating a new DOM structure rather than attaching to an existing structure.
Override this to change the way we attach our generated DOM - by default, we render the template
(if any) and make that the content of this element, and then attach this.el to the specified region.

Behold.View.prototype.attachElContent = function(){
    var content = this.render();
    this.$el.append(content);
    this.region.empty().append(this.$el);
};

render

method
Behold.View.prototype.render()

Renders the content for this view when we're not attaching to an existing DOM.
By default, we rely on a template being defined for the view, and use
underscores template function to render it, passing this.options in. If
no template is defined, we return an empty string.

Behold.View.prototype.render = function(){
    var template = (this.template) ?
        (typeof this.template === 'function') ? this.template :
            _.template($(this.template).html()) : void 0;

    if (template){
        return template(this.options);
    }

    return '';
};

getUISelector

method
Behold.View.prototype.getUISelector() ->string

Option name Type Description
ui object The ui object to look in.
key string The key to search for in the object.

Get the full selector for a given key in the ui.

Behold.View.prototype.getUISelector = function(ui, key){
    var el;

    el = ui[key];
    // If we're using our @ui sugar, keep looping through until we've got the full element string
    while (el.indexOf('@ui.') === 0)
    {
        el = el.split(' ');
        el = ui[el[0].substr(4)]+' '+el.slice(1).join(' ');
    }

    return el;
};

bindUI

method
Behold.View.prototype.bindUI()

Bind this.ui (if set) as a lazy-mapping to the jQuery element references, to replace the css locator string that
previously occupied it. Cache retrieved jQuery element references to return after the first time the ui key has
been referenced. Save the original css strings in case we need to unbind or rebind in _uiBindings.

Behold.View.prototype.bindUI = function(){
    var ui = _.extend({}, this._uiBindings || this.ui),
        uiCache = (this._uiCache = {}),
        $el = this.$el,
        keys;

    if (!ui || Object.getOwnPropertyNames(ui).length === 0) return this;
    if (!this._uiBindings) this._uiBindings = _.extend({}, ui);

    this.ui = {};

    keys = Object.keys(ui);
    keys.forEach(function(key){
        var el = this.getUISelector(ui, key);

        Object.defineProperty(this.ui, key, {
            get:function(){
                if (uiCache[key]){
                    return uiCache[key];
                }
                return (uiCache[key] = $el.find(el));
            }
        });
    }, this);
    return this;
};

unBindUI

method
Behold.View.prototype.unBindUI() ->Behold.View

Unbind all ui element references by deleting the entries and restoring this.ui to the contents of _uiBindings.

Behold.View.prototype.unBindUI = function(){
    var ui = this.ui,
        keys;

    if (!ui || !this._uiBindings) return this;

    keys = Object.keys(ui);
    keys.forEach(function(key){
        delete this.ui[key];
    }, this);

    this.ui = _.extend({}, this._uiBindings);
    delete this._uiBindings;
    return this;
};

attachEvents

method
Behold.View.prototype.attachEvents()

Assign events based on this.events(if set) to map either this.ui element references (prefaced by
"@ui."), or else a bare css locator string. Namespace all events with '.dgbehold' plus the cid of this view.
Delegate events to the root element for efficiency. If the event key is prefaced with the 'capture'
indicator, the > symbol, bind to the specified element using event capturing, allowing us to 'delegate' to
the root element for events that don't bubble.

Behold.View.prototype.attachEvents = function(){
    var that = this,
        ui = this._uiBindings || this.ui,
        events = this.events,
        cid = this.cid,
        $el = this.$el,
        captures = this._capturingListeners = this._capturingListeners || {},
        keys;

    if (events)
    {
        keys = Object.keys(events);
        keys.forEach(function(key){
            var func = events[key],
                sKey = key.split(' '),
                el,
                event;

            func = (typeof func === 'function') ? func : that[func];

            el = (sKey[1].indexOf('@ui.') === 0) ? that.getUISelector(ui, sKey[1].substr(4)) : sKey[1];
            event = sKey[0]+'.dgbehold'+cid;

            if (event[0] === '>'){
                event = sKey[0].substr(1);
                captures[key] = func.bind(that);
                $el[0].addEventListener(event, captures[key], true);
                return;
            }

            $el.on(event, el, func.bind(that));
        });
    }
    return this;
};

detachEvents

method
Behold.View.prototype.detachEvents()

Detach events attached by calling attachEvents.

Behold.View.prototype.detachEvents = function(){
    var that = this,
        $el = this.$el,
        events = this.events,
        cid = this.cid,
        keys = Object.keys(events);

    if (keys.length)
    {
        // Remove all delegated events
        $el.off('.dgbehold'+cid);

        // Remove all capturing events
        keys.filter(function(key){
            return key[0] === '>';
        }).forEach(function(key){
            var func = that._capturingListeners[key],
                event;

            event = key.split(' ')[0].substr(1);
            $el[0].removeEventListener(event, func, true);
        });
        this._capturingListeners = {};
    }
    return this;
};

remove

method
Behold.View.prototype.remove() ->Behold.View

Remove the referenced element from the DOM.

Behold.View.prototype.remove = function(){
    this.detachEvents();
    this.$el.remove();
    return this;
};

setElement

method
Behold.View.prototype.setElement() ->Behold.View

Option name Type Description
element string,HTMLElement,jQuery The element to set as the root of this view.
skipEvents boolean= Whether to skip binding events.
skipUnbinding boolean= Whether to skip unbinding events before binding new events.

Set a new element as the referenced element for this view.

Behold.View.prototype.setElement = function(element, skipEvents, skipUnbinding){
    if (this.$el && !skipUnbinding)
    {
        this.detachEvents();
        this.unBindUI();
    }

    this.$el = (element instanceof $) ? element : $(element);
    this.el = this.$el[0];

    if (!skipEvents)
    {
        this.bindUI();
        this.attachEvents();
    }

    return this;
};

initialize

method
Behold.View.prototype.initialize()

An empty function by default, to be overridden by extending classes.

Behold.View.prototype.initialize = function(){};

extend

property
Behold.View.extend

Make Behold.View extensible.

Behold.View.extend = Behold.extend.bind(Behold.View);

Router

constructor
Behold.Router()

Create a router to attach to a module that has a routes object defined on it.
By default we use the module itself as the controller, but the module can also
define a controller object that can contain the handler functions.

Behold.Router = function(routes, module){
    this.controller = module.controller || module;
    this.routes = routes;
    this.handlers = this._parseRoutes();

    window.addEventListener('popstate', function(event){
        this._onChange(window.location.pathname, event.state || {});
    }.bind(this));
};

// Cached Regexes for Routing
Behold.Router.optionalParam = /\((.*?)\)/g;
Behold.Router.namedParam    = /(\(\?)?:\w+/g;
Behold.Router.splatParam    = /\*\w+/g;
Behold.Router.escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;
Option name Type Description
url string
data object=

Navigate to the specified url, optionally passing the data object to the popstate event
both on navigation, and if we were to press the 'back' button to navigate away.

Behold.Router.navigate = function(url, data){
    var event = new Event('popstate');

    data = data || {};

    window.history.pushState(data, document.title, url);

    event.state = data;
    window.dispatchEvent(event);

    return this;
};

Application

constructor
Behold.Application()

Application container for behold views.

Behold.Application = function(){
    this.modules = {};
};

start

method
Behold.Application.prototype.start()

Call the initialize function on all registered modules when start is called.

Behold.Application.prototype.start = function(){
    var modules = this.modules,
        setupRoutes = [],
        callInit = [],
        module;

    // Iterate through all routes and router setup, before iterating through again to
    // call init, so that routers are ready to handle possible navigation called from
    // initialize functions.
    for (var name in modules)
    {
        if (modules.hasOwnProperty(name)){
            module = modules[name];

            if (module.routes)
            {
                setupRoutes.push(module);
            }

            if (module.initialize && typeof module.initialize === 'function')
            {
                callInit.push(module);
            }
        }
    }

    setupRoutes.forEach(function(module){
        module._router = new Behold.Router(module.routes, module);
    });

    callInit.forEach(function(module){
        module.initialize();
    });
};

module

method
Behold.Application.prototype.module()

Option name Type Description
name string
initializer function=
arguments ... to be passed to the initializer

Register a module, instantiating it and passing it the arguments specified.
Alternatively, if the initializer isn't provided, expect that we want a reference to the module
passed back to us. Will always return an object. Module declaration can be split amongst
multiple files.

Behold.Application.prototype.module = function(name, initializer){
    var module = this.modules[name] = this.modules[name] || {},
        args = [module, this, $, _].concat(Array.prototype.slice.call(arguments, 2));

    if (!initializer) return this.modules[name];

    initializer.apply(module, args);
    return this.modules[name];
};

root.Behold = Behold;
})(window, jQuery, _);