nant

0.1.2 • Public • Published

nant lets you generate your html templates with plain javascript (browser and server).

Features

  • No need to learn another language, just old good javascript
  • Tags are simple POJOs (Plain Old Javascript Object) and can be manipulated with ease before stringification
  • Uses all language constructs and programming techniques to define your reusable building blocks
  • Attach your own methods to specific Tags using Mixins

Get started

On the Browser :

It's just one file - nant.js - to import in your page.

<html>
    <head>
        <script src="nant.js"></script> 
    </head>
    <body>
        <div id="placeholder">...</div>
        <script>
            var el = document.getElementById('placeholder');
            var ht = nant.ht;
            el.innerHTML = ht.p('this is as easy as writing javascript code').toString(); 
        </script> 
    </body>
</html>

On the server

npm install nant

Then

// get the Html Templates namespace
var ht = require('nant').ht;
var html = ht.p('this is as easy as writing javascript code').toString();

So what's this ? A new Templating Language ?

Obviously no; Instead javascript is the templating language.

Html tags are exposed as functions in the ht namespace and tag attributes are passed as arguments.

var html = ht.input({ type: 'text', name: 'myinput', required: 'true'}).toString();
console.log( html );
<input type="text" name="myinput" required>

Tag body is passed as an optional argument

var html = ht.div({ id: 'myid', class: 'myclass' }, 'String body').toString();
console.log( html );
<div class="myclass" id="myid" required>
    String body
</div>

You can pass nested tags as body (note you don'need to call toString() on nested tags)

ht.form({ id: 'myform', class: 'myclass' },
    ht.div(
        ht.label('My Input'),
        ht.input({ name: 'myinput', placeholder: 'My Input' })
    )
).toString();

If you pass a function as body, it will be called upon rendering (with the parent tag as parameter)

function myBody(tag) {
    return ht.p('Hello to ' + tag.name);
}
 
ht.form({ id: 'myform', class: 'myclass' }, 
    myBody
).toString();

if you need to embody multiples tags, simply list them in order

ht.form({ id: 'myform', class: 'myclass' }, 
    ht.label({ for: 'myinput' }, 'My input'),
    ht.input({ id:'myinput', name: 'myinput' }),
    'Help text',
    someFunction
).toString();

You can also group body tags in arrays

ht.form({ id: 'myform', class: 'myclass' },
    ht.h1('Form header'),
    [
        ht.label({ for: 'myinput' }, 'My input'),
        ht.input({ id:'myinput', name: 'myinput' })
    ],
    ht.button('Submit')
).toString();

css class attribute can be defined as an array

ht.input({ class: ['class1', 'class2'] })
<input class="class1 class2">

or a conditional object, only members with a truthy value will be picked

ht.input({ class: { class1: 1 < 2, class2: 1 > 2 })
<input class="class1">

you may also use array of both string/conditional object

ht.input({ class: [ 'myclass',  { class1: 1 < 2, class2: 1 > 2 } ])
<input class="myclass class1">

Tag manipulation

all methods of the ht namespace returns an objet of type Tag;

the Tag's prototype exposes a few methods; this is useful if you want to manipulate the tag object before calling .toString()

.attr( attrName [, attrValue ] )

get/set current attribute value

var div = ht.input({ name: 'myinput', class: 'myclass' });
 
div.attr('name');               // 'myinput'
div.attr('class');              // ['myclass']
div.attr('name', 'newName');    // div.attr('name') === 'newName'

note how class attribute was converted to an Array; internally all tags maintains an Array instance for class attribute

.attr( [ attrName1, attrName2, ...] )

When passed an array, returns an object with given attributes

var div = ht.input({ id:'myid', name: 'myname', placeholder: 'My Input' });
div.attr(['id', 'name']); // { id:'myid', name: 'myname' }

.attr( object )

if you pass it an object, tag's attributes will be extended with object's members

var div = ht.input({ name: 'myinput', class: 'myclass' });
 
div.attr({ id: 'myid', class: 'class1' });
div.attr('id'); // 'myid'
div.attr('class'); // ['myclass', 'class1']

.hasClass()

used to check if tag instance references a css class

var div = ht.input({ name: 'myinput', class: ['myclass',  { class1: 1 === 1, class2: 1 !== 1} ] });
 
div.hasClass('myclass');            // true
div.hasClass('class1');             // true
div.hasClass('class2');             // false
div.hasClass('myclass class1');     // true
div.hasClass(['myclass', 'class2']);     // false

.toggleClass()

is another familiar method to to toggle on/off css class references

var div = ht.input({ name: 'myinput', class: ['myclass',  { class1: 1 === 1, class2: 1 !== 1} ] });
 
div.toggleClass('myclass');                 
// div.hasClass('myclass') == false
 
div.toggleClass('class2', true);            
// div.hasClass('class2') == true
 
div.toggleClass('class1', true);           
// nothing changes as class1 is already enabled
 
div.toggleClass(['myclass', 'class2']);    
// div.hasClass('myclass') == true && div.hasClass('class2') == false

.match( selector )

Another useful method to check weather a tag matches the given selector; note only a small subset of css selectors is supported at the moment You can also supply a function (see example below) to perform tag matching

exemple

var divTag = ht.div({ id: 'mydiv', class: ['form', 'group', 'col'] });
var inputTag = ht.input({ id: 'myinput', class: ['control', 'col'] });
 
// tag name selectors
divTag.match('div');                  // true
inputTag.match('input');              // true
divTag.match('div, input');           // true
inputTag.match(['div', 'input']);     // true
 
// class names, ids
divTag.match('div.form');               // true
divTag.match('div.form.group');         // true
divTag.match('#mydiv');                 // true
divTag.match('div#mydiv.form.col');     // true
divTag.match('*.col');                  // true
inputTag.match('*.col');                // true
 
// uses custom matching method
divTag.match( function(t) {  return t.name === 'div' } );

.children( [ selector ] )

returns all direct children of the current tag; if provided, selector will be used to filter out the result

.find( selector )

returns all descendents (including non-direct children) matching the given selector


Mixins

Mixins allows attaching custom methods to selected tags

for example, suppose you have a data-model attribute you want to apply to all input tags

//First you define you mixin function
function dataModel(model) {
    return this.attr({ dataModel: model });
}
 
// Then you attach it to all <input/> tags
nant.mixin( 'input', dataModel );

therefore you can call input tags with the dataModel method

ht.input({ name: 'myinput' }).dataModel('mymodel');
<input name="myinput" data-model="mymodel">

the exact signature of the nant.mixin() method is

nant.mixin( selector, mixinFn, [mixinName] )

examples

function dataModel() { ... }
 
// define 'dataModel' methods on all <input/> and <select/> tags
nant.mixin( ['input', 'select'], dataModel );
function tagName(name) { 
    this.attr('name', name);
}
 
// define 'name' method on all tags
nant.mixin( '*', tagName, 'name' );
function cols(cols) { 
    this.attr({ class: 'col-sm-'+cols);
}
 
// define 'cols' method on all div tags with class 'form-group'
nant.mixin( function(tag) {
    return tag.name === 'div' && tag.hasClass('form-group');
}, cols );

Object attributes (aka angular/knockout/... users)

nant supports passing nested objects as attribute values, this is useful in some cases (if you're working with data-binding libs like angular or knockout)

ht.div({ 
    objAttr: { 
        strAttr: 'strAttr', 
        numAttr: 1, 
        nested: { 
            nestedStr: 'nest\'edStr', 
            nestedNum: 1 
        }
    }
})
<div obj-attr="{strAttr: 'strAttr', numAttr: 1, nested: {nestedStr: 'nest\'edStr', nestedNum: 1}}"></div>

Note also that camelCased tag attribute names are transformed to their dash-delimited countreparts (ie objAttr become obj-attr)

Sometimes, when working with data-binding libs, we have to pass expressions in the object attribute that will be evaluated later by the lib. observe this angular example

<p ng-class="{strike: !deleted, bold: some.isBold() }">...</p>

we can't write this object straight into our code because the expressions will be evaluateed directly

// Error, strike and bold members will get evaluated right now, probably raises an error if deleted or some aren't in the scope
ht.p({ ngClass: {strike: !deleted, bold: some.isBold() } })

instead use nant.uq(expr) to build an unquoted expression

// Correct , strike and bold members will get evaluated later
ht.p({ ngClass: {strike: nant.uq('!deleted'), bold: nant.uq('some.isBold()') } })

Defining Custom Tags

=======================

Simply, use nant.makeTag to make a tag builder function

// It's better to define custom tags in their own namespace
var ns = nant.ns = {};
// define your custom element inside the namespace
ns.myElement = nant.makeTag('MyElement', isVoid)
 
//Later you can use your tag function
var myHtml = ht.div(
    ns.myElement({ ...}, body )
)

If isVoid parameter is true, then any body provided to the tag function will be ignored and closing tag (</myelement>) will not be generated upon tag stringification


Tutorial: Bootstrap forms

the following exemple builds a twitter bootstrap form

ht.form({ class: 'form-horizontal', role: 'form' }, 
    ht.div({ class: 'form-group' },
        ht.label({ for:'email', class: ['col-sm-2', 'control-label']}, 'Email'),
        ht.div({ class: 'col-sm-10' },
            ht.input({ type: 'email', class: 'form-control', id: 'email', placeholder: 'Email' })
        )
    ),
    ht.div({ class: 'form-group' },
        ht.div({ class: 'col-sm-offset-2 col-sm-10' },
            ht.button({ type: 'submit', class: 'btn btn-default'}, 'Sign in')
        )
    )
);

As we are simply using javascript, we are free to structure our templating the way we want. So we can also write

var bt = nant.bt = {};
 
bt.horzForm = function horzForm() {
    var body = Array.prototype.slice.call(arguments);
    return ht.form({ class: 'form-horizontal', role: 'form' }, body );
}
 
bt.formGroup = function formGroup(input, label) {
    return ht.div({ class: 'form-group' },
        label,
        ht.div({ class: { 'col-sm-10': true, 'col-sm-offset-2': !label }}, input )
    )
}
 
var myHtml = bt.horzForm(
    bt.formGroup(
        ht.label({ for:'email', class: ['col-sm-2', 'control-label']}, 'Email'),
        ht.input({ type: 'email', class: 'form-control', id: 'email', placeholder: 'Email' })
    ),
    bt.formGroup(
        ht.button({ type: 'submit', class: 'btn btn-default'}, 'Sign in')
    )
)

Lets review the last example, w've reusable bootstrap tags to build form elements.

You may have noted that the grid's columns layout (all those col-sm-* classes) is hard coded inside the templates.

What if we want to move layout defintion (col-sm-* classes) outside ? we can make changes on form layout once and then apply it to all our form template.

// Form layout definition
var layout = {
    label: { class: 'col-sm-2' },
    input: { class: 'col-sm-10' },
    offset: { class: 'col-sm-offset-2' }
}
 
// Form group apply layout def to its input and label
bt.formGroup = function formGroup(input, label) {
    input = ht.div(input).attr(layout.input);
    if(label) {
        label = label.attr(layout.label);
    } else {
        input = input.attr(layout.offset);
    }
    return ht.div({ class: 'form-group' },
        label, input
    )
}
 
var myHtml = bt.horzForm(
    bt.formGroup(
        ht.input({ type: 'email', class: 'form-control', id: 'email', placeholder: 'Email' }),
        ht.label({ for:'email', class: 'control-label'}, 'Email')
        
    ),
    bt.formGroup(
        ht.button({ type: 'submit', class: 'btn btn-default'}, 'Sign in')
    )
)

See how we've decoupled form layout and form field définition.

We can do better by abstracting away more bootstrap grid concepts

function BtFormLayout(cols, media) {
    this.cols = cols;
    this.media = media;
    
    this.label = { class: 'col-'+ this.media + '-' + this.cols[0] };
    this.input = { class: 'col-'+ this.media + '-' + this.cols[1] };
    this.offset = { class: 'col-'+ this.media + '-offset-' + this.cols[0] };
}
 
var layout = new BtFormLayout([2,10], 'sm');
 
bt.formGroup = function formGroup(input, label, layout) {
    input = ht.div(input).mixin(layout.input);
    if(label) {
        label = label.attr(layout.label);
    } else {
        input = input.attr(layout.offset);
    }
    return ht.div({ class: 'form-group' },
        label, input
    )
}
 
var myHtml = bt.horzForm(
    bt.formGroup(
        ht.input({ type: 'email', class: 'form-control', id: 'email', placeholder: 'Email' }),
        ht.label({ for:'email', class: 'control-label'}, 'Email')
    ),
    bt.formGroup(
        ht.button({ type: 'submit', class: 'btn btn-default'}, 'Sign in')
    )
)

You can go ever further to acheive better reusability; Because you're in the javascript land, you can apply your favourite desgin patterns.

Package Sidebar

Install

npm i nant

Weekly Downloads

1

Version

0.1.2

License

MIT

Last publish

Collaborators

  • yelouafi