Giter VIP home page Giter VIP logo

human-view's Introduction

human-view

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

Part of the Human JavaScript toolkit

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:

  • classBindings: Maintains a class on the element according to the following rules:
    1. If the bound property is a boolean: the name of the property will be used as the name of the class. The class will be on the element when true, and removed when the propety is false.
    2. If the property is a string: the current value of the property will be used as the class name. When the property value changes the previous class will be removed and be replaced by the current value. No other classes on that element will be disturbed.
  • textBindings: Maintains the current value of the property as the text content of the element specified by the selector.
  • htmlBindings: Just like textBindings except html is not escaped.
  • srcBindings: Binds to the src attribute (useful for avatars, etc).
  • hrefBindings: Binds to the href attribute.
  • inputBindings: Binds to the input value.
  • attributeBindings: Lets you create other arbitrary attributes bindings. For example, this would bind the model's id attribute to the data-id attribute of the span element:
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])

  • collection {Backbone Collection} The instantiated collection we wish to render.
  • itemViewClass {View Constructor} The view constructor that will be instantiated for each model in the collection. This view will be instantiated with a reference to the model and collection and the item view's render method will be called with an object containing a reference to the containerElement as follows: .render({containerEl: << element >>}).
  • containerEl {Element} The element that should hold the collection of views.
  • viewOptions {Object} [optional] Additional options
    • viewOptions {Object} Options object that will get passed to the initialize method of the individual item views.
    • filter {Function} [optional] Function that will be used to determine if a model should be rendered in this collection view. It will get called with a model and you simply return true or false.
    • reverse {Boolean} [optional] Convenience for reversing order in which the items are rendered.

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)

  • viewInstance {Object} Any object with a "remove" method, typically an instantiated view. But doesn't have to be, it can be anything with a remove method. The remove method doesn't have to actually remove itself from the DOM (since the parent view is being removed anyway), it is generally just used for unregistering any handler that it set up.

.renderSubview(viewInstance, containerEl)

  • viewInstance {Object} Any object with a .remove(), .render() and an .el property that is the DOM element for that view. Typically this is just an instantiated view.
  • containerEl {Element | String | jQueryElement} This can either be an actual DOM element or a CSS selector string such as .container. If a string is passed human view runs this.$("YOUR STRING") to try to grab the element that should contain the sub view.

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])

  • context {Object | null} [optional] The context that will be passed to the template function, usually {model: this.model}.
  • template {Function | String} [optional] A function that returns HTML or a string of HTML.

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])

  • context {Object | null} The context object that will be passed to the template function if it's a function.
  • template {Function | String} [optional] template function that returns a string of HTML or a string of HTML. If it's not passed, it will default to the template property in the view.

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)

  • name {String} The name of the 'role' attribute we're searching for.

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="/user.png"/></li>',
    render: function () {
        this.renderAndBind();

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

Changelog

  • 1.7.0 diff - Unpin from specific backbone version.
  • 1.6.5 diff - Cleaner approach for clearing out child nodes for previous bug fix.
  • 1.6.4 diff - Switch from .innnerHTML = "" to looping/removing children in renderCollection. This is a workaround for a bug in IE10.
  • 1.6.3 diff - Move throw statment for too many root elements inside non <body> case.
  • 1.6.2 diff - Make getByRole work even if role attribute is on the root element. Throws an error if your view template contains more than one root element.
  • 1.6.1 diff - Make sure renderSubview registers the subview first, so it has a .parent before it calls .render() on the subview.
  • 1.6.0 diff - Adding getByRole method
  • 1.5.0 - Adding bower.json, adding missing dev dependencies, other small bugfixes.
  • 1.4.1 - Removing elements without using jQuery's .empty() in renderCollection. (fixes: #13)
  • 1.4.0 - Adding parent reference to subviews registered via registerSubview

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

human-view's People

Contributors

henrikjoreteg avatar ike avatar qmx avatar latentflip avatar

Stargazers

azu avatar Sean Goresht avatar BJR Matos avatar Xin(Khalil) Zhang avatar Quang Van avatar Lance Harper avatar Greg Hoin avatar Rach avatar Carl Olsen avatar Glen Somerville avatar JP Camara avatar Christopher avatar Nicolas Rudas avatar Julio Carlos Menendez avatar Fredrik Forsmo avatar  avatar Tom Atkins avatar JT5D avatar David Thompson avatar Brian Walsh avatar Tai An Su avatar Kenichiro Murata avatar Yuya Saito avatar runelabs avatar Michael Whalen avatar Luke Karrys avatar Olu Ayandosu avatar Chris Witko avatar Dan Dean avatar Pierre Drescher avatar Fabian Neumann avatar Martin Lundberg avatar Jonathan Barratt avatar

Watchers

 avatar Pierre Drescher avatar James Cloos avatar Paddy O'Hanlon avatar  avatar

human-view's Issues

IE10 issue: views[] array inside renderCollection loses el.innerHTML

Bare with me, this is not easy or simple to explain.

The array you create called "views" on line 228 which is then used to store "copies" of the rendered views on line 253 is causing problems for Internet Explorer 10.

The problem is, IE10 maintains a reference between the original view and the "copy" of the view stored in views[], so when you set containerEl[0].innerHTML = '' on line 265, IE10 also wipes the innerHTML of the "copy" of the view which was stored in views[]. (All other browsers leave the innerHTML of the view stored in views[] intact.)

Then, when addView() is called for each view in reRender(), say, when a new model is added to the collection via create(), containerEl tries to append the retrieved view (from line 250) to itself on line 259, but the view.el's innerHTML is blank at that point because of line 265.

This causes all views to disappear when a new model is added to a collection and the views are reRender()ed. You can test this behavior in IE10 by doing the following:

function reRender() {
// empty without using jQuery's empty (which removes jQuery handlers)
for (var i = 0, l = views.length; i < l; i++){
console.log(views[i].el.innerHTML);
}
containerEl[0].innerHTML = '';
for (var i = 0, l = views.length; i < l; i++){
console.log(views[i].el.innerHTML);
}
collection.each(addView);
}

Looping the views[] array before and after the containerEl clears its HTML gives you a different result in IE10 vs any other browser. In IE10, the views in views[] all have correct innerHTML before containerEl clears its own innerHTML, and then empty innerHTML after. In all other browsers, the innerHTML in the views[] array is correct both before and after.

I tried several solutions to this bug, but have not solved it. I attempted to put a bandaid on the problem with my pull request #19, but that caused unexpected problems with event bindings. One solution was to pass the view through JSON.stringify then JSON.parse to create a true "copy" of the view to store in the views[] array, but while IE10 was OK with that, the other browsers choked while trying to stringify a circular ref inside the view object. Other attempts to create true copies of the view object to store in the views[] array also failed.

Somehow, the copy of the view stored in views[] needs to be "detached" from the original view object, otherwise IE10 will always destroy the innerHTML of all the views that get passed through reRender(), leaving blank markup on the page.

A fix for this would remove the need for PR #19, which is now clearly not the correct solution.

events collection binding does not target view.$el

Using the events collection to bind events to the view actually binds the events to any element in the DOM that matches the selector passed in.

Instead, the events should bind only to children of the view's $el.

Feature #2?

What does this mean, and how/why do I use it?

  1. 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.

AMD compatibility

I'd like to have AMD compatibility. Both for importing Backbone and exporting the module. Would you accept a pull request?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.