xml

xml is a tagged template that parses HTML-like markup at runtime and produces a JSX-equivalent component — a compiler-less alternative to JSX. Inspired and influenced by @trusktr.

Templates are parsed as text/xml, so markup must be well-formed: void elements need a trailing slash (<br/>, <img src=""/>), every open tag must be closed, and attribute values must be quoted. Ill-formed input renders a visible parsererror element rather than throwing. Unlike the browser HTML parser, attributes, tags, and components are case sensitive, matching JSX.

xml is the global, shared instance. For a sandboxed instance with its own component registry, see XML().

Arguments

xml is a tagged template, so it is called as xml`markup` with ${...} interpolations rather than with positional arguments.

Member Type Description
${...} unknown An interpolated value — anything render accepts.
xml.define (components: Record<string, JSX.ElementType>) => void Registers components addressable by tag name (case sensitive).
xml.components Record<string, JSX.ElementType> The registry, seeded with the built-in components.

Returns: a function (a component) — or an array of them when the template has several root nodes — so xml composes with render, <Show/>, <Suspense/> and friends like any other component.

Predefined components

Every built-in component from pota/components is registered out of the box — For, Show, Switch, Match, Suspense, Dynamic, and the rest — so they can be referenced by tag name without calling xml.define. The lowercase/exception helpers load, customElement, and CustomElement are intentionally not registered.

Notes

  1. A children attribute applies only while the element has no childNodes. If the element has any children, the children attribute is ignored.
  2. xml.define is case sensitive.
  3. xml.define must run before the first xml`...` invocation that uses the new tag — templates compile once and cache their component-vs-element decisions, so a later define won't rebind an already-compiled template. An unregistered capitalized tag logs a warning; lowercase and hyphenated tags are assumed to be real elements and stay quiet.
  4. xml.define can override a name like div, making all div tags behave differently. This is a warning, not a recommendation.
  5. Defining a component on the exported xml makes it global — visible wherever xml is imported. To avoid polluting the global registry, create a local instance with XML().

Examples

Counter

A reactive template: pass the signal's reader {count.read} as a child, and wire native events with on:click.

import { signal, render } from 'pota'
import { xml } from 'pota/xml'

const count = signal(0)

const App = xml`
	<div>
		<p>count: ${count.read}</p>
		<button on:click="${() => count.update(n => n + 1)}">+</button>
		<button on:click="${() => count.write(0)}">reset</button>
	</div>
`

render(App)

Interpolation

A ${...} accepts anything render accepts — strings, numbers, arrays, other xml components, even JSX elements. Interpolated text is never parsed as markup, so a value like <JSX> stays literal text rather than becoming a tag.

import { render } from 'pota'
import { xml } from 'pota/xml'

const Bold = text => xml`<b>${text}</b>`

const App = xml`
	<div>
		${'Hola!'}
		${[4, 5, 6, Bold(' really')]}
		${Bold('whoa')}
		<u>${'Look mom no <JSX>'}</u>
		${(<div>:)</div>)}
	</div>
`

render(App)

Defining a global component

Registering a component on the exported xml makes it addressable by tag name everywhere xml is imported.

import { render } from 'pota'
import { xml } from 'pota/xml'

xml.define({ test: () => 'hello world!' })

const App = xml`<div><test/> div contents</div>`

render(App)

Using a built-in component

Built-in components like <For/> work by tag name without any registration.

import { render } from 'pota'
import { xml } from 'pota/xml'

const App = xml`<For each="${[1, 2, 3]}">${val => val * 2}</For>`

render(App)

Context flows through templates

context propagates through xml templates exactly as it does through JSX. Register the Provider (and any consumers) with xml.define, then nest them — the value resolves by tree position.

import { render, context } from 'pota'
import { XML } from 'pota/xml'

const xml = XML()

const Context = context({ myValue: 1 })

function Value() {
	return Context().myValue
}

xml.define({ Provider: Context.Provider, Value })

// renders "12321" (whitespace-only text between tags is dropped,
// following JSX whitespace rules)
const App = xml`
	<Value />
	<Provider value="${{ myValue: 2 }}">
		<Value />
		<Provider value="${{ myValue: 3 }}">
			<Value />
		</Provider>
		<Value />
	</Provider>
	<Value />
`

render(App)