Source handlebars.el.form/handlebars.el.form.js

(function (moduleFactory) {
    if(typeof exports === "object") {
        module.exports = moduleFactory(require("lodash"), require("handlebars.phrase"), require("handlebars.el"));
    } else if (typeof define === "function" && define.amd) {
        define(["lodash", "handlebars.phrase", "handlebars.el"], moduleFactory);
    }
}(function (_, Phrase, ElHelper) {
/**
 * @module handlebars%el%form
 * @description  Provides helper to generate form elements
 * 
 *      var Handlebars = require("handlebars");
 *      var FormHelper = require("handlebars.el.form");
 *      FormHelper.registerHelper(Handlebars);
 *      
 * @returns {object} FormHelper instance
 */

    var FormHelpers = function (Handlebars, prefix) {
        var CONTROLCLASS = {
            edit: "edit",
            display: "display"
        };

        var helpers = {};

        var formDefaults = {
            "el-tag": "form",
            "method": "post",
            "el-escape": false
        };
        /**
         * @template el-form
         * @block helper
         * @param {string} [method=post] Form method
         * @param {boolean} [el-escape=false] Whether to escape form content
         * @param {object} [model] Model object to provide values for form controls
         * @description Outputs a form element
         *
         *     {{#el-form}}…{{/el-form}}
         *
         * If a model it passed, it is used to provide values for any nested controls (unless any of those controls have been passed an explicit value).
         *
         * Accepts any parameter that the *el* helper takes since it is built upon it
         * @returns {string} Form output
         */ 
        // TODO wizard
        helpers["el-form"] = function () {
            var args = Array.prototype.slice.call(arguments);
            var options = args.pop();
            var hash = options.hash;
            var model = hash.model;
            delete hash.model;
            var wizard = hash.wizard;
            delete hash.wizard;

            hash = _.extend({}, hash.attributes, hash);
            delete hash.attributes;
            hash = _.extend({}, formDefaults, hash);
            options.hash = hash;

            var that = this || {};
            that.model = model;
            that.wizardSchema = wizard;
            return Handlebars.helpers.el.apply(that, [options]);
        };

        //TODO - submit, fieldset, field, group, field, form
        // redo el to include el- on attributes

        /**
         * @method inputHelper
         * @param {string} 0 Input type
         * @param {string} [id] Control id
         * @param {string} [name] Control name
         * @param {string} [value] Control value
         * @param {boolean} [el-control] Wjether or not to treat output as a control or to be displayed
         * @description  Generic method to handle output of all input elements
         *
         * Accepts any parameter that the *el* helper takes since it is built upon it
         * @return {string} Input field
         */ 
        function inputHelper () {
            var args = Array.prototype.slice.call(arguments);
            var hash = args[args.length-1].hash;
            hash["el-tag"] = "input";
            hash.type = args.shift();
            // display rather than edit mode
            if (hash["el-control"] === false) {
                for (var prop in hash.attributes) {
                    if (hash[prop] === undefined) {
                        hash[prop] = hash.attributes[prop];
                    }
                }
                delete hash.attributes;
                // default tag - but why hard-coded and not overridable?
                hash["el-tag"] = "p";
                if (hash.content === undefined) {
                    if (hash.type === "checkbox") {
                        var checked = !!hash.checked;
                        hash.content = checked.toString();
                    } else {
                        hash.contents = hash.value;
                    }
                    delete hash.value;
                    delete hash.id;
                    delete hash.name;
                    delete hash.type;
                }
            }
            delete hash["el-control"];
            var output = Handlebars.helpers.el.apply(this, args);
            return output;
        }

        /**
         * @template el-text
         * @block helper
         * @description  Outputs text input element
         * 
         *     {{{el-text name=name}}}
         *     
         * Calls {@link module:handlebars%el%form~inputHelper} passing *text* as the type
         *
         * Since inputHelper in turn calls the *el* helper, accepts any parameter that the *el* helper takes
         * @return {string} Text input
         */
        helpers["el-text"] = function () {
            var args = Array.prototype.slice.call(arguments);
            args.unshift("text");
            return inputHelper.apply(this, args);
        };

        /**
         * @template el-checkbox
         * @block helper
         * @description  Outputs checkbox input element
         * 
         *     {{{el-checkbox name=name}}}
         *     
         * Calls {@link module:handlebars%el%form~inputHelper} passing *checkbox* as the type
         *
         * Since inputHelper in turn calls the *el* helper, accepts any parameter that the *el* helper takes
         *
         * ### Examples
         * 
         *     {{{el-checkbox name="foo"}}}
         *     →  <input name="foo" type="checkbox" value="true">
         *     
         *     {{{el-checkbox name="foo" value="bar"}}}
         *     →  <input name="foo" type="checkbox" value="bar">
         *     
         *     {{{el-checkbox name="foo" attributes=attributes}}}
         *     Context {attributes:{value:"bar"}}
         *     →  <input name="foo" type="checkbox" value="bar">
         *     
         *     {{{el-checkbox name="foo" checked=true}}}
         *     →  <input checked name="foo" type="checkbox" value="true">
         * @return {string} Checkbox input
         */
        helpers["el-checkbox"] = function () {
            var args = Array.prototype.slice.call(arguments);
            args.unshift("checkbox");
            args[args.length-1].hash["el-fallback-value"] = true;
            return inputHelper.apply(this, args);
        };

        /**
         * @template el-radio
         * @block helper
         * @description  Outputs radio input element
         * 
         *     {{{el-radio name=name}}}
         *     
         * Calls {@link module:handlebars%el%form~inputHelper} passing *radio* as the type
         *
         * Since inputHelper in turn calls the *el* helper, accepts any parameter that the *el* helper takes
         * @return {string} Radio input
         */
        helpers["el-radio"] = function () {
            var args = Array.prototype.slice.call(arguments);
            args.unshift("radio");
            return inputHelper.apply(this, args);
        };

        /**
         * @template el-password
         * @block helper
         * @description  Outputs password input element
         * 
         *     {{{el-password name=name}}}
         * 
         * Calls {@link module:handlebars%el%form~inputHelper} passing *password* as the type
         *
         * Since inputHelper in turn calls the *el* helper, accepts any parameter that the *el* helper takes
         * @return {string} Password input
         */
        helpers["el-password"] = function () {
            var args = Array.prototype.slice.call(arguments);
            args.unshift("password");
            return inputHelper.apply(this, args);
        };

        /**
         * @template el-file
         * @block helper
         * @description  Outputs file input element
         * 
         *     {{{el-file name=name}}}
         *     
         * Calls {@link module:handlebars%el%form~inputHelper} passing *file* as the type
         *
         * Since inputHelper in turn calls the *el* helper, accepts any parameter that the *el* helper takes
         * @return {string} File input
         */
        helpers["el-file"] = function () {
            var args = Array.prototype.slice.call(arguments);
            args.unshift("file");
            return inputHelper.apply(this, args);
        };

        /**
         * @template el-hidden
         * @block helper
         * @description  Outputs hidden input element
         * 
         *     {{{el-hidden name=name}}}
         *     
         * Calls {@link module:handlebars%el%form~inputHelper} passing *hidden* as the type
         *
         * Since inputHelper in turn calls the *el* helper, accepts any parameter that the *el* helper takes
         * @return {string} Hidden input
         */
        helpers["el-hidden"] = function () {
            var args = Array.prototype.slice.call(arguments);
            args.unshift("hidden");
            return inputHelper.apply(this, args);
        };

        /**
         * @template el-submit
         * @block helper
         * @description  Outputs submit input element
         * 
         *     {{{el-submit name=name}}}
         *     
         * Calls {@link module:handlebars%el%form~inputHelper} passing *submit* as the type
         *
         * Since inputHelper in turn calls the *el* helper, accepts any parameter that the *el* helper takes
         * @return {string} Submit input
         */
        helpers["el-submit"] = function () {
            var args = Array.prototype.slice.call(arguments);
            args.unshift("submit");
            return inputHelper.apply(this, args);
        };

        /**
         * @method  normaliseParamArray
         * @private
         * @param  {array|string} param Parameters to be normalised
         * @description Ensures that all items of the passed param are stringified
         *
         * If param is not an array, it is first wrapped in an array.
         * @return {array} param
         */
        function normaliseParamArray (param) {
             if (param !== undefined && param !== null) {
                if (!_.isArray(param)) {
                    param = [param];
                }
                for (var i = 0; i < param.length; i++) {
                    param[i] = param[i].toString();
                }
            }
            return param;
        }

        /**
         * @template el-select
         * @block helper
         * @param {array} [options] Array of option objects or strings
         * @params {boolean} [options.content] Option content
         *
         * If option passed as a string, it is coerced to an object setting the content property to the string’s value
         * @params {boolean} [options.value] Option value
         *
         * If no value is passed, the option’s content is used
         * @params {boolean} [options.selected] Whether option is selected
         * @params {boolean} [options.disabled] Whether option is disabled
         * @param {array} [values] Array of values
         * 
         * If passed, trumps any value passed as part of the corresponding option object
         *
         * If no options are passed, values is used instead
         * @param {string|array} [selected] Values which are selected
         *
         * If passed, trumps any selected value passed as part of the corresponding option object
         * @param {string|array} [value] Alternative param to pass selected value
         * @param {string|array} [options-disabled] Values which are disabled
         *
         * If passed, trumps any disabled value passed as part of the corresponding option object
         *
         * NB. The parameter is options-disabled, so that disabled can still be used to disable the entire control
         * @param {string|object} [cue] String to display as first element of select when no values has been selected
         * @description  Outputs a select control
         *
         *     {{{el-select name=name options=options}}}
         *
         * ### Examples
         *
         *     {{{el-select name="foo" options=opts}}}
         *     Context {opts:["a", "b"]}
         *     →  <select name="foo">
         *          <option value="a">a</option>
         *          <option value="b">b</option>
         *        </select>
         *
         *     {{{el-select options=opts}}}
         *     Context {opts: [{content:"Foo", value:1}, {content:"Bar", value:2}] }
         *     →  <select>
         *          <option value="1">Foo</option>
         *          <option value="2">Bar</option>
         *        </select>
         *
         *     {{{el-select options=opts}}}
         *     Context {opts: [{content:"Foo", value:1}, {content:"Bar", value:2, selected:true}] }
         *     →  <select>
         *          <option value="1">Foo</option>
         *          <option selected value="2">Bar</option>
         *        </select>
         *
         *     {{{el-select options=opts values=vals}}}
         *     Context {opts: ["Foo", "Bar"], vals: [1, 2]}
         *     →  <select>
         *          <option value="1">Foo</option>
         *          <option value="2">Bar</option>
         *        </select>
         *
         *     {{{el-select options=opts values=vals selected=selected}}}
         *     Context {opts: ["Foo", "Bar"], vals: [1, 2], selected:2}
         *     →  <select>
         *          <option value="1">Foo</option>
         *          <option selected value="2">Bar</option>
         *        </select>
         *
         *     {{{el-select options=opts values=vals selected=selected}}}
         *     Context {opts: ["Foo", "Bar"], vals: [1, 0], selected:0}
         *     →  <select>
         *          <option value="1">Foo</option>
         *          <option selected value="0">Bar</option>
         *        </select>
         *
         *     {{{el-select options=opts values=vals selected=selected}}}
         *     Context {opts: ["Foo", "Bar"], vals: ["", "a"], selected:""}
         *     →  <select>
         *          <option selected value="">Foo</option>
         *          <option value="a">Bar</option>
         *        </select>
         *
         *     {{{el-select options=opts values=vals options-disabled=optionsDisabled}}}
         *     Context {opts: ["Foo", "Bar"], vals: [1, 2], optionsDisabled:2}
         *     →  <select>
         *          <option value="1">Foo</option>
         *          <option disabled value="2">Bar</option>
         *        </select>
         *
         *     {{{el-select options=opts values=vals selected=selected options-disabled=optionsDisabled}}}
         *     Context {opts: [{content:"Foo", value:1, disabled:true}, {content:"Bar", value:2, selected:true}], vals:["x", "y"], selected:"x", optionsDisabled:"y" }
         *     →  <select>
         *          <option selected value="x">Foo</option>
         *          <option disabled value="y">Bar</option>
         *        </select>
         *
         *     {{{el-select options=opts cue=cue}}}
         *     Context {opts: [{content:"Foo", value:1}, {content:"Bar", value:2}], cue: "Please select" }
         *     →  <select>
         *          <option class="select-cue" disabled selected>Please select</option>
         *          <option value="1">Foo</option>
         *          <option value="2">Bar</option>
         *        </select>
         *
         *     {{{el-select options=opts values=vals value=selected}}}
         *     Context {opts: ["Foo", "Bar"], vals: [1, 2], selected:2}
         *     →  <select>
         *          <option value="1">Foo</option>
         *          <option selected value="2">Bar</option>
         *        </select>
         * 
         * @return {string} Select control
         */
        helpers["el-select"] = function () {
            //TODO
            // map, mapcontent, mapvalue, optgroups
            var args = Array.prototype.slice.call(arguments);
            var options = args[args.length-1];
            var optionsHash = options.hash;
            optionsHash = _.extend({}, optionsHash.attributes, optionsHash);
            delete optionsHash.attributes;
            options.hash = optionsHash;

            var output = "";
            var selected = optionsHash.selected;
            var value = optionsHash.value;
            var optionsDisabled = optionsHash["options-disabled"];
            var cue = optionsHash.cue;
            var opts = optionsHash.options;
            var vals = optionsHash.values;
            if (!opts) {
                opts = vals;
            }
            delete optionsHash.selected;
            delete optionsHash.value;
            delete optionsHash["options-disabled"];
            delete optionsHash.cue;
            delete optionsHash.options;
            delete optionsHash.values;

            if (selected === undefined) {
                selected = value;
            }

            selected = normaliseParamArray(selected);
            optionsDisabled = normaliseParamArray(optionsDisabled);

            var hasSelected;
            for (var i in opts) {
                var optionArgs = opts[i];
                if (typeof optionArgs !== "object") {
                    optionArgs = {
                        content: optionArgs
                    };
                }
                if (vals && vals[i] !== undefined) {
                    optionArgs.value = vals[i];
                } else if (optionArgs.value === undefined) {
                    optionArgs.value = optionArgs.content;
                }
                optionArgs.value = optionArgs.value.toString();
                if (selected !== undefined) {
                    optionArgs.selected = _.intersection(selected, [optionArgs.value]).length > 0;
                    if (!hasSelected) {
                        hasSelected = optionArgs.selected;
                    }
                }

                if (optionsDisabled) {
                    optionArgs.disabled = _.intersection(optionsDisabled, [optionArgs.value]).length > 0;
                }

                opts[i] = optionArgs;
            }

            // No need for a cue if we have a genuine selection
            if (!hasSelected && cue) {
                if (typeof cue !== "object") {
                    cue = {
                        content: cue,
                        "class": "select-cue",
                        "selected": true,
                        "disabled": true
                    };
                }
                opts.unshift(cue);
            }

            for (var opt in opts) {
                opts[opt]["el-tag"] = "option";
                output += Handlebars.helpers.el.apply(this, [{hash:opts[opt]}]);
            }
            optionsHash["el-tag"] = "select";
            optionsHash.content = output;
            optionsHash["el-escape"] = false;
            output = Handlebars.helpers.el.apply(this, args);
            return output;
        };

        /**
         * @template el-textarea
         * @block helper
         * @param {string|array} [value] Text to be output in textarea
         *
         * NB. this is in addition to the usual content and el-content params that the el helper accepts to provide consistency with the other input helpers
         * @param {string} [placeholder] Placeholder text
         * @param {string} [el-phrase-key] Key to use to lookup placeholder text
         * @param {boolean} [el-control] Whether it is in edit or display mode
         * @description  Outputs a textarea element
         *
         *     {{{el-textarea name=name}}}
         *     {{#el-textarea name=name}}...{{/el-textarea}}
         * 
         * Calls the *el* helper so accepts any parameter that the *el* helper takes
         * @return {string} Textarea control
         */
        helpers["el-textarea"] = function () {
            var args = Array.prototype.slice.call(arguments);
            var hash = args[args.length-1].hash;
            //if (hash["el-control"] !== undefined && hash["el-control"] === false) {
            if (hash["el-control"] === false) {
                hash = _.extend({}, hash.attributes, hash);
                delete hash.attributes;
                hash["el-tag"] = "p";
                hash.content = hash.value;
                delete hash.value;
                delete hash.name;
                delete hash.placeholder;
                args[args.length-1].hash = hash;
            } else {
                hash = _.extend({}, hash.attributes, hash);
                delete hash.attributes;
                if (!hash["el-content"]) {
                    hash["el-content"] = hash.value;
                    delete hash.value;
                }
                hash["el-tag"] = "textarea";
                if (!hash.placeholder && hash["el-phrase-key"]) {
                    hash.placeholder = Phrase.get(hash["el-phrase-key"]+"."+"placeholder");
                }
                args[args.length-1].hash = hash;
            }
            var output = Handlebars.helpers.el.apply(this, args);
            return output;
        };

        /**
         * @template el-label
         * @block helper
         * @param {string} [for] Id of input label is connected to
         * @description  Outputs a label for a control
         *
         *     {{{el-label for=for content=content}}}
         *     {{#el-label}}...{{/el-label}}
         * 
         * Calls the *el* helper so accepts any parameter that the *el* helper takes
         * @return {string} Label for control
         */
        helpers["el-label"] = function () {
            var args = Array.prototype.slice.call(arguments);
            var hash = args[args.length-1].hash;
            hash["el-tag"] = "label";
            if (hash.edit !== undefined && hash.edit === false) {
                hash["el-tag"] = "h3";
                delete hash.for;
            }
            delete hash.edit;
            var output = Handlebars.helpers.el.apply(this, args);
            return output;
        };

        function legendOutput (leg) {
            return "<legend><span>" + leg + "</span></legend>";
        }

        /**
         * @template el-fieldset
         * @block helper
         * @param {string} [legend] Text for fieldset legend
         * @description  Outputs a fieldset
         *
         *     {{#el-fieldset legend=legend}}...{{/el-fieldset}}
         *
         * Calls the *el* helper so accepts any parameter that the *el* helper takes
         * @return {string} Fieldset
         */
        helpers["el-fieldset"] = function () {
            var args = Array.prototype.slice.call(arguments);
            var hash = args[args.length-1].hash;
            hash["el-tag"] = "fieldset";
            hash["el-escape"] = false;
            if (hash.legend) {
                hash["el-content-before"] = legendOutput(hash.legend);
                delete hash.legend;
            }
            var output = Handlebars.helpers.el.apply(this, args);
            return output;
        };

        /**
         * @template el-message
         * @block helper
         * @param {string} 0  Field messages relate to
         * @param {string} 1  Message type
         * @param {array|string} 2  Message(s) to display
         * @description  Outputs a message block for a control
         *
         *     {{{el-message name type messages}}}
         *
         * Can be
         *
         * - error
         * - warning
         * - info
         *
         * Gets message block heading by using field.message.heading.{{type}} as a lookup key
         * @return {string} Message block for control
         */
        helpers["el-message"] = function () {
            var args = Array.prototype.slice.call(arguments);
            var field = args.shift();
            var type = args.shift();
            var messages = args.shift();

            if (! _.isArray(messages)) {
                messages = [messages];
            }
            var listArgs = {
                content: messages,
                "el-wrap": "li",
                "el-tag": "ul"
            };
            var messagesOutput = Handlebars.helpers.el({
                hash: listArgs
            });
            var messageHeading = Phrase.get("field.message.heading." + type, {
                field: field,
                count: messages.length
            });
            if (messageHeading) {
                messagesOutput = "<h2>" + messageHeading + "</h2>" + messagesOutput;
            }
            var messageBundle = {
                "el-tag": "div",
                "class": "control-" + type,
                "content": messagesOutput,
                "el-escape": false
            };

            var messageOutput = Handlebars.helpers.el({
                hash: messageBundle
            });

            return messageOutput;

        };

        /**
         * @method  marshallNamespacedAttributes
         * @private
         * @param  {string}  namespace  Property name to marshall
         * @param  {object|string}  attributes  Attributes to retrieve property from
         * @description  Used by el-field to allow for passing of multi-variate attributes
         * @return {object}  attributes
         */
        function marshallNamespacedAttributes(namespace, attributes) {
            if (attributes[namespace] === undefined) {
                attributes[namespace] = {};
            } else if (typeof attributes[namespace] === "string") {
                attributes[namespace] = {
                    content: attributes[namespace]
                };
            }
            var nameSpAlias = attributes[namespace];
            for (var prop in attributes) {
                if (prop.indexOf(namespace+"-") === 0) {
                    var mappedProp = prop.split(namespace+"-")[1];
                    nameSpAlias[mappedProp] = attributes[prop];
                    delete attributes[prop];
                }
            }
            return attributes;
        }

        /**
         * @template el-field
         * @block helper
         * @param {string} name Input name
         *
         * Can also be passed as a property of input
         * @param {string} [id=input-{{name}}] Input id
         * @param {string} [type]  Field type
         * @param {string|array} [class] Field class(es)
         * @param {object} [field] Parameters for field wrapper
         * @param {object|string} [input] Parameters for single input
         * @param {string|object} [introduction]  Parameters for introduction block
         * @param {string|object} [label]  Parameters for label block
         * @param {string|object} [description]  Parameters for description block
         * @param {string|object} [help]  Parameters for help block
         * @param {array} [inputs] Parameters for multiple inputs within control
         *
         * If no id is passed as part of the individual inputs items it will be generated as {{id}}-{{n}}
         * @param {array} [labels]
         * 
         * If passed, trumps any label passed as part of the corresponding inputs array
         *
         * If no inputs are passed, labels is used instead
         * @param {string|object} [legend]  Optional legend block when multiple inputs are passed
         *
         * Ignored otherwise
         * @param {array|string} [checked]  Values which are checked
         * @param {array|string} [inputs-disabled]  Values which are disabled
         * @param {boolean} [edit=true]  Whether to render in edit or display mode
         * @param {boolean} [display=false]  Whether to render in edit or display mode (trumped by edit if defined)
         * @param {object} [model=this.model]  Model from which to retrieve schema, value and phraseKey
         *
         * If no model is passed, will attempt to use the current context’s model property
         * @param {string} [phrase-key={{model.phraseKey}}.{{name}}]  String to use as base lookup key
         *
         * - blocks
         *   {{phraseKey}}.{{blockKey}}
         * - multi-input labels when schema type is enum
         *   {{phraseKey}}.value.{{fieldOptValue}}.label 
         * @param {string|array} [error]  Parameters for error block
         * @param {string|array} [warning]  Parameters for warning block
         * @param {string|array} [info]  Parameters for info block
         * @description  Outputs a field control
         *
         *     {{{el-field name=name}}}
         *     {{{el-field name=name type=type}}}
         *     {{{el-field name=name input=input}}}
         *     {{{el-field name=name inputs=inputs}}}
         *     {{{el-field name=name model=model}}}
         *     {{{el-field name=name
         *                 introduction=introduction
         *                 description=description
         *                 help=help
         *                 error=error
         *                 extra=extra}}}
         *     {{#el-field name=name}}...{{/el-field}}
         * 
         * Generates a field element wrapped around any specified blocks
         *
         * ### Examples
         * 
         *     {{{el-field name="bar"}}}
         *     →  <div class="control control-bar">
         *          <label for="input-bar">Bar</label>
         *          <div class="edit">
         *            <input id="input-bar" name="bar" type="text">
         *          </div>
         *        </div>
         *
         * el-field can capture content
         *
         *     {{#el-field name="bar"}}{{{el-text name="foo"}}}{{/el-field}}
         *     →  <div class="control control-bar">
         *          <input name="foo" type="text">
         *        </div>
         *
         * Edit and display modes
         *
         *     {{{el-field name="foo" value="bar"}}}
         *     →  <div class="control control-foo">
         *          <label for="input-foo">Foo</label>
         *          <div class="edit">
         *            <input id="input-foo" name="foo" type="text" value="bar">
         *          </div>
         *        </div>
         *
         *     {{{el-field name="foo" value="bar" display=true}}}
         *     →  <div class="control control-foo">
         *          <h3>Foo</h3>
         *          <div class="display">
         *            <p>bar</p>
         *          </div>
         *        </div>
         *
         *     {{{el-field name="foo" value="bar" edit=false}}}
         *     →  <div class="control control-foo">
         *          <h3>Foo</h3>
         *          <div class="display">
         *            <p>bar</p>
         *          </div>
         *        </div>
         *
         * Pass additional class on the field wrapper
         *
         *     {{{el-field name="bar" class="foo" value="myvalue"}}}
         *     →  <div class="control control-bar foo">
         *          <label for="input-bar">Bar</label>
         *          <div class="edit">
         *            <input id="input-bar" name="bar" type="text" value="myvalue">
         *          </div>
         *        </div>
         *
         * Pass options to a select field
         *
         *     {{{el-field name="bar" type="select" options=opts}}}
         *     Context {opts: ["foo", "bar"]}
         *     →  <div class="control control-bar">
         *          <label for="input-bar">Bar</label>
         *          <div class="edit">
         *            <select id="input-bar" name="bar">
         *              <option value="foo">foo</option>
         *              <option value="bar">bar</option>
         *            </select>
         *          </div>
         *        </div>
         *
         * Pass a label
         *
         *     {{{el-field name="bar" label="Foo"}}}
         *     →  <div class="control control-bar">
         *          <label for="input-bar">Foo</label>
         *          <div class="edit">
         *            <input id="input-bar" name="bar" type="text">
         *          </div>
         *        </div>
         *
         *     {{{el-field name="bar" label="Foo" id="zam"}}}
         *     →  <div class="control control-bar">
         *          <label for="zam">Foo</label>
         *          <div class="edit">
         *            <input id="zam" name="bar" type="text">
         *          </div>
         *        </div>
         *
         * Passing mutltiple inputs - checkboxes
         *
         *     {{{el-field name="bar" legend="Foo" inputs=inputs checked="b"}}}
         *     Context {inputs:["a", "b", "c"]}
         *     →  <div aria-labelledby="control-group-bar-label" class="control control-bar" role="group">
         *          <p id="control-group-bar-label">Foo</p>
         *          <div class="edit">
         *            <input id="bar-0" name="bar" type="radio" value="a">
         *            <label for="bar-0">a</label>
         *          </div>
         *          <div class="edit">
         *            <input checked id="bar-1" name="bar" type="radio" value="b">
         *            <label for="bar-1">b</label>
         *          </div>
         *          <div class="edit">
         *            <input id="bar-2" name="bar" type="radio" value="c">
         *            <label for="bar-2">c</label>
         *          </div>
         *        </div>
         *
         *     {{{el-field name="bar" legend="Foo" type="checkbox" inputs=inputs checked="b"}}}
         *     Context {inputs:["a", "b", "c"]
         *     →  <div aria-labelledby="control-group-bar-label" class="control control-bar" role="group">
         *          <p id="control-group-bar-label">Foo</p>
         *          <div class="edit">
         *            <input id="bar-0" name="bar" type="checkbox" value="a">
         *            <label for="bar-0">a</label>
         *          </div>
         *          <div class="edit">
         *            <input checked id="bar-1" name="bar" type="checkbox" value="b">
         *            <label for="bar-1">b</label>
         *          </div>
         *          <div class="edit">
         *            <input id="bar-2" name="bar" type="checkbox" value="c">
         *            <label for="bar-2">c</label>
         *          </div>
         *        </div>
         *
         * Passing a lookup key
         *
         *     {{{el-field name="bar" phrase-key="foo.bar"}}}
         *     →  <div class="control control-bar">
         *          <div class="control-introduction">
         *            <p>Bar introduction</p>
         *          </div>
         *          <label for="input-bar">Bar label</label>
         *          <div class="control-description">
         *            <p>Bar description</p>
         *           </div>
         *           <div class="control-help">
         *             <p>Bar help</p>
         *           </div>
         *           <div class="edit">
         *             <input id="input-bar" name="bar" type="text">
         *           </div>
         *           <div class="control-extra">
         *             <p>Bar extra line 1</p>
         *             <p>Bar extra line 2</p>
         *           </div>
         *         </div>
         *
         * Passing errors
         *
         *     {{{el-field name="bar" error=error}}}
         *     Context {error:["Error A", "Error B"]}
         *     →  <div class="control control-bar">
         *          <label for="input-bar">Bar</label>
         *          <div class="edit">
         *            <input id="input-bar" name="bar" type="text">
         *          </div>
         *        <div class="control-error">
         *          <h2>There are 2 problems with the "bar" field</h2>
         *          <ul>
         *            <li>Error A</li>
         *            <li>Error B</li>
         *          </ul>
         *        </div>
         *      </div>
         *
         * #### Using models with el-field
         *
         *     {{{el-field model=model name="baz"}}}
         *     Context {model:fooModel}
         *     →  <div class="control control-baz">
         *          <label for="input-baz">Baz label</label>
         *          <div class="edit">
         *            <input id="input-baz" name="baz" type="text" value="Model baz value">
         *          </div>
         *        </div>
         *     
         *     {{{el-field model=model name="jim" type="select" options=opts}}}
         *     Context {model:fooModel, opts:["zim", "bam", "zoom"]}
         *     →  <div class="control control-jim">
         *          <label for="input-jim">Jim</label>
         *          <div class="edit">
         *            <select id="input-jim" name="jim">
         *              <option value="zim">zim</option>
         *              <option selected value="bam">bam</option>
         *              <option value="zoom">zoom</option>
         *            </select>
         *          </div>
         *        </div>
         *
         *     {{{el-field model=model name="flim" type="radio" inputs=opts}}}
         *     Context {model:fooModel, opts:["zim", "zam", "boom"]}
         *     →  <div class="control control-flim">
         *          <div class="edit">
         *            <input id="flim-0" name="flim" type="radio" value="zim">
         *            <label for="flim-0">zim</label>
         *          </div>
         *          <div class="edit">
         *            <input id="flim-1" name="flim" type="radio" value="zam">
         *            <label for="flim-1">zam</label>
         *          </div>
         *          <div class="edit">
         *            <input checked id="flim-2" name="flim" type="radio" value="boom">
         *            <label for="flim-2">boom</label>
         *          </div>
         *        </div>
         *
         *     {{{el-field model=model name="flim"}}}
         *     Context {model:fooModel}
         *     →  <div class="control control-flim">
         *          <div class="edit">
         *            <input id="flim-0" name="flim" type="radio" value="bim">
         *            <label for="flim-0">Bim label</label>
         *          </div>
         *          <div class="edit">
         *            <input id="flim-1" name="flim" type="radio" value="bam">
         *            <label for="flim-1">Bam label</label>
         *          </div>
         *          <div class="edit">
         *            <input checked id="flim-2" name="flim" type="radio" value="boom">
         *            <label for="flim-2">Boom label</label>
         *          </div>
         *        </div>
         *
         *     {{{el-field model=model name="jim" type="select"}}}
         *     Context {model:fooModel}
         *     →  <div class="control control-jim">
         *          <label for="input-jim">Jim</label>
         *          <div class="edit">
         *            <select id="input-jim" name="jim">
         *              <option value="bim">bim</option>
         *              <option selected value="bam">bam</option>
         *              <option value="boom">boom</option>
         *            </select>
         *          </div>
         *        </div>
         *
         *     {{{el-field model=model name="wozz" type="select"}}}
         *     Context {model:fooModel}
         *     →  <div class="control control-wozz">
         *          <label for="input-wozz">Wozz</label>
         *          <div class="edit">
         *            <select id="input-wozz" name="wozz">
         *              <option value="waz">Waz label</option>
         *              <option value="wez">Wez label</option>
         *              <option selected value="wiz">Wiz label</option>
         *            </select>
         *          </div>
         *        </div>
         *
         *     {{{el-field model=model name="gozz"}}}
         *     Context {model:fooModel}
         *     →  <div class="control control-gozz">
         *          <div class="edit">
         *            <input checked id="gozz-0" name="gozz" type="radio" value="gaz">
         *            <label for="gozz-0">Gaz label</label>
         *          </div>
         *          <div class="edit">
         *            <input checked id="gozz-1" name="gozz" type="radio" value="gez">
         *            <label for="gozz-1">Gez label</label>
         *          </div>
         *          <div class="edit">
         *            <input id="gozz-2" name="gozz" type="radio" value="giz">
         *            <label for="gozz-2">Giz label</label>
         *          </div>
         *        </div>
         *
         * Inheriting a model from the enclosing context
         * 
         *     {{#el-form model=model}}{{{el-field name="flim"}}}{{{el-field name="wozz" type="select"}}}{{{el-field name="gozz"}}}{{/el-form}}
         *     Context {model:fooModel}
         *     →  <form method="post">
         *          <div class="control control-flim">
         *            <div class="edit">
         *              <input id="flim-0" name="flim" type="radio" value="bim">
         *              <label for="flim-0">Bim label</label>
         *            </div>
         *            <div class="edit">
         *              <input id="flim-1" name="flim" type="radio" value="bam">
         *              <label for="flim-1">Bam label</label>
         *            </div>
         *            <div class="edit">
         *              <input checked id="flim-2" name="flim" type="radio" value="boom">
         *              <label for="flim-2">Boom label</label>
         *            </div>
         *          </div>
         *          <div class="control control-wozz">
         *            <label for="input-wozz">Wozz</label>
         *            <div class="edit">
         *              <select id="input-wozz" name="wozz">
         *                <option value="waz">Waz label</option>
         *                <option value="wez">Wez label</option>
         *                <option selected value="wiz">Wiz label</option>
         *              </select>
         *            </div>
         *          </div>
         *          <div class="control control-gozz">
         *            <div class="edit">
         *              <input checked id="gozz-0" name="gozz" type="radio" value="gaz">
         *              <label for="gozz-0">Gaz label</label>
         *            </div>
         *          <div class="edit">
         *            <input checked id="gozz-1" name="gozz" type="radio" value="gez">
         *            <label for="gozz-1">Gez label</label>
         *          </div>
         *          <div class="edit">
         *            <input id="gozz-2" name="gozz" type="radio" value="giz">
         *            <label for="gozz-2">Giz label</label>
         *          </div>
         *        </div>
         *      </form>
         * 
         *
         * #### Data used in examples
         *
         * Phrase lookup keys
         *
         *     {
         *         "field.message.heading.error": 'There {{#choose count}}{{#choice "other"}}are {{count}} problems{{/choice}}{{#choice "one"}}is a problem{{/choice}}{{/choose}} with the "{{field}}" field',
         *         "foo.bar.description": "Bar description",
         *         "foo.bar.introduction": "Bar introduction",
         *         "foo.bar.label": "Bar label",
         *         "foo.bar.help": "Bar help",
         *         "foo.bar.extra": "Bar extra line 1\nBar extra line 2",
         *         "foo.baz.label": "Baz label",
         *         "foo.flim.value.bim.label": "Bim label",
         *         "foo.flim.value.bam.label": "Bam label",
         *         "foo.flim.value.boom.label": "Boom label",
         *         "foo.wozz.value.waz.label": "Waz label",
         *         "foo.wozz.value.wez.label": "Wez label",
         *         "foo.wozz.value.wiz.label": "Wiz label",
         *         "foo.gozz.value.gaz.label": "Gaz label",
         *         "foo.gozz.value.gez.label": "Gez label",
         *         "foo.gozz.value.giz.label": "Giz label",
         *         "really./^x.$/": "Regex lookup"
         *     }
         *
         * fooModel
         *
         *     {
         *         attributes: {
         *             baz: "Model baz value",
         *             jim: "bam",
         *             flim: "boom",
         *             wozz: "wiz",
         *             gozz: ["gaz", "gez"]
         *         },
         *         get: function (name) {
         *             return this.attributes[name];
         *         },
         *         phraseKey: "foo",
         *         schema: schema
         *     }
         *
         * schema
         *
         *     {
         *     ...
         *         "properties":{
         *             "bar": {
         *                 "type":"string",
         *                 "required":true
         *             },
         *             "baz": {
         *                 "type":"number"
         *             },
         *             "jim": {
         *                 "enum": [
         *                     "bim",
         *                     "bam",
         *                     "boom"
         *                 ]
         *             },
         *             "flim": {
         *                 "type": "string",
         *                 "enum": [
         *                     "bim",
         *                     "bam",
         *                     "boom"
         *                 ]
         *             },
         *             "wozz": {
         *                 "type": "string",
         *                 "enum": [
         *                     "waz",
         *                     "wez",
         *                     "wiz"
         *                 ]
         *             },
         *             "gozz": {
         *                 "type": "array",
         *                 "items": {
         *                     "type": "string",
         *                     "enum": [
         *                         "gaz",
         *                         "gez",
         *                         "giz"
         *                     ]
         *                 }
         *             }
         *         }
         *     }
         * 
         * @return {string} Field control
         */
        helpers["el-field"] = function () {
            var blocks = {};
            /**
             * @member blockList
             * @memberof template:el-field
             * @private
             * @description Blocks to be processed
             *
             * - legend
             * - introduction
             * - label
             * - description
             * - help
             * - error
             * - warning
             * - info
             * - extra
             */
            var blockList = [
                "legend",
                "introduction",
                "label",
                "description",
                "help",
                "error",
                "warning",
                "info",
                "extra"
            ];
            /**
             * @member editBlockOrder
             * @memberof template:el-field
             * @private
             * @description Order of blocks output in edit mode
             *
             * 1. introduction
             * 2. label
             * 3. description
             * 4. help
             * 5. main
             * 6. error
             * 7. warning
             * 8. info
             * 9. extra
             */
            var editBlockOrder = [
                "introduction",
                "label",
                "description",
                "help",
                "main",
                "error",
                "warning",
                "info",
                "extra"
            ];
            /**
             * @member displayBlockOrder
             * @memberof template:el-field
             * @private
             * @description Order of blocks output in display mode
             * 
             * 1. label
             * 2. main
             */
            var displayBlockOrder = [
                "label",
                "main"
            ];
            var blockMessage = {
                error: true,
                warning: true,
                info: true
            };

            var args = Array.prototype.slice.call(arguments);
            var options = args[args.length-1];
            var hash = options.hash;
            hash = _.extend({}, hash.attributes, hash);
            delete hash.attributes;

            var inputType;

            hash = marshallNamespacedAttributes("input", hash);
            hash = marshallNamespacedAttributes("help", hash);
            hash = marshallNamespacedAttributes("error", hash);
            hash = marshallNamespacedAttributes("label", hash);
            hash = marshallNamespacedAttributes("field", hash);

            function getAttr (name) {
                var attr = hash[name];
                delete hash[name];
                return attr;
            }
            function getFieldAttr (name) {
                var attr = hash[name];
                delete hash[name];
                if (attr === undefined) {
                    attr = field[name];
                }
                delete field[name];
                return attr;
            }
            var field = getAttr("field");
            var input = getFieldAttr("input");
            var inputs = getFieldAttr("inputs");
            var labels = getFieldAttr("labels");
            var checked = getFieldAttr("checked");
            var inputsDisabled = getFieldAttr("inputs-disabled");
            var phraseKey = getFieldAttr("phrase-key");
            /*var error = getFieldAttr("error");
            var warning = getFieldAttr("warning");
            var info = getFieldAttr("info");*/

            var model = getFieldAttr("model") || this.model;
            var edit = getFieldAttr("edit");
            var display = getFieldAttr("display");
            // by default, editable
            // edit beats display beats edit on view
            var thisEditable = this.edit === undefined || this.edit;
            if (display === undefined) {
                display = !thisEditable;
            }
            if (edit === undefined) {
                edit = !display;
            }
            var controlType = edit ? "edit" : "display";

            if (field.type) {
                hash.type = hash.type || field.type;
                delete field.type;
            }
            if (hash.type) {
                inputType = hash.type;
                delete hash.type;
            }
            if (hash.name) {
                input.name = hash.name;
                delete hash.name;
            }

            var fieldset = (inputs || labels) ? true : false;

            if (model && input.name) {
                var fieldName = input.name;
                if (!phraseKey && model.phraseKey) {
                    var modelKey = typeof model.phraseKey === "function" ? model.phraseKey() : model.phraseKey;
                    phraseKey = modelKey + "." + fieldName;
                }
                if (model.schema) {
                    var schema = model.schema;
                    var fieldSchema = schema.properties[fieldName];
                    var fieldArray;
                    if (fieldSchema.type === "array") {
                        fieldArray = true;
                        fieldSchema = fieldSchema.items;
                    }
                    var fieldOptions;
                    if (fieldSchema["enum"]) {
                        fieldOptions = _.extend([], fieldSchema["enum"]);
                        var fieldLabels = [];
                        for (var fieldOpt in fieldOptions) {
                            var fieldOptValue = fieldOptions[fieldOpt];
                            var fieldOptLabel = Phrase.get(phraseKey + ".value." + fieldOptValue + ".label");
                            fieldOptLabel = fieldOptLabel ? fieldOptLabel.toString() : fieldOptValue;
                            fieldLabels.push(fieldOptLabel);
                        }
                        if (inputType === "text") {
                            inputType = fieldArray ? "checkbox" : "radio";
                        }
                        if (inputType === "select") {
                            if (_.isEmpty(input.options) && _.isEmpty(hash.options)) {
                                input.options = fieldLabels;
                                input.values = fieldOptions;
                            }
                        } else {
                            fieldset = true;
                            if (_.isEmpty(inputs)) {
                                inputs = fieldOptions;
                                labels = fieldLabels;
                            }
                        }
                    }
                }
                var modelValue = model.get(fieldName);
                if (modelValue !== undefined) {
                    if (fieldset) {
                        if (checked === undefined) {
                            checked = modelValue;
                        }
                    } else {
                        if (input.value === undefined && inputType !== "checkbox") {
                            input.value = modelValue;
                        }
                    }
                }
                if (inputType === "checkbox") {
                    if (input.value === undefined) {
                        input.value = true;
                    }
                     if (input.checked === undefined) {
                        if (modelValue === input.value) {
                            input.checked = true;
                        }
                    }
                }
                if (inputType === "password" && input.value) {
                    input.placeholder = input.value.replace(/./g, "*");
                    if (edit) {
                        delete input.value;
                    } else {
                        input.value = input.placeholder;
                    }
                }
            }


            for (var block in blockList) {
                var blockKey = blockList[block];
                var blockVal = getFieldAttr(blockKey);
                if (phraseKey) {
                    if (blockVal === undefined || (_.isObject(blockVal) && !_.isArray(blockVal) && !("content" in blockVal))) {
                        blockVal = Phrase.get(phraseKey+"." + blockKey, {display:display, edit:edit});
                        if (blockVal) {
                            blockVal = {
                                content: blockVal.toString()
                            };
                        }
                    }
                }

                if (!(_.isEmpty(blockVal))) {
                    blocks[blockKey] = blockVal;
                }
            }

            var label = blocks.label || {};
            delete blocks.label;
            var legend = blocks.legend;
            delete blocks.legend;

            for (var blockType in blocks) {
                var blockIn = blocks[blockType];
                if (blockMessage[blockType]) {
                    blocks[blockType] = Handlebars.helpers["el-message"](input.name, blockType, blockIn);
                } else {
                    blockIn["class"] = "control-" + blockType;
                    if (!blockIn.content.match(/^<(p|div|ul|ol)[ >]/)) {
                        blockIn["el-wrap"] = "p";
                    }
                    var attr= {
                        hash: {
                            attributes: blockIn
                        }
                    };
                    blocks[blockType] = Handlebars.helpers.el(attr);
                }
            }

            if (hash["class"]) {
                field["class"] = hash["class"];
                delete hash["class"];
            }
            if (!field["class"]) {
                field["class"] = [];
            }
            if (! _.isArray(field["class"])) {
                field["class"] = [field["class"]];
            }
            field["class"].push("control");
            if (input.name) {
                field["class"].push("control-" + input.name);
            }
            // could do this with defaults in marshallNamespaceAttributes
            if (field["el-escape"] === undefined) {
                field["el-escape"] = false;
            }

            field.content = "";
            if (options.fn) {
                field.content = options.fn(this);
                delete options.fn;
            } else {
                if (hash.id) {
                    input.id = hash.id;
                }
                if (!input.id) {
                    input.id = "input-" + input.name;
                }

                if (fieldset) {
                    inputType = inputType || "radio";

                    var fieldsetContent = "";

                    if (!inputs) {
                        inputs = labels;
                    }
                    checked = normaliseParamArray(checked);
                    inputsDisabled = normaliseParamArray(inputsDisabled);

                    var name = input.name;
                    for (var i in inputs) {
                        var inputArgs = inputs[i];
                        var labelArgs = {};
                        if (typeof inputArgs !== "object") {
                            inputArgs = {
                                value: inputArgs
                            };
                        }
                        inputArgs.name = inputArgs.name || name;
                        inputArgs.id = inputArgs.id || inputArgs.name + "-" + i;

                        if (labels && labels[i] !== undefined) {
                            labelArgs.content = labels[i];
                        } else {
                            labelArgs.content = inputArgs.value;
                        }
                        labelArgs["for"] = inputArgs.id;

                        inputArgs.value = inputArgs.value.toString();
                        if (checked !== undefined) {
                            inputArgs.checked = _.intersection(checked, [inputArgs.value]).length > 0;
                        }

                        if (inputsDisabled) {
                            inputArgs.disabled = _.intersection(inputsDisabled, [inputArgs.value]).length > 0;
                        }

                        inputs[i] = [inputArgs, labelArgs];

                    }
                    for (var item in inputs) {
                        var fieldsetItemOutput = "";
                        var itemInput = inputs[item][0];
                        itemInput["el-tag"] = "input";
                        fieldsetItemOutput += Handlebars.helpers["el-"+inputType]({hash:itemInput});
                        var itemLabel = inputs[item][1];
                        itemLabel["el-tag"] = "label";
                        fieldsetItemOutput += Handlebars.helpers.el({hash:itemLabel});
                        var fieldsetItemArgs = {
                            hash: {
                                "el-tag": "div",
                                "class": CONTROLCLASS[controlType],
                                "content": fieldsetItemOutput,
                                "el-escape": false
                            }
                        };
                        fieldsetContent += Handlebars.helpers.el(fieldsetItemArgs);
                    }
                    blocks.main = fieldsetContent;


                    if (legend) {
                        var fieldsetId = field.id || "control-group-" + input.name + "-label";
                        if (!field.role) {
                            field.role = "group";
                            field["aria-labelledby"] = fieldsetId;
                        }
                        var legendParams = {
                            hash: {
                                "el-tag": "p",
                                content: legend,
                                id: fieldsetId
                            }
                        };
                        legend = Handlebars.helpers.el(legendParams);
                        blocks.label = legend;
                    }
                } else {
                    inputType = inputType || "text";
                    if (!label["for"]) {
                        label["for"] = input.id;
                    }
                    if (!label.content) {
                        labelContent = input.name;
                        if (labelContent) {
                            labelContent = labelContent.substr(0,1).toUpperCase() + labelContent.substr(1);
                        }
                        label.content = labelContent;
                        // using name field.$name.label
                        // using model view.$model.$name.label
                    }
                    label.edit = edit;
                    var labelOutput = Handlebars.helpers["el-label"]({hash:label});
                    blocks.label = labelOutput;

                    hash.attributes = input;
                    hash["el-wrap-outer"] = {
                        "el-tag": "div",
                        "class": CONTROLCLASS[controlType]
                    };
                    hash["el-phrase-key"] = phraseKey;
                    hash["el-control"] = edit;
                    args[args.length-1].hash = hash;
                    //var inputOutput = Handlebars.helpers["el-"+inputType]({hash:input});
                    var inputOutput = Handlebars.helpers["el-"+inputType].apply(this, args);

                    blocks.main = inputOutput;
                    //field.content = labelOutput + inputOutput;
                }

            }

            var blockOrder = edit ? editBlockOrder : displayBlockOrder;
            for (var bk in blockOrder)
            if (blocks[blockOrder[bk]]) {
                field.content += blocks[blockOrder[bk]];
            }

            if (field["el-tag"] === undefined) {
                field["el-tag"] = "div";
            }
            var output = Handlebars.helpers.el({hash:{attributes:field}});
            return output;
        };

        for (var prop in helpers) {
            Handlebars.registerHelper(prop, helpers[prop]);
        }

    };

    return {
        /**
         * @method registerHelpers
         * @static
         * @param {object} hbars Handlebars instance
         * @description Register FormHelper helpers with Handlebars
         */
        //@param {string} [prefix=el] String to prefix helpers with
        registerHelpers: function (hbars, prefix) {
            if (!hbars.helpers.phrase) {
                Phrase.registerHelper(hbars);
            }
            if (!hbars.helpers.el) {
                ElHelper.registerHelper(hbars);
            }
            FormHelpers(hbars, prefix);
        }
    };
}));