HenrikJoreteg / human-view

A smart base view for Backbone apps, to make it easy to bind collections and properties to the DOM.
33 stars 6 forks source link

human-view

A set of common helpers and conventions for using as a base view for humanjs / backbone applications.

It adds:

  1. Simple declarative property/template bindings without needing to include a template engine that does it for you. Which keeps your code with your code and your template as a simple function that returns an HTML string, and your payload light.
  2. A pattern for easily including the view's base element into render. Rather than having to specify tag type and attributes in javascript in the view definition you can just include that in your template like everything else.
  3. A way to render a collection of models within an element in the view, each with their own view, preserving order, and doing proper cleanup when the main view is removed.
  4. A simple way to render sub-views that get cleaned up when the parent view is removed.

Install

npm install human-view

Usage

Basics

Nothing special is required, just use HumanView in the same way as you would Backbone.View:

var MyView = HumanView.extend({
    initialize: function () { ... }, 
    render: function () { ... }
});

Declarative Bindings

var MyView = HumanView.extend({
    // set a `template` property of your view. This can either be
    // a function that returns an HTML string or just a string if 
    // no logic is required.
    template: myTemplateFunction, 
    textBindings: {
        // the model property: the css selector
        name: 'li a' 
    },
    render: function () {
        // method for rendering the view's template and binding all
        // the model properties as described by `textBindings` above.
        // You can also bind other attributes, and if you're using
        // human-model, you can bind derived properties too.
        this.renderAndBind({what: 'some context object for the template'});
    }
});

Binding types:

var View = HumanView.extend({
    template: '<li><span></span></li>',
    attributeBindings: {
        // <model_property>: [ '<css-selector>', '<attribute-name>']
        id: ['span', 'data-thing']
    }
});

handling subviews

Often you want to render some other subview within a view. The trouble is that when you remove the parent view, you also want to remove all the subviews.

HumanView has two convenience method for handling this that's also used by renderCollection to do cleanup.

It looks like this:

var HumanView = require('human-view');

// This can be *anything* with a `remove` method
// and an `el` property... such as another human-view
// instance.
// But you could very easily write other little custom views
// that followed the same conventions. Such as custom dialogs, etc.
var SubView = require('./my-sub-view');

module.exports = HumanView.extend({
    render: function () {
        // this takes a view instance and either an element, or element selector 
        // to draw the view into.
        this.renderSubview(new Subview(), '.someElementSelector');

        // There's an even lower level api that `renderSubview` usees
        // that will do nothing other than call `remove` on it when
        // the parent view is removed.
        this.registerSubview(new Subview());
    }
})

registerSubview also, stores a reference to the parent view on the subview as .parent

API Reference

Note that we're simply extending Backbone.View here, so all the methods/properties here still exist: http://backbonejs.org/#View

.template

The .template is a property for the view prototype. It should either be a string of HTML or a function that returns a string of HTML. It isn't required, but it is used as a default for calling renderAndBind and renderWithTemplate.

The important thing to note is that the HTML should not have more than one root element. This is because the view code assumes that it has one and only one root element that becomes the .el property of the instantiated view.

.renderCollection(collection, ItemView, containerEl, [viewOptions])

This method will maintain this collection within that container element. Including proper handling of add, remove, sort, reset, etc.

Also, when the parent view gets .remove()'ed any event handlers registered by the individual item views will be properly removed as well.

Each item view will only be .render()'ed once (unless you change that within the item view itself).

Example:

// some view for individual items in the collection
var ItemView = HumanView.extend({ ... });

// the main view
var MainView = HumanView.extend({
    template: '<section class="page"><ul class="itemContainer"></ul></section>',
    render: function (opts) {
        // render our template as usual
        this.renderAndBind();

        // call renderCollection with these arguments:
        // 1. collection
        // 2. which view to use for each item in the list
        // 3. which element within this view to use as the container
        // 4. options object (not required):
        //      {
        //          // function used to determine if model should be included
        //          filter: function (model) {},
        //          // boolean to specify reverse rendering order
        //          reverse: false,
        //          // view options object (just gets passed to item view's `initialize` method)
        //          viewOptions: {}
        //      }
        this.renderCollection(this.collection, ItemView, this.$('.itemContainer')[0], opts);
        return this;
    }  
})

.registerSubview(viewInstance)

.renderSubview(viewInstance, containerEl)

This method is just sugar for the common use case of instantiating a view and putting in an element within the parent.

It will:

  1. fetch your container (if you gave it a selector string)
  2. register your subview so it gets cleaned up if parent is removed and so view.parent will be available when your subview's render method gets called
  3. call the subview's render() method
  4. append it to the container
  5. return the subview

Example:

var view = HumanView.extend({
    template: '<li><div class="container"></div></li>',
    render: function () {
        this.renderAndBind();

        ...

        var model = this.model;
        this.renderSubview(new SubView({
            model: model
        }), '.container');

        ... 

    } 
});

.renderAndBind([context], [template])

This is shortcut for the default rendering you're going to do in most every render method, which is: use the template property of the view to replace this.el of the view and re-register all handlers from the event hash and any other binding as described above.

Example:

var view = HumanView.extend({
    template: '<li><a></a></li>',
    textBindings: {
        'name': 'a'
    },
    events: {
        'click a': 'handleLinkClick'
    },
    render: function () {
        // this does everything
        // 1. renders template
        // 2. registers delegated click handler
        // 3. inserts and binds the 'name' property
        //    of the view's `this.model` to the <a> tag.
        this.renderAndBind();
    }
});

.renderWithTemplate([context], [template])

This is shortcut for doing everything we need to do to render and fully replace current root element with the template that our view is wanting to render. In typical backbone view approaches you never replace the root element. But from our experience, it's nice to see the entire html structure represented by that view in the template code. Otherwise you end up with a lot of wrapper elements in your DOM tree.

.getByRole(name)

This is for convenience and also to encourage the use of the role attribute for grabbing elements from the view. Using roles to select elements in your view makes it much less likely that designers and JS devs accidentally break each other's code. This will work even if the role attribute is on the view's root el.

Example:

var view = HumanView.extend({
    template: '<li><img role="avatar" src="https://github.com/HenrikJoreteg/human-view/raw/master/user.png"/></li>',
    render: function () {
        this.renderAndBind();

        // cache an element for easy reference by other methods
        this.imgEl = this.getByRole('avatar');
    } 
});

Changelog

Test coverage?

Why yes! So glad you asked :)

Open test/test.html in a browser to run the QUnit tests.

Like this?

Follow @HenrikJoreteg on twitter and check out my recently released book: human javascript which includes a full explanation of this as well as a whole bunch of other stuff for building awesome single page apps.

license

MIT