View Source

Subview.js

Views done right!

Subview.js is a minimalistic view framework designed to be highly modular, extensible and efficient for applications containing thousands of views. Subview is an excellent choice for complex user interfaces that are redrawn often such as word processors, messengers and other single page web-apps.

Installation

npm install subview
  • Depends on jQuery or Zepto
  • Plays nicely with CommonJS, RequireJS or plain old script tags

Features

Quick Start

The base for a Subview.js application is the definition of the main view using the subview function. This is the only view that is automatically appended to the <body> of your application. Other views are added to the main view using a template or by direct DOM manipulation.

subview('main', {

    /*** Life-Cycle Methods ***/
    once: function() {
        //Runs the first time a subview is created then never again.
    },
    init: function() {
        //Runs when initializing a subview via the .spawn method
    },
    clean: function() {
        //Runs when a subview is removed to clean and prepare it to be reused
    },

    /*** Templating ***/
    template: Handlebars.compile("\
        Hello World!\
        This  is !\
        }\
    "),
    subviews: {     //Subviews that will be available in the template
        content: SomeSubview
    },
    data: {         //Data available in the template (may also be a function)
        adjective:  "excellent",
        noun:       "framework"
    },

    /*** Extensions ***/
    myExtension: myExtension({

    }),

    /*** My View's API ***/

    // ... Some API Functions
});

Subview Namespace

The core of Subview.js is the definition of reusable subview templates. Calling subview() defines a subview template and returns a SubviewPool object. Subview instances, which can be added to the DOM, are created by calling the SubviewPool.spawn() method or by passing a SubviewPool into a template.

subview(name [string], {definition [object]})

subview('my-subview', {
    init: function() {
        ...
    },
    foo: function() {
        ...
    }
})

Defines a subview template with the given name and the methods and properties provided in the definition. The definition contains methods and properties from the Subview API along with additional methods and properties for the object. These methods and properties will be present on the prototype of the subview instances.

The name may contain letters, numbers, "-"" and "_". In addition, the name will form a CSS class of form .subview-{name} that will be applied to every instance of the subview along with any subviews sub-classed from the subview definition. In addition to the specific named subview classes, the class .subview will be added to every subview DOM element.

subview(element [DOM element or jQuery object])

subview($('.view-main')).foo();

The subview function is overloaded to provide a method for getting the subview instance associated with a given DOM element or jQuery object (element).

subview.lookup(name [string])

var newInstance = subview.lookup('my-subview').spawn();

Returns the SubviewPool object with the given name.

subview.extension(extensionConfig [object])

Used to define a subview extension. See extensions below for more details.

subview.init()

Appends the main subview to the document. This function fires automatically unless the subview.noInit property is set to true.

subview.noInit = true

A boolean property that determines if the main subview is to be immediately appended to the document when ready. This is useful in cases where jQuery/Zepto's ready function does not actually represent a ready document state, such as when using Subview with PhoneGap. Note that the main subview will not be added to the document until subview.init is called.

subview.noInit = true;

onReady(function() {
    subview.init();
});

subview.subviews

An object dictionary containing SubviewPools listed with their names as the keys. Best practice is to use subview.lookup rather than directly referencing subview.subviews.

SubviewPool API

SubviewPools are returned from the subview() function and are responsible for managing subviews, including recycling through pooling.

SubviewPool.spawn( {config [object]} )

var Pool = subview('my-subview'),
    instance = Pool.spawn();

instance.$wrapper.appendTo('body'); //Actually puts the Subview Pool

Creates a Subview instance from the subview template defined by the subview() function. This instance is initialized but not appended to the DOM. You can add a subview instance to the DOM by manipulating Subview.$wrapper or Subview.wrapper properties. Note that an instance returned from SubviewPool.spawn is not necessarily brand new but could have been recycled after being removed using Subview.remove and subsequently cleaned by Subview.clean (See Subview Life-Cycle for details).

The config object is an optional object that is passed to the Subview.once, Subview.init and Subview.clean functions.

Best Practice: In most cases it is unnecessary to use the SubviewPool.spawn method since subviews can easily be inserted using templating. SubviewPool.spawn is instead used when for some reason it is necessary to have a direct reference to a subview instance.

SubviewPool.extend(name [string], {definition [object]})

Extends a subview template and returns a new SubviewPool object. SubviewPool.extend takes the same arguments as the subview function. In most cases, methods and properties present in the definition object will overwrite those of the template being extended. However, life-cycle functions such as once, init and clean will be extended in order to preserve core functionality of the superclass.

var A = subview('A', {
    init: function() {
        //This will be called on initialization for A & B (life-cycle function)
    },
    foo: function() {
        //This will be available for A
    },
    bar: function() {
        //This will be available on A & B
    }
});

var B = SuperClass.extend('B', {
    init: function() {
        //This will be called on initialization for B
    },
    foo: function() {
        //This will overwrite A.foo
    },
    chu: function() {
        //This will only be available on B
    }
});

SubviewPool.Subview

Stores the prototype object for the subview template.

SubviewPool.type

Stores the name of the subview template.

SubviewPool.super

Stores a reference to the super-class of the subview template.

SubviewPool.isSubviewPool = true

Simply used to identify that an object is a SubviewPool.

SubviewPool.destroy()

Destroys a SubviewPool. This is rarely used in applications but can be useful for testing.

Subview API

Life-Cycle Methods

Subview.once({config})
Extends Parent's Method

Fired exactly once when the subview is initialized. config is optionally passed through from the SubviewPool.spawn method.

Best Practice: Use the Subview.once function in cases such as:
  • Event delegation for DOM events within the $wrapper

Subview.init({config})
Extends Parent's Method

Fires every time the subview is spawned. This may happen multiple times per subview since subviews are recycled in a pool when they are removed. config is optionally passed through from the SubviewPool.spawn method.

Important: Do not use Subview.init for anything that depends on elements created in the template. Instead use Subview.postRender which fires after every render event.

Best Practice: Use the Subview.init function for things like:
  • Data Model Bindings

Subview.clean()
Extends Parent's Method

Fires when the subview is removed. This function is responsible for cleaning up any bindings created in the init function.

Important: Make sure to clean up any event bindings created in Subview.init.

Subview.remove()

Removes the subview and returns it to the pool of available subviews to be recycled. Triggers Subview.clean.

Subview.active [boolean]

A boolean indicated whether or not the subview is in use or in the pool waiting to be spawned.

Templating

Subview.template = ""

A template to render inside the $wrapper. The template can be a plain HTML string or a compiled Handlebars, Underscore, EJS or Jade template. Subview.data will be passed in to the template along with a special attribute subview that contains subviews & SubviewPools listed in [Subview.subviews].

Important: Make sure to use unescaped values for inserting subviews and SubviewPools with templates:
  • Handelbars - {{{ subview.mySubview }}}
  • Underscore & EJS - <%- subview.mySubview %>
  • Jade - div!= subview.mySubview

var Li = subview('special-li', {
        tagName: 'li'
    }),
    Content = subview('content'),
    content = Content.spawn();

subview('main', {
    template: Handlebars.compile("\ 
        {{ msg }}\
        <article>\
            {{{ content }}}\
        </article>\
        <ul>\
            {{{ Li }}},\
            {{{ Li }}}\
        </ul>\ 
    "),
    data: {
        msg: "Hello World!"
    },
    subviews: {
        Li:      Li,      // A SubviewPool that will spawn subview instances wherever inserted
        content: content  // An instance of Content to be inserted into the template
    }
});

Will render in the <body>:

<div class='subview subview-main'>
    Hello World!
    <article>
        <div class='subview subview-content'></div>
    </article>
    <ul>
        <li class='subview subview-special-li'></li>
        <li class='subview subview-special-li'></li>
    </ul>
</div>

Best Practice: Write templates in a separate file and include them using CommonJS/Browserify or RequireJS.

Subview.data = {} [object or function]

Data to be passed into the template. May be an object or a function that returns an object and recieves the config passed to SubviewPool.spawn as its only argument. Note that the subview property is reserved for passing subviews into the template using Subview.subviews.

subview('some-subview', {
    template: Handlebars.compile("\ 
        {{ key }}\
        {{ hi }}\ 
    ")
    data: function(config) {
        return {
            key: config.value,
            hi:  'there'
        };
    }
});

Important: If you are going to use a data function, remember that by default the subview is only rendered once at the begining of its life. If you wish to update the template with each spawn, set Subview.reRender to true or manually call the Subview.render function.

Subview.subviews = {}

An object that contains subviews and SubviewPools that will be passed to the template as subview.{key}. See Subview.template for a usage example.

Subview.tagName = "div"

The html element type that will be created for the subview's wrapper. Defaults to "div".

Subview.className = ""

Extra class name(s) to be added to the subview's wrapper.

subview('main', {
    className: "class1 class2 class3"
});

Results in:

<div class="class1 class2 class3 subview subview-main"></div>

Subview.preRender()

A callback that fires before rendering.

Best Practice: Use this function to destroy event bindings created in Subview.postRender.

Subview.postRender()

A callback that fires after rendering.

Best Practice: Use this function for anything that references elements created in the template:
  • Manual DOM manipulation and Data additions
  • Direct DOM event bindings
  • DOM and jQuery object caching

Important: If your subview is rendered multiple times in its life-cycle, make sure to destroy any event bindings created here in Subview.preRender.

Subview.wrapper [DOM Object]

The DOM element that represents the subview.

Subview.$wrapper [jQuery Object]

A cached jQuery wrapper of Subview.wrapper.

Subview.render()

Renders the template when called. This function is automatically called when the subview is created and, when Subview.reRender is set to true, each time the subview is subsequently spawned. This method may be overwritten if no templating is being used on the subview. Note that when it is overwritten Subview.preRender and Subview.postRender will not fire.

Subview.html(html [string])

Sets the HTML inside of the subview and returns any child subviews to the available pool.

Important: Always use Subview.html when removing and changing content within a subview to ensure that all child subviews are returned to the pool.

Subview.reRender = false

When this attribute is set to true the template will be rendered every time the subview is spawned rather than just the first time the subview is created.

Events

Subview events are used to decouple subviews and eliminate the need to have direct references to subview instances. This results in very flexible and modular subviews that can be reused over and over again in different contexts.

Subview.listeners = {}

An object containing event listener definitions as keys and their callbacks as values. Listeners are scoped directionally relative to the current DOM hierarchy and may also specify a type of subview that they are listening for. The available directions are: 'up', 'down', 'across', 'all' and 'self'. The event specification format is:

"[direction]:[event name]{:[from type]}, {...}"

The callback receives the arguments passed as the second argument of the Subview.trigger method. These arguments are passed by reference, so don't worry about passing around large values. The value of this in the callbacks is the subview itself.

subview('some-subview', {
    listeners: {
        //Listen for parents broadcasting the 'open' and 'new' events
        'up:open, up:new':  function(document) {
            this.$wrapper; // this refers to the subview itself.
        }, 

        //Listen for all 'sign-out' events
        'all:sign-out':     function() {},

        //Listen for 'error' events in 'input' child subviews and in the subview itself
        'down:error:input, self:error':  function(msg) {},

        //Listen for siblings of 'form' type broadcasting 'show-extra-fields'
        'across:show-extra-fields:form': function() {}
    }
});

Note that listeners are applied when the subview is created and cannot be modified afterwards.

Best Practice: Use the most specific listener direction you can. This prevents events from other, unrelated parts of the application from effecting your subview.

Subview.trigger(name [string], {args [array]})

Broadcasts an event of name to all subviews listening in the appropriate direction for this subview's type. args is a list of arguments that will be passed to the listeners. See Subview.listeners.

subview('input', function() {
    once: function() {
        var self = this;
        this.$wrapper.blur(function() {
            var error = self.error();

            if(error) {
                self.trigger('error', [error]);
            }
        });
    },
    validate: function() {
        //Some validation
        return false;
    }
});

Traversing & Manipulation

Subview.$(selector)

A convenience function that is a shortcut for Subview.$wrapper.find(selector). Inspired by Backbone.

Subview.parent({subviewType [string]})

Returns the closest parent of the given subviewType. If no subviewType is given, returns the closest parent subview. If no subview is found, returns null.

Subview.children({subviewType [string]})

Returns the children of the given subviewType. If no subviewType is given, returns all child subviews. If no subview is found, returns null.

Subview.next({subviewType [string]})

Returns the next sibling of the given subviewType. If no subviewType is given, returns the next sibling subview. If no subview is found, returns null.

Subview.prev({subviewType [string]})

Returns the previous sibling of the given subviewType. If no subviewType is given, returns the previous sibling subview. If no subview is found, returns null.

Attributes

Subview.type [string]

The type of the subview as defined in the first argument of the subview() function.

Subview.isSubview = true

An identifier property that denotes that this is a Subview object.

Extensions

Subviews can include third-party functionality compositionally with extensions or by inheritance from another subview (subview.extend()).

Using Extensions

Subview extensions are added as properties of a subview and are given the namespace you assign. The extension itsself is a function that is called with a configuration object, with properties that depend on the individual extension. See the example below:

subview('main', {
    init: function() {
        this.foo.bar();
    },
    foo: myExtension({
        config: 'property'
    })
});

Available Extensions

Submit a pull request here to add your extension to the list!

Writing Extensions

Subview extensions are created with the subview.extension method:

module.exports = window.subview.extension({
    init: function(config, view) {

    }
    // ... more methods
});

At the moment extensions have only an init method that passes user configuration and the view it applies to its-self. In the future, this will be extended to include bindings to subview life-cycle methods such as once, preRender, postRender and clean. In all of the extension's methods this refers to the local scope of the extension NOT the subview that it is extending. This property gives extensions their own unique namespace preventing conflicts with multiple extensions.

Important: Make sure to use the instance of subview on the global (window) object to ensure that your extension is built using whatever version of subview is present on the page.

FAQ

  1. Where are my models?! Subview is not and will never be an MVC framework. Subview.js is designed to be a View/Controller component that is used along-side other libraries for Models, Routing and Event Binding. So, you ask what should I use for my model? For now try the Backbone.js model and if you know of another stand-along model module please add it here!

  2. What is an object pool and why do I have to worry about all this cleaning stuff? Subview uses object pools to manage views rather than creating new views every time one is needed. By pooling objects, they are recycled which removes the overhead of recreating objects, prevents DOM element leaks and prevents DOM listener leaks. For example, using this technique yielded speed enhancements of about 60% for document loading in FastFig. However, it does mean that you have to clean up after yourself.

  3. I want two-way data binding! Sorry, Subview.js specifically omits two-way binding because of the power it takes away from the developer to coordinate expensive changes to the DOM. This is very important when you want to create glossy-smooth animations and other frame rate sensitive effects. However, Subview plays nice with other frameworks so you can use Subview for they components of your app that it best applies to.