str-html
is a fast and lightweight JavaScript UI library relying on
JavaScript's tagged template literals feature.
It basically allows writing HTML in JS (like JSX) without the need of a separate transpilation step (unlike JSX) nor of an heavy library, everything being handled natively by JavaScript's tagged template features:
import strHtml from "str-html";
// Create a simple HTMLElement by just writing its HTML content
const someInnerHtmlElement = strHtml`<p>Click Me!</p>`;
// `someInnerHtmlElement` is now a regular `HTMLElement` object:
someInnerHtmlElement.onclick = () => {
// Dynamically appending new elements
document.body.appendChild(strHtml`<p>Clicked!</p>`);
};
// We can also insert template expressions in that string
const myAttributeValue = "foo";
const someClassName = "some-class";
const someTextToEscape = "<script>alert();</script>"
const myElem = strHtml`<div class="my-class" attr2=${myAttributeValue}>
${someInnerHtmlElement}
Some example text.
<div>Inserted expressions are escaped: ${someTextToEscape}</div>
<span class="some-other-element ${someClassName}">Even ${"more"} text.</span>
</div>`;
document.body.appendChild(myElem);
// HTML body content now:
// <div class="my-class" attr2="foo">
// <p>Click Me!</p>
// Some example text.
// <div>Inserted expressions are escaped: <script>alert();</script></div>
// <span class="some-other-element some-class">Even more text.</span>
// </div>
Run that example in your browser
The goal of this library is to be a much lighter weight and faster solution for
projects where you wish for an HTML/JSX-like syntax without the need of a big
UI framework. With str-html
, the dependency size, API, heavy library logic and
complex library behaviors are all kept to a minimum allowing you to just write
your own UI in a readable manner while relying on known native HTML concepts.
Note that the template string expression's placements are properly checked by
str-html
and sanitized by relying on the browser's API, so security worries
can be kept to a minimum - at least like with regular JSX and the JS HTML API.
You can install str-html
from npm/yarn:
npm install str-html
Or just copy-paste the ./main.mjs
file in this repository's root in your
project.
You can then just write HTML directly as a template string, prefixing it with
str-html
's main export:
import strHtml from "str-html";
// Simply declare an element by writing its HTML.
const myElement = strHtml`<div class="my-div"><p>Some paragraph</p></div>`;
// `myElement` is now a regular `HTMLElement`. Let's add it to the DOM.
document.body.appendChild(myElement);
// You can do any usual task with `HTMLElement`s, let's register an onclick
// handler:
myElement.onclick = function (evt) {
// do things with it
console.log("Received click event", evt);
};
// We can also dynamically update its text content like usual
myElement.textContent = "Some other text";
str-html
also allows to insert attributes and the inner content of elements
through expressions:
import strHtml from "str-html";
// Attribute values and element inner contents can be declared as
// "${expression}" in which case they will be properly sanitized.
//
// You can also put newline/whitespaces where authorized by HTML.
const className = 'class1 class2 classWithAQuote"3';
const textContent = "Some text with <> characters";
const anotherElement = strHtml`<div class="some-outer-element">
<div class=${className}>
${textContent}
</div>
</div>`;
// You can insert an element in another element like this
const myElement = strHtml`<div class="some-class">Some initial text</div>`;
const parentElt1 = strHtml`<div class="parent-element">${myElement}</div>`;
// You can also concatenate multiple values by relying on an array
const parentElt2 = strHtml`<div class="parent-element">
${[myElement, anotherElement]}
You can even add some text before of after or in the array, as you wish
</div>`;
// That's it, you know how to use it now!
The ultimate goal of this library is to be very simple to understand. No magic (well, tagged template literals are kind of magic, but you get my point), and all HTML rules applies for the rest of your application.
The only rules to understand with this library are that in str-html
template
litterals:
-
At least for now, CDATA sections, HTML comments and DOCTYPE HTML features are not supported. If any of the unauthorized elements is detected,
str-html
will throw.If you don't know what some or all of those are, you probably don't use nor need them.
-
An
${expression}
:-
Can only be set as an attribute value and inside an element's content.
This is enforced by
str-html
, which will throw if an expression is found elsewhere.Note that you can put in both of those places multiple ${expression} and strings concatenated and interpolated, in which case it will behave like you may suspect: concatenated and interpolated.
Example:
console.log( strHtml`<div class="${"class1 "}class2 class3${" class4"}" />` .outerHTML ); // Outputs: "<div class="class1 class2 class3 class4"></div>"
-
Will behave differently depending on the type found in the expression:
-
null
andundefined
will be translated to an empty string. -
HTMLElement
, will be appended as a child element when set as another element's inner content. -
str-html
will throw if anHTMLElement
is set as an attribute's value (why would you do that?). -
any other values but an array (see below) will have its
toString
method called on it and its result will be first sanitized then used. -
An array. Same rules than for the corresponding single values, but repeated for each element of that array. From the first to the last item of that array. The obtained results are then concatenated.
-
-
That's it!
Some elements, like <br>
or <input>
cannot have an inner content as per the
HTML spec and do not need/have a closing tag, those are called
"void elements" in HTML linguo.
In str-html
they have been handled as is the more logical for library users:
-
The HTML-compliant way of declaring one without closing it is supported, you can picture it as if a closing tag was automatically added just after its opening tag.
-
If you put content inside a void element, it will be actually added to its parent element, exactly like if you did the same thing on a browser with the
innerHTML
API (at least on browsers I checked). -
Closed void elements are just skipped
This should lead to a sensible way of handling them regardless of your
preferences. For example, with <br>
, any of those will be parsed the same
way:
strHtml`<p>first line<br>second line</p>`; // HTML way
strHtml`<p>first line<br/>second line</p>`; // Self-closing way
strHtml`<p>first line<br></br>second line</p>`; // Second closing tag
strHtml`<p>first line<br>second line</br></p>`; // User may have not understood `<br>`
It should be pretty fast and faster than most heavier UI libraries. However I have no idea how much as I wasn't bothered yet to bench it.
It does has some overhead over native browser APIs due to the string parsing
involved.
For places where you really need the toppest performances, a huge advantage of
str-html
is that, because it just outputs an HTMLElement
and can read them,
it is possible to combine it with the fastest framework available: native
browser APIs!
For example, you can do:
// Declare 1000 HTMLElement with native browser API
const elements = [];
for (let i = 0; i < 1000; i++) {
const element = document.createElement("div");
element.className = "some-class-name";
element.textContent = `Some text content for ${i}`;
elements.push(element);
}
// Include them in a `str-html` template to produce a parent HTMLElement in a
// more readable way.
const parentElt = strHtml`<div class="parent-elt">${elements}</div>`;
// And the other way arround, include `parentElt` in a element created through
// native browser API
const wrapperElt = document.createElement("div");
wrapperElt.appendChild(parentElt);
Again, maybe it turns out that str-html
is already very fast for your need,
it is for mine at least.
You don't, that's what's beautiful!
HTMLElement
produced by str-html
are not dynamic: they produce an
HTMLElement
which corresponds to the values declared in it at this instant,
you can just picture it like a regular JS template literal.
If you ever want to update one of this HTMLElement's content, just use regular browser API:
let counter = 0;
// Put the current value of counter in an element
const counterElt = strHtml`<div>${counter}</div>`;
counter++; // we increment the counter, now set to `1`
// `counterElt` still contains `0` right now, but to update it you can just do:
counterElt.textContent = counter;
Note that an HTMLElement
added through str-html
keeps the same original
reference:
let counter = 0;
const counterElt = strHtml`<div>${counter}</div>`;
// `parentElt` will contain `counterElt`
const parentElt = strHtml`<div>${counterElt}</div>`;
// Update `counterElt` even inside `parentElt`
counter++;
counterElt.textContent = counter;
Just rely on usual HTML event handlers:
const mySuperButton = strHtml`<button class="my-button">Click me</button>`;
mySuperButton.onClick = () => {
mySuperButton.textContent = "You clicked!";
alert("Great job!");
};
// Note that you can just target an inner element through the usual HTML API
const myElementWithAButtonInside = strHtml`<div>
<button class="inner-button">Click me</button>`
<div class="some-other-huge-inner-div-I-dont-want-to-listen-to-for-clicks" />
</div>`;
const innerButtonElt = myElementWithAButtonInside
.getElementByClassName("inner-button")[0];
innerButtonElt.onClick = () => alert("Done!");
Technically, it wouldn't be hard to also allow it as element names and attribute names, or even as a collection of attribute names and values, but I found that it complexified too much the API for something I don't really need.
My opinion may change in the future if I need this.
After some initial considerations, I think HTML custom elements already answer that need, so you might consider them.
Though, str-html
is not React, if you really need custom component, dynamic
updates and complex state management, you may want to look-up for another more
complete solution.