0.0.9 • Published 8 years ago

nerve-templates v0.0.9

Weekly downloads
2
License
MIT
Repository
github
Last release
8 years ago

Nerve

Write HTML templates as Javascript data structures. Use CSS selectors to describe DOM elements in a hierarchical style like JSON.

WIP Warning: Nerve is a work in progress. Use at your own risk.

Give me

npm install nerve-templates

The nerve object is a simple object containing methods designed to be used together. It is designed to be mixed into another component or view library, but its API can also be used independently.

A browserified core.bundle.js file is supplied that can be loaded client-side.

The Nutshell

Take something like this, where the keys of an object literal are CSS selectors, and the values may be objects, arrays, or strings for interpolation or otherwise:

var template = { 'div.some-class-1': {
		'ul.my-list[data-attr="custom"]': [
			{
				li: 'Favorite Show: ~~foo~~'
			},
			{
				li: 'Other Favorite Show: ~~bar~~'
			}
		]
	}
}

Turn it into an HTML template we could hand to something like Underscore, assuming that double tildes mark variable interpolation:

var string = nerve.stringify.normalized( nerve.normalize( template ) );

// string outputs: '<div class="some-class-1"><ul class="my-list" data-attr="custom"><li>Favorite Show: ~~foo~~</li><li>Other Favorite Show: ~~bar~~</li></div>'

// using underscore's _.template
var render = _.template( string );
var html = render({
	foo: 'Bob\'s Burgers',
	bar: 'Archer'
});

// our stringified html:
<div class="some-class-1">
    <ul class="my-list" data-attr="custom">
        <li>Favorite Show: Bob's Burgers</li>
        <li>Other Favorite SHow: Archer</li>
    </ul>
</div>

Why?

Writing templates as Javascript lets us embed them in Components or Views without string concatenation or importing them as HTML files.

HTML syntax (XML) isn't fun to write by hand (/opinion).

CSS selectors represent DOM elements well in a terse syntax that is expressive and well understood by many developers and designers. JSON easily models the nested hierarchy of HTML/XML.

Expressing templates as Javascript opens up other possibilities:

  • APIs for templates. Mutate the template without touching the DOM.
  • Server side rendering.
  • No transpiling.
  • Swap in parsing modules for things that aren't HTML.

For science, and fun.

The Pain

{
	div#someId: 'fubar'
}

// Syntax Error!

We can get around the characters in CSS selectors that we can't write in the keys of an object literal by wrapping them in quotes, explicitly stating that the key is a string.

{
	'div#someId': 'fubar'
}

// <div id="someId">fubar</div>

We can't use the same key twice in an object literal. In an object like this, only the last key gets saved:

c = {
	li: 'foo',
	li: 'wat',
	li: 'bar'
}

// c.li outputs 'bar', the last key overrules any duplicates

We can use arrays to get around this if we need multiple elements with the same selector.

c = [
	{ li: 'foo' },
	{ li: 'bar' }
]

// <li>foo</li><li>bar</li>

#Nested Structures A test structure as complex as this:

testStructure = [{
	'#a.foo[data-val=1][data-val="a"]': {
		'#b.bar[data-val=2]': {
			'#c.baz.banksy[data-val=3]': 'hello World!'
		},

		'#d.bar[data-val=4]': {
			'#e.baz.banksy[data-val=5]': [{
					'div': 'blah'
				}, {
					'div': 'blah'
				}, {
					'div': 'blah'
				}, {
					'span': 'this is just a test template'
				}, {
					'div#amazing.my-other-class': 'a series of nested elements…'
				}, {
					'nav': {
						'span': 'this is a nested child'
					}
				}, {
					'div': 'blah'
				}
			]
		}
	}
}]

Will yield:

<div id="a" class="foo" data-val="1" data-val="a">
    <div id="b" class="bar" data-val="2">
        <div id="c" class="baz banksy" data-val="3">hello World!</div>
    </div>
    <div id="d" class="bar" data-val="4">
        <div id="e" class="baz banksy" data-val="5">
            <div>blah</div>
            <div>blah</div>
            <div>blah</div><span>this is just a test template</span>
            <div id="amazing" class="my-other-class">a series of nested elements…</div>
            <nav><span>this is a nested child</span></nav>
            <div>blah</div>
        </div>
    </div>
</div>

Inline Scripting

Templates need logic. What if we use functions to embed logic in our templates?

template = {
	'div.foo': function() {
		if (this.data) {
			return {
				span: this.data
			}
		} else {
			return {
				span: 'not sure'
			}
		}
	}
}

nerve.stringify.normalized( nerve.normalize(template) ); 

Outputs:

<div class="foo"><span>not sure</span></div>

The methods in the nerve object are designed to be mixed into a larger component/view library. Let's assume such a library. Give a Component instance where we want to render some state object:

var myTest = new Component({
	data: true,

	template: {
		'div#test': function() {
			if (this.data) {
				return {
					span: this.data
				}
			} else {
				return {
					span: 'nevermore'
				}
			}
		}
	}
});

myTest.stringify.normalized( myTest.normalize( myTest.template ) );

Should output as a string:

<div class="test">true</div>

Functions run during a template's normalization, but bind "this" to a parent instance. This allows the template to grab properties from the instance, or other variables in scope, by reference.

It also offers a workaround for dealing with context in nested objects.

var myTest = new Component({
	data: 'my nested datum',

	template: {
		div: {
			div: {
				div: function() {
					return this.data
				}
			}
		}
	}
});

myTest.stringify.normalized( myTest.normalize( myTest.template ) );

// <div><div><div>my nested datum</div></div></div>

The idea is give the developer the full power of Javascript within their templates. Logic gets run when a template is normalized. Since we stringify after that, we don't need underscore-style microtemplates or helper methods.

Interpolating Data in Keys

Let's still assume our psuedo-Component library. What if we want to render a custom attribute with some dynamic data property?

Assuming that we use double brackets for marking references for interpolation:

var myTest = new Component({
	data: {
		id: 'my-custom-id'
	},

	template: {
		'div#<<this.data.id>>': 'should have a unique id'
	}
});

myTest.stringify.normalized( myTest.normalize( myTest.template ) );

Should output as a string:

<div id="my-custom-id">should have a unique id</div>

This is particularly useful for rendering image "src" attributes and other potentially dynamic properties.

API

Core

nerve.normalize( templateStruct )

Parse a nested structure and return a more accessible version of itself that we can manipulate or stringify.

var templateStruct = {
	'div#hashTag.wow.much-css[data-has="aValue"]': 'something neat'
}

// outputs:
[
	{
		attrs: [
			{ attrKey: 'data-has' },
			{ attrVal: 'aValue' }
		],
		classes: [
			'wow', 'much-css'
		],
		id: 'hashTag',
		inner: 'something neat',
		tagName: 'div',
		type: 'html'
	}
]

Nested structures are recursively placed in the inner property, which is either an array or a string. The custom 'type' property is used for easier type checking. We can also use it to check for custom types like components.

nerve.stringify

Container for stringification methods.

nerve.stringify.normalized( normalizedStruct )

Takes a normalized structure (see above) and outputs the html equivalent. It relies on the rest of the stringification module, and is the main entrance point for stringifying a nested object.

nerve.parse, nerve.parse.css

Containers for parsing CSS selectors and other template schemes.

nerve.interpolate

Used to render in-context variable references in keys when demarked with double brackets (eg. <<this.data.foo>>). Custom syntaxes may be supported in the future.

Todos

Testability. Expose edge cases. Keep functions isolated and modularized. Parsing configuration / alternate syntaxes.

License

MIT

0.0.9

8 years ago

0.0.7

8 years ago

0.0.6

8 years ago

0.0.5

8 years ago

0.0.4

8 years ago

0.0.3

8 years ago

0.0.2

8 years ago

0.0.1

8 years ago