What is Template Syntax?
O! provides a template syntax using JavaScript’s tagged template literals that feels like writing JSX, but without requiring a build step or transpiler. The x` ` function converts HTML-like strings into virtual nodes.
import { x } from '@zserge/o';
// Instead of this:
const vnode = h('div', { className: 'container' },
h('h1', {}, 'Hello!'),
h('p', {}, 'Welcome')
);
// Write this:
const vnode = x`
<div className="container">
<h1>Hello!</h1>
<p>Welcome</p>
</div>
`;
The x` ` syntax is pure JavaScript (ES6 tagged templates) - no build tools needed!
Basic Syntax Rules
The template parser has specific rules that must be followed:
1. Attributes Must Be Double-Quoted
Constant attribute values must use double quotes:
// ✅ Correct
x`<div className="container" id="main"></div>`
// ❌ Wrong - single quotes not supported
x`<div className='container'></div>`
// ❌ Wrong - unquoted attributes not supported
x`<div className=container></div>`
Tags without children should be self-closed:
// ✅ Correct
x`<input type="text" />`
x`<img src="logo.png" />`
x`<br />`
// ✅ Also correct
x`<div></div>`
3. Single Top-Level Element
There must be exactly one root element:
// ✅ Correct - single root
x`
<div>
<h1>Title</h1>
<p>Content</p>
</div>
`
// ❌ Wrong - multiple roots
x`
<h1>Title</h1>
<p>Content</p>
`
Using Placeholders
Placeholders (${}) allow you to inject dynamic values. They can appear in three positions:
1. As Tag Names
Use a component variable as the element:
const MyComponent = (props) => x`<div>${props.text}</div>`;
// Component as a placeholder
const app = x`<${MyComponent} text="Hello" />`;
2. As Attribute Values
Inject dynamic values into attributes:
const className = 'active';
const onClick = () => console.log('Clicked!');
const isDisabled = false;
// No quotes needed for placeholders!
const button = x`
<button
className=${className}
onclick=${onClick}
disabled=${isDisabled}
>
Click Me
</button>
`;
Placeholder attribute values should NOT be quoted. The parser treats them differently from constant strings.
3. As Text Content
Insert text or child elements between tags:
const name = 'Alice';
const count = 42;
const greeting = x`
<div>
<h1>Hello, ${name}!</h1>
<p>Count: ${count}</p>
</div>
`;
4. As Child Components
Nest components and elements:
const Header = () => x`<h1>Title</h1>`;
const Footer = () => x`<p>Footer</p>`;
const Page = () => x`
<div>
${Header()}
<main>Content</main>
${Footer()}
</div>
`;
Real-World Example: Counter
From the counter.html example:
const Counter = ({ name = 'Counter', initialValue = 0 }) => {
const [value, setValue] = useState(initialValue);
return x`
<div className="counter">
<h1>${name}</h1>
<div>${value}</div>
<div className="row">
<button onclick=${() => setValue(value + 1)}>+</button>
<button onclick=${() => setValue(value - 1)}>-</button>
</div>
</div>
`;
};
Breakdown:
- Constant attributes:
className="counter" uses double quotes
- Dynamic text:
${name} and ${value} inject variables as text
- Event handlers:
onclick=${() => setValue(value + 1)} passes functions
- Nested structure: Proper hierarchy with one root
<div>
How It Works
The x` ` function is a tagged template literal that parses HTML strings:
// From o.mjs:49-128
export const x = (strings, ...fields) => {
// Stack of nested tags. Start with a fake top node.
const stack = [h()];
// Three distinct parser states:
const MODE_TEXT = 0; // Text between tags
const MODE_OPEN_TAG = 1; // Opening tag with attributes
const MODE_CLOSE_TAG = 2; // Closing tag
// ... parsing logic ...
return stack[0].c[0]; // Return first child (the root element)
};
The parser:
- Tokenizes the template string
- Switches between three states: text, open tag, close tag
- Builds a stack of virtual nodes
- Returns the root VNode
Template Syntax vs h()
Both approaches create the same VNode structure:
// Using template syntax
const withTemplate = x`
<div className="card">
<h1>Title</h1>
<p>Content</p>
</div>
`;
// Using h() (equivalent)
const withH = h('div', { className: 'card' },
h('h1', {}, 'Title'),
h('p', {}, 'Content')
);
// Both create the same VNode:
// {
// e: 'div',
// p: { className: 'card' },
// c: [
// { e: 'h1', p: {}, c: ['Title'] },
// { e: 'p', p: {}, c: ['Content'] }
// ]
// }
When to Use Each
Use x` ` When:
- You have complex nested HTML structure
- You want more readable, familiar HTML syntax
- You’re building UI with mostly static structure
const Card = ({ title, content }) => x`
<div className="card">
<div className="card-header">
<h2>${title}</h2>
</div>
<div className="card-body">
<p>${content}</p>
</div>
</div>
`;
Use h() When:
- You need to dynamically generate elements
- You’re mapping over arrays
- You want more programmatic control
const List = ({ items }) => {
return h('ul', {},
...items.map(item =>
h('li', { k: item.id }, item.text)
)
);
};
Mix Both:
You can combine both approaches:
const TodoList = ({ todos }) => x`
<div className="todo-list">
<h2>My Todos</h2>
<ul>
${todos.map(todo => x`
<li className=${todo.done ? 'done' : ''}>
${todo.text}
</li>
`)}
</ul>
</div>
`;
Common Patterns
Conditional Rendering
const UserGreeting = ({ user }) => x`
<div>
${user ? x`
<h1>Welcome back, ${user.name}!</h1>
` : x`
<h1>Please sign in</h1>
`}
</div>
`;
List Rendering
const ItemList = ({ items }) => x`
<ul>
${items.map(item => x`
<li>${item}</li>
`)}
</ul>
`;
Event Handlers
const Form = () => {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log('Submitted:', text);
};
return x`
<form onsubmit=${handleSubmit}>
<input
type="text"
value=${text}
oninput=${(e) => setText(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
`;
};
Component Composition
const Header = () => x`<header>Header</header>`;
const Footer = () => x`<footer>Footer</footer>`;
const Layout = ({ children }) => x`
<div className="layout">
<${Header} />
<main>${children}</main>
<${Footer} />
</div>
`;
SVG Support
Template syntax works with SVG elements using the xmlns attribute:
const Icon = () => x`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M12 2L2 7v10l10 5 10-5V7z" />
</svg>
`;
Limitations
- No fragments: Must have a single root element
- Quoted constants: Attribute values must be double-quoted (unless they’re placeholders)
- No spread operators: Can’t spread props directly in templates
- Basic parsing: Error messages may not be as helpful as JSX
These limitations are trade-offs for having zero build step and staying under 1KB!
Next Steps