nant
v0.1.2
Published
A 'not-another-templating-language' for javascripters
Downloads
4
Readme
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.