This package has been deprecated

Author message:

Moved to @mojule/templating

mojule-templating

0.1.27 • Public • Published

mojule-templating

Templating using HTML nodes rather than string templates. Heavily influenced by mustache, eg logicless and declarative. I had a need to build and manipulate templates at runtime and came up with this as an alternative to mustache. I initially considered parsing mustache templates into some kind of AST so that I could do these runtime building/manipulation tasks but decided that as I'm only dealing with HTML it would be easier to just design something more suited to this task, particularly as I already have a ton of code for dealing with trees.

Also, using HTML as the syntax for the templates rather than text with curly braces, you can embed the templates directly in an HTML <template> tag.

Quick start

Based on the examples in the mustache docs:

Template

<p>
  Hello <span data-text="name"></span>
</p>
<p>
  You have just won <span data-text="value"></span> dollars!
</p>
<p data-if="in_ca">
  Well, <span data-text="taxed_value"></span> dollars, after taxes.
</p>

Model

{
  "name": "Chris",
  "value": 10000,
  "taxed_value": 10000 - (10000 * 0.4),
  "in_ca": true
}

Output

<p>
  Hello <span>Chris</span>
</p>
<p>
  You have just won <span>10000</span> dollars!
</p>
<p>
  Well, <span>6000</span> dollars, after taxes.
</p>

If you don't want the extra noise in the output (eg the <span> around the name), you can use the non-standard <fragment data-text="name"></fragment> element instead, these will be removed post populating the template, see below for more details. The reason that we leave these elements in by default is that we will probably enable two way data binding in future.

Token types

Tokens are specified by including a data-<token type>="<property name>" attribute on the target HTML element with the value set to the name of the property on the model.

data-text

Replace the contents of the tag with the specified text from the model, or an empty string if the property does not exist on the model. Any instances of < in the model text are replaced with &lt.

Template

<p data-text="name"></p>
<p data-text="group"></p>
<p data-text="age"></p>

Model

{
  "name": "Nik",
  "group": "<b>Github</b>"
}

Output

<p>Nik</p>
<p>&lt;b>Github&lt;/b></p>
<p></p>

data-html

Replace the contents of the tag with the specified text from the model, or an empty string if the property does not exist on the model. HTML is not escaped, and is also parsed for additional tokens.

Template

<p data-html="name"></p>
<p data-html="group"></p>

Model

{
  "name": "Nik",
  "group": "<b>Github</b>"
}

Output

<p>Nik</p>
<p><b>Github</b></p>

data-if

If the property on the model evaluates to truthy, emit the block's contents. If not, omit them. Compare to data-not.

Template

<div data-if="name">
  <p>
    Your name is <span data-text="name"></span>
  </p>
</div>
<div data-if="age">
  <p>
    Your age is <span data-text="age"></span>
  </p>
</div>

Model

{
  "name": "Nik"
}

Output

<div>
  <p>
    Your name is Nik
  </p>
</div>
<div>
</div>

data-not

If the property on the model evaluates to falsy, emit the block's contents. If it's truthy, omit them. Compare to data-if.

Template

<div data-not="name">
  <p>
    A girl has no name.
  </p>
</div>

Model

{
  "name": ""
}

Output

<div>
  <p>
    A girl has no name.
  </p>
</div>

data-each

If the property exists and is an array, outputs the block for each array element, using the array element as the context for evaluating each block. Compare to data-empty.

Template

<ul data-each="people">
  <li data-text="name"></li>
</li>

Model

{
  "people": [
    { "name": "Jane" },
    { "name": "Bob" }
  ]
}

Output

<ul>
  <li>Jane</li>
  <li>Bob</li>
</ul>

data-empty

If the property exists and is an array but has no elements, emit this block

Template

<div data-empty="desires">
  <p>A girl has no desires</p>
</div>

Model

{
  "desires": []
}

Output

<div>
  <p>A girl has no desires</p>
</div>

data-context

Sets the current context to the property in the model with this name, and disallow searching upwards in the object tree.

Template

<div data-context="user">
  <p data-text="name"></p>
  <p data-text="email"></p>
</div>

Model

{
  "name": "Alice",
  "email": "alice@example.com",
  "user": { "email": "user@example.com" }
}

Normally, the missing name in user would cause a search up the tree, but by declaring data-context we isolate this property from the rest of the model, so no name is found:

Output

<div>
  <p></p>
  <p>user@example.com</p>
</div>

data-include

Includes the named template, then populate it from the model

Main template

<div data-each="users">
  <div data-include="user"></div>
</div>

user template

    <p data-text="name"></p>
    <p data-text="email"></p>

Model

{
  "users": [
    { "name": "Alice", "email": "alice@example.com" },
    { "name": "Jane", "email": "jane@example.com" }
  ]
}

Output

<div>
  <div>
    <p>Alice</p>
    <p>alice@example.com</p>
  </div>
  <div>
    <p>Jane</p>
    <p>jane@example.com</p>
  </div>
</div>

data-tag

Used for populating the tagName or attributes of an object.

Template

<h1 data-tag="header" data-removeme class="removeMe toggleMeOff">
  Hello <span data-text="name"></span>
</h1>

Model

The model value is an object or array of objects where the object properties are action names such as addClass, and the values are a single argument for that action, or an array of arguments.

{
  "header": [
    {
      "tagName": "h2",
      "attr": [ "id", "myHeader" ],
      "removeAttr": "data-removeme",
      "addClass": "primary-header",
      "removeClass": "removeMe",
      "toggleClass": "toggleMeOff"
    },
    {
      "toggleClass": "toggleMeOn"
    }
  ]
}

Output

<h2 id="myHeader" class="primary-header toggleMeOn">
  Hello <span data-text="name"></span>
</h1>

Action types

Note that the value can also be a single object rather than an array - we allow an array here because you might want to use an action more than once, as in the case of toggleClass above.

Single object in model
{
  "header": {
    "addClass": "primary-header",
    "etc...": ""
  }
}

The possible actions are:

tagName

A single string containing the tagName to use:

{
  "tagName": "h2"
}
attr

An array with two elements, the first is the attribute to set, the second is the attribute value:

{
  "attr": [ "id", "myHeader" ]
}
removeAttr

A single string with the attribute name to remove

{
  "removeAttr": "id"
}
addClass

A single string with the class to add:

{
  "addClass": "highlight"
}
removeClass

A single string with the class to remove:

{
  "removeClass": "highlight"
}
toggleClass

A single string with the class to toggle - that is, add it if it does not already have it, and remove it if it does

{
  "toggleClass": "shown"
}
clearAttrs

Remove all attributes from the tag

{
  "clearAttrs": true
}
clearAttrs

Remove all attributes from the tag

{
  "clearAttrs": true
}
clearClasses

Remove all classes from the tag

{
  "clearClasses": true
}
attributes

An object map of attribute names to values - removes all existing attributes, and replaces them from this map

{
  "attributes": {
    "id": "myHeader",
    "class": "highlight shown"
  }
}

TODO: this does remove and replace, right? Not extend existing?

The <fragment> element

This non-standard element wrapper is removed from the output after all the tokens have been populated. This way you can wrap your blocks with something that won't appear in the final output.

Template

<fragment data-text="name"></fragment>

Model

{
  "name": "Alice"
}

Output

Alice

Differences and similarity with mustache

We operates on tree nodes rather than strings, and use more token types.

We use the same scoping as mustache, eg search the model's object tree upwards from the current context until we find a variable matching the token name. Unlike mustache, you can override this behaviour by explicitly setting the context for a block using the data-context attribute. This is useful if you want to ensure you don't accidentally fill a missing value in with an unrelated value with the same name which exists higher up in the object tree.

Like mustache, a variable miss results in an empty string.

We use more token types, as although mustache does a similar job with less types, some of them are overloaded (eg do different things depending on the type of the model data). After writing thousands of mustache templates I found in practise that it can be hard to tell what a template does just by looking at it, you have to find the models that are being used to populate them and I decided I'd rather have more tokens that are more explicit in what they do to aid readability.

Mustache variable

Normal variable replacement, eg escapes < to &lt;.

{{something}}

mojule-templating:

<span data-text="something"></span>

Mustache variable, unescaped

Literal variable replacement, eg don't escape < in html strings.

{{{something}}}

mojule-templating:

<span data-html="something"></span>

Mustache section

Overloaded in mustache. You can't always tell by looking at the template if something in this instance is a boolean/truthy value, if this is an array section where the contents of the block are repeated, or if this is an object value and the context is to be set to that object, so we have a seperate token for each of these cases.

{{#something}}
  block text
{{/something}}

mojule-templating:

<div data-if="something">
  block text
</div>
 
<div data-each="something">
  block text
</div>
 
<div data-context="something">
  block text
</div>

Mustache inverted section

Overloaded, using the same rules as a section but where a boolean is falsey or an array is empty. Our equivalent tokens mirror those for sections, but there is no opposite for data-context as this is handled unambiguously by data-not.

{{^something}}
  block text
{{/something}}

mojule templating:

<div data-not="something">
  block text
</div>
 
<div data-empty="something">
  block text
</div>

Mustacle partial

Just an include, finds the template named something rather than a property on the model, and replaces the contents of the block with the contents of the included partial.

{{>something}}

mojule templating:

<div data-include="something"></div>

Future

Arrays

In mustache you can do something like this:

<ul>
  {{#people}}
  <li>{{.}}</li>
  {{/people}}
</ul>
{
  "people": [ "Jane", "Bob" ]
}

Could we allow this by introducing a data-current token?

<ul data-each="people">
  <li data-current></li>
</li>

Or data-text="."?

<ul data-each="people">
  <li data-text="."></li>
</li>

Or omitting a value defaults to the current context?

<ul data-each="people">
  <li data-text></li>
</li>

The above syntax would also allow a model to be something other than an object:

[ "Jane", "Bob" ]
<ul data-each>
  <li data-text></li>
</li>

Two way binding

You could use it to do two-way binding as well, with a combination of extra data attributes and mutation observers

The data- tokens are removed after they're populated, so you would have a couple of extra tokens on the block to find them later:

<p data-text="name" data-type="text" data-property="name"></p>

Then after populating it would be:

<p data-type="text" data-property="name">Nik</p>

Set up a mutation observer, if the text changes you can use the information in data-type and data-property to repopulate the model and perhaps post the model back to the server or do something else like that. You'd also probably want to find other tags that point to the same property and update those as well. You could also have something watching the model, or an api to update the model or whatever than posted back to the nodes when the model changes for full binding.

Package Sidebar

Install

npm i mojule-templating

Weekly Downloads

0

Version

0.1.27

License

MIT

Last publish

Collaborators

  • nrkn
  • andybell