whits
v1.4.1
Published
Write HTML in TypeScript
Downloads
13
Readme
whits
- Write HTML in TypeScript
whits
is a Node.js library that generates HTML code programmatically with all the advantages of TypeScript, such as
type-checking, autocompletion, decorators, etc. It provides a clean and concise way to create dynamic HTML templates,
with types that provide safeguards against generating invalid HTML.
Contents
whits
- Write HTML in TypeScript
Installation
npm i whits
Basic Usage
Import $
To use whits
in your TypeScript project, you can import the $
object.
import {$} from 'whits';
Creating tags
There are many ways to build out your HTML. The shortest, if the tag doesn't have an ID or class, is to use the
properties of the $
object, which correspond to all the valid HTML tags. You can also call $
as a function and
pass a selector to create tags with class
and/or id
attributes. There is no "best" way, except what is most
readable and convenient for you!
// Many ways of creating equivalent divs
const divs = [
// Use the individual tag factory, and pass the attribues as an object in the first argument
// Pass the content as the second argument
$.div({id: 'example', class: ['foo', 'bar']}, 'Hello, world!'),
// Create a factory by passing a CSS-style selector to the `$` function
// Then pass attributes as an object in the first argument, and content as the second argument
$('div')({id: 'example', class: ['foo', 'bar']}, 'Hello, world!'),
$('div#example')({class: ['foo', 'bar']}, 'Hello, world!'),
// If there are no attributes to add, pass the content as the first argument
$('div#example.foo.bar')('Hello, world!'),
// If there is no tag name passed to the `$` function, it will default to `div`
$('#example.foo.bar')('Hello, world!'),
$('#example')({class: ['foo', 'bar']}, 'Hello, world!'),
$('')({id: 'example', class: ['foo', 'bar']}, 'Hello, world!'),
];
// Each tag has an `html` getter that returns a string representation of the tag
// All of the examples above will have the same HTML output
console.log(
divs.every((example) => example.html === '<div class="foo bar" id="example">Hello, world!</div>')
);
// Output: true
Nesting basics
One of the essential capabilities is nesting of tags. This works in an intuitive way, just like in HTML.
// Children are passed to the factory in an array, after the attributes object if there is one
$.div([
$.h1('Example'),
$.p('Hello, world!')
]);
$.div({'data-foo': 'bar'}, [
$.h1('Example'),
$.p('Hello, world!')
]);
// Children can be a mix of tags, raw content (more on this later), and strings
$.div([
$.h1('Example'),
'Hello, world!',
$.p('This is a paragraph.')
]);
// A single child doesn't need to go in an array
$.div('Hello, world!');
$.div(raw('<p>Hello, world!</p>'));
$.div($.h1('Hello, world!'))
// If a child is an empty or void tag, the factory function itself can be passed without being called
$.div([
$.div, // Empty div
$.br, // Void tag <br>
$('i.fa.fa-star') // Empty tag with classes passed as a selector
])
// Nesting can go as deep as you need it to
$.main([
$('#container')([
$.section({'data-foo': 'bar'}, [
$.h1('Example'),
$.div([
$.p(['Hello,', $.b('world'), '!']),
])
])
])
]);
Nesting shortcut with compound tags
You can create compound tags by passing multiple selector strings to the $
function. A compound tag can be thought of as
a hierarchy of tags nested within each other. This serves as a shortcut when you need to stack tags together. When you
create a compound tag, any attributes or children you pass will be redirected into the innermost tag in the hierarchy.
Classes and IDs passed as part of the selector will still work as expected.
These 2 examples are equivalent:
$('header', 'nav#navigation', 'ul.navbar')({title: 'UL'}, [
$('li', 'a')({href: '/'}, 'Home'),
$('li', 'a')({href: '/page1'}, 'Page 1'),
]);
$.header(
$('nav#navigation')(
$('ul.navbar')({title: 'UL'}, [
$.li($.a({href: '/'}, 'Home')),
$.li($.a({href: '/page1'}, 'Page 1'))
])
)
);
They both output the same HTML:
<header><nav id="navigation"><ul class="navbar" title="UL"><li><a href="/">Home</a></li><li><a href="/page1">Page 1</a></li></ul></nav></header>
Interpolation 1.3.3+
Sometimes it is inconvenient or difficult to read an array of tag children, such as when there are Tag
instances and
other content placed within a paragraph of text. This is where the interpolation _
template literal function comes in
handy. Using this function, you can pass any valid Tag
children as expressions in a template literal.
These 2 examples are equivalent:
import {$, _} from 'whits';
const externalString = 'external string';
$.div([
'This could be a long chunk of text, with ', $.i('several'), ' ', $.span('tags'),
' and other content embedded, including this ', externalString, '.'
]);
$.div(_`
This could be a long chunk of text, with ${$.i('several')} ${$.span('tags')}
and other content embedded, including this ${externalString}.
`);
They both output the same HTML:
<div>This could be a long chunk of text, with <i>several</i> <span>tags</span> and other content embedded, including this external string.</div>
Strings and raw content
By default, strings are escaped automatically. You can import and use the raw
, comment
, css
, and javascript
template literal functions to pass unescaped content as children into a tag.
⚠ Use caution when calling these functions, as they are unfiltered and can be unsafe and error-prone.
raw()
- Insert raw, unescaped HTML content.
import {$, raw} from 'whits';
// Use as a template literal tag
$.div(
// This string is escaped automatically
'Hello, <world>!',
// But this one is not
raw`
<div>
<p>Raw HTML</p>
</div>
`
);
// Use as a standard function
$.div(raw('<p>Raw HTML</p>'));
comment()
- Insert an HTML comment
import {$, comment} from 'whits';
// Use as a template literal tag or a standard function
$.div(comment`This is a comment`);
$.div(comment('This is a comment'));
javascript()
- Insert raw javascript, automatically wrapped in a script
tag.
ⓘ The
es6-string-javascript
VSCode extension can be used to get syntax highlighting within the template literal.
import {$, javascript} from 'whits';
// Must be used as a template literal tag
$.head([
javascript`
const message = 'Hello, world!';
console.log(message);
`
]);
css()
- Insert raw CSS, automatically wrapped in a style
tag.
ⓘ The
es6-string-html
VSCode extension can be used to get syntax highlighting within the template literal.
import {$, css} from 'whits';
// Must be used as a template literal tag
$.head([
css`
body {
background-color: #000;
color: #fff;
}
`
]);
Whitespace
Whitespace within regular strings and template literals is truncated to a single space character, and there is no space
rendered between HTML tags. Therefore, if you want space in your output, you must add it manually via RawContent
instances or raw()
calls.
By default, the pre
and textarea
tags are immune from this whitespace truncation. The list of immune tags can be
changed globally by adding/removing items from the Tag.keepWhitespace
static property, which is a Map
instance
whose items are strings representing HTML & SVG tag names.
Whitespace truncation does not apply to RawContent
. It does apply to the
interpolation _
template literal function, but not any of the other
template literal functions.
⚠ This behavior has changed as of version
1.4.0
. Previous versions did not truncate whitespace.
import {$, raw} from 'whits';
// Add a full empty line between tags
$.div([
$.h1('Hello, world!'),
raw('\n\n'),
$.p('This is a paragraph.')
]);
// Add `a` to the list of tags that are immune from whitespace truncation
Tag.keepWhitespace.add('a');
// Now this will render the whitespace within the `a` tag
$.a(`
Line 1
Line 2
`);
Creating full HTML templates
You can create HTML template files by exporting instances of the Template
and RootTemplate
classes, which include
methods for rendering the content as a string. The Template
constructor allows passing the template content as a
Tag instance, raw content, a string, or an array of any of these. Alternatively, you can pass a function that returns
the content in any of those formats. The function accepts a params
object which you can use to pass variables from
your application code.
See the basic code example for details.
Generating static HTML
Generating static HTML files is also a simple process, which comes down to 3 steps:
- Name the files you want to compile
*.html.ts
. The compiler will ignore other files. - Set the default export of the template files. It can be any of the following:
// RootTemplate.render() output, passing a predefined params object export default (new RootTemplate<{foo: string}>(({foo}) => [$.div(foo)])).render({foo: 'bar'}); // A RootTemplate instance, receiving the params object from the command line export default new RootTemplate<{foo: string}>(({foo}) => [$.div(foo)]); // A RootTemplate instance with no params export default new RootTemplate([$.div('bar')]); // Any other string or instance of Template, RootTemplate, or RawContent is acceptable export default 'This string will be escaped.'; export default raw('<template>This is an HTML <b>template</b> partial.</template>');
- Run the
whits
cli, passing the input/output directories and (optional) params object as arguments.
See the static code example for details.
Using the CLI
The CLI is useful for generating static HTML from your templates. It can generate from your TypeScript source or the
compiled JavaScript if you've already run tsc
or similar.
Usage: whits [-w] [-e <extend>] <input> <output> [...params]
-w - watch for changes
-e - extend whits with a module that adds new tags
extend - path to a module that extends whits, relative to the input directory
input - path to the input directory
output - path to the output directory
params - JSON-formatted object of params to pass to the templates
# Build from compiled JS files, assuming they are in `dist` dir, output HTML to `html` dir
npx whits dist html
# Build and watch source TS files, assuming they are in `src` dir, output HTML to `out` dir
# Intermediate JS files will go into a `.whits-dist` dir, which can be deleted after
npx whits -w src out
# Assuming there is a `tsconfig.json` file, build according to that and output HTML to `out` dir
# Built JS files will go into the `outDir` specified in `tsconfig.json`
npx whits . out
# Same as previous example, but pass an object to the templates
npx whits . out '{"foo": "bar"}'
# Same as previous example, but also extend whits with a module called `baz`
npx whits -e baz . out '{"foo": "bar"}'
ⓘ Known issue: The
-w
feature is not perfect. It mostly works, but it may not behave as expected when adding or deleting files.
Using custom tags and attributes
In keeping with its inherent strictness, whits
forces you to use valid HTML5 tags and attributes. You may find,
however, that your project requires using non-standard tags or attributes. A quick workaround would obviously be to
wrap raw HTML strings in calls to the raw()
function. While this works for one-off tags here and there, doing it
excessively defeats the purpose of a strongly-typed templating system. This is where extend()
comes in.
Extending whits
It's super easy. One small file in your project can give you practically unlimited flexibility. You can call it
whatever you want, but we'll use extend-me.ts
for this example. Say you want to be able to add these elements:
<foo bar="baz" far="faz">foo</foo>
<boo id="boo">boo</boo>
<div invalid-prop="val">hello</div>
This is all you need:
// extend-me.ts
import {extend} from 'whits';
// Use an ambient module to modify the `whits` types
declare module 'whits' {
// Override the tag => attribute mapping
interface HtmlAttributeOverrides {
foo: 'bar' | 'far'; // Add a `foo` tag with `bar` and `far` attributes
boo: undefined; // Add a `boo` tag with only the global attributes
div: 'invalid-attr'; // Add `invalid-attr` as an attribute to div tags
}
}
// Call the extend function, passing only the new tags
// If you are only adding attributes to existing tags, you can skip this part
// This call technically doesn't have to be in the same file as the ambient module
extend('foo', 'boo');
// a-template.ts
import {$, Template} from 'whits';
// Make sure you import the extension module
import './extend-me.js'
// Export your template
export default new Template([
$.foo({bar: 'baz', far: 'faz'}, 'foo'),
$('boo#boo')('boo'),
$.div({'invalid-attr': 'val'}, 'hello')
]);
You can also add custom SVG tags and attributes, which works the same way. See the extend code example for details.
A note about importing the extension module
Since we are extending the global instance of whits
we only really need to import the module once per entrypoint. If
it has already been imported somewhere else in the app, import {$} from 'whits'
will re-import the already modified
instance. This means that if you are serving dynamic pages from a node
app, you can import the module in your app
init process and skip importing it into any of your template files.
If, however, you are building a static site, each *.html.ts
file is essentially its own entrypoint. There are two
options for this case:
- Import the module in each template where you need to use the custom tags.
- Use the
-e
command-line argument to specify a module (relative to the input path) to import globally:# This will import `src/extend-me.ts` globally npx whits -e extend-me src html # Also works to import the equivalent JS module if generating from pre-compiled code # This will import `dist/extend-me.js` globally npx whits -e extend-me dist html
Special thanks
A quick shout out to a few other folks:
- wooorm, for his amazingly useful tag name and attribute lists, which he has unwittingly provided as the basis for the type definitions in this project
- The Pug team, for lots of inspiration
- GitHub Copilot, for drastically cutting down the development time for this project
- You, for your support! ❤️
Contributing
Contributions to whits
are welcome! To contribute, please fork the repository and submit a pull request.
License
whits
is licensed under the MPL-2.0 License. See LICENSE for more information.
Future enhancements
- Advanced usage documentation