div

Template engine for node.js and browsers

npm install div
30 downloads in the last week
60 downloads in the last month

div()

Javasctipt template engine for node.js and browsers.

var template =
html(
    head(
        title('Page about $subject'),
        script({type: 'text/javascript', src: 'somescript.js'})
    ),
    body(
        h1('$subject'),
        ul({$: 'each links'},
            li({$: 'if url'},
                a({href: '$url'}, '$title'),
                span('$title')
            )
        )
        div({class: 'footer'}, 'copyright sign')
    )
);

Features

  • Work with strings on server and DOM elements on client.
  • Load templates like .js file. There is no need to use require('text!template.html) or fs.readFile('template.html') and then parse it, just require('template.js').
  • Template blocks: each, if, safe (string), with and select (for partials).
  • Controllers can be attached to templates. Templates can be attached to controllers to be almost like Backbone views.

Now div() is very raw, but all examples work.

Using

with node.js

npm install div
var _div = require('div');

in browser (needs jQuery)

<script type="text/javascript" src="div.js"></script>
<script type="text/javascript">
    var _div = window._div;
</script>

Markup

Define tag names in module's global scope.

var get = _div.get;
var div = get('div'),
    a = get('a'),
    span = get('span');

Make template with tags like tag({key: 'value'}, node, 'just text', node ...).

div({class: 'soft', id: 'bob'}, p('text in p'), span('in span'), 'just text', p('agian'));

Read context using $key to read by key and $this to read without key.

div('array $this has length $length').print([1, 2, 3]);

Add $: 'block_name arg1 arg2 ...' pair to attributes to use blocks.

div({$: 'each items'}, div('$title'));
div({$: 'if path "/"'},
    span('$title'),
    a({href: '$path'}, '$title')
);

Get result string.

template.print(context);

Render the result to DOM element.

var el = document.createElement('div');
template.render_to(el, context);

Blocks

each

ul({$: 'each numbers'}, li('$n')).print({numbers: [{n: 1}, {n: 2}, {n: 3}]});
ul({$: 'each'}, li('$this')).print([1, 2, 3]);
<!-- result for both examples -->
<ul data-each="2">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

with

var context = {
    a: 1, b: 2,
    c: {a: 111, b: 222}
};

div('$a and $b').print(context); // <div>1 and 2</div>
div({$: 'with c', '$a and $b').print(context); // <div>111 and 222</div>

// or just use needed key without 'with'
// in both two examples result will be the same
div({$: 'with smth'}, '$this');
div({$: 'smth'}, '$this');

safe

var str = '<a href="/">index</a>';

div('$str').print(str); // <div>&lt;a href=&quot;/&quot;&gt;index&lt;/a&gt;</div>
div({$: 'safe'}, '$str').print(str); // <div><a href="/">index</a>

if

Up to 2 arguments can be passed.

  • no arguments - will be evaluated the boolean value of context
  • one argument - the value will be given from context with key equal to arguement
  • two arguments - will get the value from context, eval the second argument and compare them using ===

If it's needed to compare whole context with value use this key. One or two nodes can be in if block.

p({$: 'if'}, 'truthy').print(); // <p data-if="false"></p>

// for next 4 examples result will be: <p data-if="true">truthy</p>
p({$: 'if'}, 'truthy').print(true);
p({$: 'if a'}, 'truthy').print({a: true});
p({$: 'if a 123'}, 'truthy').print({a: 123});
p({$: 'if this 123'}, 'truthy').print(123);

p({$: 'if a 123'}, 'truthy', 'falsy').print({a: 124}); // <p data-if="false">falsy</p>

If in attributes

p({'if a': 'enabled'}).print();
// <p></p>

p({'if a': 'enabled'}).print(true);
// <p a="enabled"></p>

p({'if a class': 'enabled'}).print();
// <p></p>

p({'if a class': 'enabled'}).print({a: true});
// <p class="enabled"></p>

p({'if a 123 class': 'enabled'}).print({a: 123});
// <p class="enabled"></p>

p({'if a 123 class': ['enabled', 'disabled']}).print({a: 122});
// <p class="disabled"></p>

select

var index = div('index');
var blog = div('blog');
var post = div('post');
var content = div({$: 'select'}, index, blog, post);

var context = {};
content.print(context);         // case 1
content.print(context, blog);    // case 2
<!-- case 1 -->
<div>
    <div>index</div>
    <div>blog</div>
    <div>post</div>
</div>

<!-- case 2 -->
<div data-select="1">
    <div>blog</div>
</div>

Select for layout and partials

//  small partials
var products_list = div({class: 'list'}, 'products list content here');
var products_item = div({class: 'item'}, 'detail info about product');
var index = div({class: 'index'}, 'index content');
var contacts = div({class: 'contacts'}, 'contacts content');

//  something like layout
var page = div(
    img({src: '/logo.png', class: 'logo'}),
    div({class: 'content', $: 'select'},
        index,
        div({class: 'products', $: 'select'},
            products_list,
            products_item
        ),
        contacts
    )
);

var context = {};

page.print(context, index);           // case 1
page.print(context, products_list);    // case 2
// or render_to(dom_element, context, index);
<!-- case 1 -->
<div>
    <img src="/logo.png" class="logo">
    <div class="content" data-select="0">
        <div class="index">index content</div>
    </div>
</div>

<!-- case 2 -->
<div>
    <img src="/logo.png" class="logo">
    <div class="content" data-select="1">
        <div class="products" data-select="0">
            <div class="list">products list content here</div>
        </div>
    </div>
</div>

Controller

var controller = require('div').controller;

var click_madness = controller.extend({
    handlers: {
        onclick: 'click',
        onclick_move: 'click span.move'
    },
    init: function () {
        controller.init.apply(this, arguments);
        this.click_counter = 0;
    },
    onclick: function (e) {
        this.click_counter += 1;
        this.render({i: this.click_counter});
    },
    onclick_move: function (e) {
        this.$el.css({marginLeft : '+=10px'});
    }
});

var template = div(
    div({ctrl: click_madness},
        span('I have been clicked $i times.'), ' ',
        span({class: 'move'}, 'Move me.')
    ),
    p({ctrl: click_madness}, 'me clicked $i times'),
    div({ctrl: click_madness, $: 'if i 2'},
        'i equals 2',
        'i not equals 2 its $i'
    )
);

var el = $(template.print({i: 0})).get(0);
template.set_el(el);

live example

Controller can be included into tags and will be printed as its 'tag' property prints.

var view = controller.extend({
    tag: div('im just template with handlers'),
    handlers: {
        onclick: 'click'
    },
    onclick: function () {
        this.$el.html('i have been clicked');
    }
});

var template = div(
    div('im div and view under me'),
    view
);

var el = $(template.print()).get(0);
template.set_el(el);

View

(maybe will be removed)

var view = require('div').view;

var one = view.extend({
    name: 'div',
    attrs: {class: 'field $type'},
    nodes: [
        label({for: '$id'}, 'name: '),
        input({id: '$id', type: 'text', value: '$value'})
    ],
    handlers: {
        upperkey: 'keypress input'
    },
    upperkey: function (e) {
        e.targer.value = e.target.value.toUpperCase();
    }
});
var template = div(
    div('im div and view undex me'),
    one
);

var el = $(template.print({
    type: 'name',
    id: Math.random().toString().substr(2),
    value: 'qwe'
})).get(0);

template.set_el(el);

Context preprocessing

There are some logic in application which do some work with Model data for correct representation in View, but its not exactly pure Model or View logic. For example its needed to edit some models attribute which is array and it presented in view like list of checkboxes. So value of model attribute and list of options will be enough for context.

var model = {
    attr: ['is', 'object', 'value']
};

//  distinct from all models 'attr'
var options = ['is', 'are', 'am', 'object', 'subject', 'value'];

//  sufficient data for context
var context = {
    value: model.attr,
    options: options
};

//  typical template for checkbox
var checkbox =
label(
    input({
        type: 'checkbox',
        value: '$value',
        'if checked checked': 'checked'
        // 'if $checked checked': 'checked' - will be fixed
    }),
    '$label'
);

//  template to edit attribute
var template =
fieldset(
    legend('Attribute for model'),
    ul({$: 'each options'}, li(checkbox))
);

//  but to render this template its needed context which looks like
var needed_context = {
    options: [
        {value: 'is', label: 'is', checked: true},
        {value: 'are', label: 'are', checked: false},
        {value: 'am', label: 'am', checked: false},
        {value: 'object', label: 'object', checked: true},
        {value: 'subject', label: 'subject', checked: false},
        {value: 'value', label: 'value', checked: true}
    ]
};

And where its better to make transformations from context to needed_context, in Model or View layer? Maybe context preprocessing will help.

var preproc = {
    unpack: function (context) {
        context.options = context.options.map(function (option) {
            return {
                value: option,
                label: option,
                checked: context.value.indexOf(option) !== -1
            };
        });
        return context;
    }
};
var template =
//  use template from previous code and add 'proc' attribute
fieldset({proc: preproc},
    legend('Attribute for model'),
    ul({$: 'each options'}, li(checkbox))
);
//  and use context from previous code
template.print(context);

output should be

<fieldset>
    <legend>Attribute for model</legend>
    <ul data-each="6">
        <li>
            <label>
                <input type="checkbox" value="is" checked="checked"/>is
            </label>
        </li>
        <li>
            <label>
                <input type="checkbox" value="are"/>are
            </label>
        </li>
        <li>
            <label>
                <input type="checkbox" value="am"/>am
            </label>
        </li>
        <li>
            <label>
                <input type="checkbox" value="object" checked="checked"/>object
            </label>
        </li>
        <li>
            <label>
                <input type="checkbox" value="subject"/>subject
            </label>
        </li>
        <li>
            <label>
                <input type="checkbox" value="value" checked="checked"/>value
            </label>
        </li>
    </ul>
</fieldset>
npm loves you