html-extender
v0.5.1
Published
JavaScript template engine that runs in the browser without the need for compilation. It's an extension chord for HTML.
Downloads
3
Maintainers
Readme
Deprecation
Deprecating this project in favor of already existing projects such as AlpineJS or Slim.js
HTMLExtender
An "extension chord" library for HTML templating.
Purpose
This is a vanilla JavaScript library that adds template abilities to HTML - including easy updating of element values and innerHTML, "when" conditions that hide/show elements, bind events, and set attributes and class names dynamically, all based on a passed in context. This is different than Angular in that values are not "bound" and do not automatically update. It's more like React in that the rendering occurs when the context is updated.
Important Note
Please be aware that this is not a 1.0.0 release, so the API is subject to change. The track to 1.0.0 is still undetermined, but will be described here when complete.
Contributing
Please submit issues and PRs if bugs are found, or to submit issues for discussion. As stated above, the goal is to make templating in HTML available using vanilla JavaScript. It should be able to work in any framework so long as Custom Elements and the other dependencies below are available.
Code of Conduct
Please be respectful and courteous to everyone. Discrimination, rude behavior, bigotry, etc will not be tolerated; especially from contributors and maintainers. Basically: just be a good person and cooperate!
Installation
npm install --save html-extender
This includes a browser ready version of JSON Query which is a dependency of the library. You may include different versions of this library, but HTMLExtender has only been tested with version 2.2.2 at the time of this writing.
Dependencies
- JSON Query v2.2.2 or greater
- Custom Elements API, Shadow DOM API
- Map, Set, Iterable, ES7+ Array and Object methods
- HTMLTemplateElement
Viewing the Docs
Documentation can be generated with ESDoc using the commands:
npm run docs:generate
npm run docs
It will start a server on port 9000 by default. Set the PORT
environment variable to change the port.
Usage
Syntax
There isn't any syntactical difference with the HTML, just some new attributes that can be added to any HTML element:
<div ext-when="shouldShow">
<span ext-model="path.to.value"></span>
<a ext-href="dynamic[0].href">Dynamic HREF!</a>
<button ext-bind="click:clickHandler">Click</button>
</div>
To render this, a new instance of HTMLExtender is created and used:
const extender = HTMLExtender.create(options); // defaults to searching the entire document.body
extender.update({
shouldShow() {
return true;
},
path: {
to: {
value: 'this value is rendered in the HTML of the span'
}
},
dynamic: [
{
href: '/some/link'
}
],
clickHandler(evt) {
// handle click event here...context is HTMLExtender
}
});
Attributes
ext-model=""
Updates the element's value or innerHTML property if the provided JSON query resolves in a value. Example:
<body>
<span ext-model="path.value">
<input type="text" ext-model="inputValue">
<script type="module">
import HTMLExtender from './HTMLExtender.js';
const extender = HTMLExtender.create();
extender.update({
path: {
value: "hello value"
},
inputValue: "some text"
});
console.log(document.querySelector('span').innerHTML); // prints "hello value"
console.log(document.querySelector('input').value); // prints "some text"
</script>
</body>
ext-bind="[eventName]:[JSONQuery] [, [eventName]:[JSONQuery], ...]"
Binds the defined event handlers to events. This is the only attribute that has special syntax. It's a space-delimited string of eventName:eventHandler
where the eventHandler is a JSON Query path that must result in a function. This is where the second option to the update
method is helpful:
<body>
<button ext-bind="click:..clickHandler mouseover:..path.to.overHandler">Click or hover!</button>
<script type="module">
import HTMLExtender from './HTMLExtender.js';
const extender = HTMLExtender.create();
extender.update({}, {
parent: {
clickHandler(evt) {
// called when button is clicked
},
path: {
to: {
overHandler(evt) {
// called when mouse hovers the button
}
}
}
}
});
</script>
</body>
The second option (as described in the API), is an options object that can take a parent
object. This allows the context to be separate from a component or some other instance. The ..
syntax tells JSON Query to look in the parent object instead of the main object.
ext-when="[JSONQuery]"
Adds or removes the ext-hidden
attribute based on the result of the method that the JSON Query returns. If the method returns truthy then the ext-hidden
attribute is removed, otherwise the attribute is added. Note that this does not hide or display the element; only the attribute is toggled so that the developer can decide the specific styling of elements with that attribute. The context and options that are passed to the update method are passed to the callback.
<body>
<div ext-when="conditionCallback">Click or hover!</div>
<script type="module">
import HTMLExtender from './HTMLExtender.js';
const extender = HTMLExtender.create();
extender.update({
conditionCallback(context, opts) {
return true; // causes the ext-hidden attribute to be removed from the element
}
}, {});
</script>
</body>
ext-classes="[JSONQuery]"
This attribute expects the resulting value from the JSON Query to be either an object
or a Map<string, boolean>
with the key being the name of the class and the value being a boolean. If true
, the class is added to the element, otherwise the class is removed. This was done to avoid an ugly attribute string and improve flexibility when dynamically selecting class names.
<body>
<div ext-classes="classlist">Style me!</div>
<script type="module">
import HTMLExtender from './HTMLExtender.js';
const extender = HTMLExtender.create();
extender.update({
classlist: {
btn: true,
'btn-primary': true,
'btn-error': false
}
}, {});
console.log(document.querySelector('div').classList); // prints 'btn btn-primary'
</script>
</body>
The syntax described for ext-attr
(below) also applies to ext-classes
.
ext-attr="[ATTR_NAME]:[JSONQuery];[,ATTR_NAME:JSONQuery]"
Other attributes can be dynamically set by using the ext-attr
syntax. For example, dynamically setting href
can be done by using ext-attr="attr:query"
:
<body>
<a ext-attr="href:dynamicHref">Link</div>
<script type="module">
import HTMLExtender from './HTMLExtender.js';
const extender = HTMLExtender.create();
extender.update({
dynamicHref: '/some/link'
});
</script>
</body>
Multiple attributes can be modified by separating each definition with a semicolon ;
: ext-attr="href:path;disabled:is.disabled"
This library also defines the element <ext-repeat>
which can be used to repeat a set of elements based on an iterable. Example:
<ext-repeat iterable="some.iterable">
<template>
<h2 ext-model="repeatKey"></h2>
<a ext-href="repeatValue" ext-model="..upperContext.value"></a>
</template>
</ext-repeat>
<script type="module">
import HTMLExtender from './HTMLExtender.js';
const extender = HTMLExtender.create();
extender.update({
some: {
iterable: new Map([
['key', 'value'],
['anotherKey', 'anotherValue'],
])
},
upperContext: {
value: 'hi'
}
});
</script>
A couple of important notes: this element will only repeat the content of the first <template>
tag that's found. This template must be a child of <ext-repeat>
. The elements that are created are given two new contexts: the main context is an object that contains repeatKey
and repeatValue
:
{
repeatKey: *,
repeatValue: *
}
This is the key/value from the iterable. The parent
context is the context that's passed to <ext-repeat>
; e.g. the context where the iterable is found. Under-the-hood, this element uses HTMLExtender to render each group of elements that are created.
NOTE: the generated elements are added to the parent element. These elements are created with a unique ID and removed when the repeater is next updated.
Attribute iterable="[JSONQuery]"
This is an optional attribute, if the count
attribute is provided. The JSON Query must resolve in an iterable: Set, Array, or Map. The only exception is Objects are allowed. When an object is recieved, Object.entries
is run and the repeater runs against that.
Attribute for="[ELEMENT ID]"
Sometimes it's desirable to have the generated elements be added to a different element. The for
attribute takes an id of another element and the generated elements will be placed there. This works in the same way that the for
attribute for the <label>
element works.
Attribute count="[Number|JSONQuery]"
This is an optional attribute, if the iterable
attribute is provided. If a number is provided, the template contents will be repeated that number of times. If a JSON query is provided, it should resolve to a Number that will be used to repeat the template contents.
Nested Elements
Nested elements are able to access ancestor element context objects by using the repeaters
property:
<ext-repeat iterable="someIterable" name="parentIterable">
<template>
<ext-repeat count="3">
<template>
<span ext-model="repeaters[1].repeatValue.value"></span>
</template>
</ext-repeat>
</template>
</ext-repeat>
<script>
const extender = HTMLExtender.create();
extender.update({
someIterable: [{
value: 'boom!'
}]
});
</script>
The spans will display the value
property from the first iteration (boom!
). The parent iterable can be accessed index value (1
in this case). Why is the index 1? The current context (0
) refers to the inner most <ext-repeat>
- in this case, the ext-repeat
with the count=3
attribute.
Note: Accessing parent iterables by name is not currently supported. This is due to the limitation of how JSON Query strictly treats Objects and Arrays. This is planned to be supported in the future.