vanjs-htm
v1.2.0
Published
HTM with VanJS for JSX-like syntax in vanilla JavaScript using VanJS reactivity.
Readme
VanHTM
A flexible and lightweight (<900B gzipped minified) HTM integration for VanJS and optionally VanX, supporting control flow directives, automatic SVG namespace handling, and optional HTML entity decoding.
Here's a sample based on the simplified TODO App from VanJS.
Features
- Tagged Template HTML: Write JSX-like templates in plain JavaScript using HTM with VanJS, no build step required.
- Function Components: Create reusable component functions that work seamlessly with all VanHTM features including control flow directives.
- Control Flow Directives: Use
for:each,show:when, andportal:mountfor SolidJS style declarative rendering. You can also combineshow:whenwithfor:eachandportal:mountto conditionally render lists and portals. Note: VanX is required only for thefor:eachdirective. - Automatic SVG Support: SVG elements are automatically rendered with the correct namespace. Use the
vh:svgdirective for excluded or ambiguous elements. - Optional HTML Entity Decoding: Decode HTML entities in string children (requires a HTML entities library like entities, he, html-entities, etc.).
- TypeScript Support: VanHTM is written in TypeScript and provides full type definitions.
Usage
// Script tags for including van and vanX
// <script src="https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-latest.nomodule.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/vanjs-ext/dist/van-x.nomodule.min.js"></script>
// Script tags for including htm and vanHTM
// <script src="https://cdn.jsdelivr.net/npm/htm/dist/htm.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/vanjs-htm/dist/van-htm.js"></script>
// The imports below can be replaced by the script tags above for htm and vanHTM
import htm from 'htm';
import vanHTM from 'vanjs-htm';
// const { html, rmPortals } = vanHTM({ htm, van, vanX }); // This line and the one below are interchangeable
const { html, rmPortals } = vanHTM({ htm, van, vanX: { list: vanX.list } });
const el = html`
<div>
Hello,
<b>world</b>
!
</div>
`;
van.add(document.body, el);Local Sandbox
The repository includes a sandbox environment for experimenting with VanHTM locally. To run it:
npm install
npm run sandboxThis will start a local development server where you can explore and test VanHTM features.
Browser Builds
VanHTM provides several prebuilt bundles for browser usage, available via CDN (e.g., jsDelivr). You can choose the build that best fits your needs.
Build output structure:
dist/default builds.dist/withDecoding/builds that utilize HTML Entity Decoding (requires a HTML entities library like entities, he, html-entities, etc.).
Each directory contains:
van-htm.module.js(ESM, minified, ~870B gzipped)van-htm.js(IIFE/global, minified, ~880B gzipped)van-htm.cjs(CJS, minified)van-htm.dev.module.js(ESM, unminified)van-htm.dev.js(IIFE/global, unminified)
Function Components
VanHTM supports function components, allowing you to create reusable UI components that work seamlessly with all VanHTM features including control flow directives.
Basic Function Component
// Define a reusable component
const Card = (props, ...children) => html`
<div class="card">
<h3>${props.title}</h3>
<div class="card-content">${children.length ? children : 'No children provided'}</div>
</div>
`;
const el = html`
<${Card} title="Welcome">
<p>This is the card content</p>
<button>Click me</button>
<//>
`;
van.add(document.body, el);Control Flow Directives
for:each
Renders a list by looping over a reactive array or iterable. The value of for:each should be a reactive list (e.g., from vanX.reactive). The child function receives the current value, a deleter function, and the index/key.
Note: This directive requires VanX. If vanX is not provided to vanHTM() and you attempt to use for:each, an error will occur.
const items = vanX.reactive([1, 2, 3]);
van.add(
document.body,
html`
<ul for:each=${items}>
${(v, deleter, k) =>
html`
<li>${v}</li>
`}
</ul>
`
);See VanX docs: Reactive List for more details on the itemFunc parameter.
show:when
Conditionally renders content based on a boolean, a VanJS state, or a function. If the condition is falsy, the show:fallback value is rendered instead (can be a primitive, a state or a function if you need reactivity).
Note: Due to how HTM works, children are evaluated (eagerly) before the show:when condition is checked. For complex children or performance-sensitive code, consider using a function to defer evaluation:
// ❌ Expensive operation runs even when condition is false due to
html`<div show:when=${condition}>${expensiveOperation()}</div>`
// ✅ Use a function for complex/expensive children
html`<div show:when=${condition}>${expensiveOperation}</div>`
html`<div show:when=${condition}>${() => html`...complex children...`}</div>`
// ✅ Or use a conditional function directly
${() => condition ? html`<div>${expensiveOperation()}</div>` : ''}
${() => condition ? html`<div>...complex children...</div>` : ''}const visible = van.state(true);
const toggleButton = html`
<button onclick=${() => (visible.val = !visible.val)}>Toggle Visible</button>
`;
van.add(
document.body,
html`
<div>
${toggleButton}
<div
show:when=${visible}
show:fallback=${() =>
html`
<div><b>Fallback - ${visible}</b></div>
`}
>
Visible - ${visible}
</div>
</div>
`
);show:when: Accepts a boolean, a VanJS state, or a function returning a boolean.show:fallback: (Optional) Content to render when the condition is falsy. Can be a primitive, a state or a function if you need reactivity.
portal:mount
Renders the element into a different part of the DOM (a "portal"). The portal:mount attribute determines where the content is rendered. It can be:
- A DOM
Node - A CSS selector string (e.g.,
#modal-root)
Note: For
rmPortalsto work correctly, portals should only be the direct child of their parent element. Nesting portals deeper will preventrmPortalsfrom removing them properly.
Implementation Detail: VanHTM automatically adds a
p:idattribute to portaled elements for internal tracking. This attribute is used byrmPortalsto identify and remove the correct portal elements. You should not manually set or modify this attribute. See below for more information.
const portalTarget = document.getElementById('portal-target');
const containerWithPortal = html`
<div>
<div>Some content before</div>
<div portal:mount=${portalTarget}>Content to Portal</div>
<div>Some content after</div>
<button onclick=${() => rmPortals(containerWithPortal, portalTarget)}>Remove Portal</button>
</div>
`;
van.add(document.body, containerWithPortal);You can also use a selector:
const portalTargetId = '#portal-target';
const containerWithPortal = html`
<div>
<div>Some content before</div>
<div portal:mount=${portalTargetId}>Content to Portal</div>
<div>Some content after</div>
<button onclick=${() => rmPortals(containerWithPortal, portalTargetId)}>Remove Portal</button>
</div>
`;
van.add(document.body, containerWithPortal);Removing Portaled Elements
// Removes all portaled elements created from `parentContainer` that are mounted in `portalTarget`.
// If no portalTarget is specified, it defaults to document.body.
rmPortals(parentContainer, portalTarget?);Parameters:
parentContainer(Node): The container element that contains the portal placeholder commentsportalTarget(Element | string, optional): The target where portal content was mounted. Can be:- A DOM Element
- A CSS selector string (e.g.,
'#modal-root','.portal-container') - If omitted, defaults to
document.body
Examples:
// Remove portals mounted in a specific element
rmPortals(containerWithPortal, document.getElementById('modal-root'));
// Remove portals mounted using a CSS selector
rmPortals(containerWithPortal, '#modal-root');
// Remove portals mounted in document.body (default behavior)
rmPortals(containerWithPortal);
// Equivalent to:
rmPortals(containerWithPortal, document.body);Combining show:when with for:each and portal:mount
You can combine the show:when directive with for:each and portal:mount on the same element to conditionally render lists or portaled elements. If the show:when condition is falsy, neither the list nor the portal will be rendered, and the show:fallback (if provided) will be used instead.
Example: Conditionally render a list
const items = vanX.reactive([1, 2, 3]);
const showList = van.state(true);
van.add(
document.body,
html`
<button onclick=${() => (showList.val = !showList.val)}>Toggle List</button>
<button onclick=${() => items.push(Object.keys(items).length + 1)}>Add item</button>
<ul for:each=${items} show:when=${showList}>
${(v) =>
html`
<li>${v}</li>
`}
</ul>
`
);Example: Conditionally render a portal
const getTime = () => new Date().toLocaleTimeString();
const portalTarget = document.getElementById('portal-target');
const showPortal = van.state(true);
const time = van.state(getTime());
const intervalId = setInterval(() => {
time.val = getTime();
}, 1000);
const container = html`
<div>
<div portal:mount=${portalTarget} show:when=${showPortal}>Portaled Content ${time}</div>
<button onclick=${() => (showPortal.val = !showPortal.val)}>Toggle Portal</button>
</div>
`;
van.add(document.getElementById('main-content'), container);SVG Support
VanHTM automatically handles SVG elements by applying the correct namespace when rendering. This ensures that SVG elements work properly without any additional configuration.
Automatically Handled SVG Elements
The following SVG elements are automatically rendered with the SVG namespace:
Shapes: circle, ellipse, line, path, polygon, polyline, rect
Container elements: svg, g, defs, symbol, use
Gradient and pattern elements: linearGradient, radialGradient, stop, pattern
Text elements: text, textPath, tspan
Other common elements: clipPath, desc, filter, foreignObject, marker, mask
// Basic SVG with automatic namespace handling
const radius = van.state(30);
const basicSVG = html`
<svg width="100" height="100">
<circle cx="50" cy="50" r=${radius} fill="lightblue" stroke="darkblue" stroke-width="2" />
<text x="50" y="55" text-anchor="middle" fill="darkblue">SVG</text>
</svg>
<br />
<input type="range" min="10" max="45" value=${radius} oninput=${(e) => (radius.val = parseInt(e.target.value))} />
<span>Radius: ${radius}</span>
<br />
`;
van.add(document.body, basicSVG);
// Complex SVG with gradients and paths
const complexSVG = html`
<svg width="200" height="100">
<defs>
<linearGradient id="gradient">
<stop offset="0%" stop-color="#f00" />
<stop offset="100%" stop-color="#00f" />
</linearGradient>
</defs>
<rect x="10" y="10" width="180" height="80" fill="url(#gradient)" rx="10" />
<path d="M 50 50 L 150 50" stroke="white" stroke-width="3" />
</svg>
`;
van.add(document.body, complexSVG);Excluded Elements
To keep the bundle size small, some SVG elements are excluded from automatic namespace handling:
- Animation elements:
animate,animateMotion,animateTransform,set - Filter effect elements: All
fe*elements (e.g.,feGaussianBlur,feBlend,feColorMatrix, etc.) - Other elements:
metadata,mpath,switch,view
vh:svg Directive
For excluded elements or when you need explicit control, use the vh:svg directive to force SVG namespace:
// Animated SVG using vh:svg directive for excluded elements
const animatedCircle = html`
<svg width="200" height="200">
<circle cx="100" cy="100" r="40" fill="purple">
<!-- animate is excluded, so we need vh:svg -->
<animate vh:svg attributeName="r" values="40;60;40" dur="2s" repeatCount="indefinite" />
</circle>
</svg>
`;
van.add(document.body, animatedCircle);
// SVG with filter effects using vh:svg
const blurredRect = html`
<svg width="200" height="200">
<defs>
<filter id="blur">
<!-- feGaussianBlur is excluded, so we need vh:svg -->
<feGaussianBlur vh:svg in="SourceGraphic" stdDeviation="5" />
</filter>
</defs>
<rect x="50" y="50" width="100" height="100" fill="orange" filter="url(#blur)" />
</svg>
`;
van.add(document.body, blurredRect);Shared HTML/SVG Elements
Some elements exist in both HTML and SVG (a, script, style, title). These default to HTML namespace for compatibility:
// Using both HTML and SVG styles
const styledSVG = html`
<div>
<style>
.highlight {
fill: yellow;
}
</style>
<svg width="100" height="100">
<style vh:svg>
.svgtext {
font-size: 20px;
}
</style>
<circle cx="50" cy="50" r="40" class="highlight" />
<text x="50" y="55" text-anchor="middle" class="svgtext">Hi</text>
</svg>
</div>
`;
van.add(document.body, styledSVG);Optional HTML Entity Decoding
import { decode } from 'html-entities';
import vanHTM from 'vanjs-htm/withDecoding';
// const { html, rmPortals } = vanHTM({ htm, van, vanX, decode }); // This line and the one below are interchangeable
const { html, rmPortals } = vanHTM({ htm, van, vanX: { list: vanX.list }, decode });
// Example below
const el = html`
<div>
Hello,
<b>world</b>
! 😎
</div>
`;
van.add(document.body, el);API
vanHTM(options)
htm: Required in all builds. The HTM instance.van: Required in all builds. The VanJS instance.vanX: Required only for thefor:eachdirective. The VanJS Extension instance or an object that contains alistproperty set asvanX.list. If not provided andfor:eachis used, an error will occur.decode: Required in builds that include HTML Entity Decoding (vanjs-htm/withDecoding). The decode method from a HTML entities library like entities, he, html-entities, etc.
Returns:
html: The htm template tag.rmPortals(parentContainer: Node, portalTarget?: Element | string): Remove portaled elements created fromparentContainer. TheportalTargetparameter specifies where to look for the portal content:- Can be an Element or a CSS selector string
- Defaults to
document.bodyif not provided - Refer to the examples here.
Technical Details
Error Handling
- Invalid for:each Data: The
for:eachdirective relies on VanX'slistfunction. Refer to VanX documentation for error handling behavior with invalid reactive data. - Invalid Portal Selectors: If a CSS selector provided to
portal:mountdoesn't match any element, VanJS will throw an error when attempting to mount the portal content. - Missing Portal Targets: If
rmPortalsis called with an invalid selector or non-existent element, the function will silently return without performing any operations. - Missing VanX for for:each: If
vanXis not provided tovanHTM()and thefor:eachdirective is used, an error will occur.
HTM Caching Behavior
VanHTM explicitly disables HTM's template string caching mechanism by setting this[0] = 3 in the template processor. This ensures that each template evaluation creates fresh elements, which is necessary for proper VanJS reactivity and state management. Refer to HTM documentation on Caching for more information.
Portal Implementation
VanHTM automatically adds a p:id attribute to portaled elements for internal tracking. This attribute uses an auto-incrementing counter (format: p-${counter}) and is used by rmPortals to identify and remove the correct portal elements. You should not manually set or modify this attribute.
License
MIT
