# Handlebars Editor
URL: /docs/web/components/handlebars-editor
A three-pane workbench for writing and rendering Handlebars templates — a JSON input pane, a template editor with IntelliSense, and a live output pane with HTML preview.
**Demo:**
```tsx
'use client';
import { useState } from 'react';
import {
HANDLEBARS_SAMPLES,
HandlebarsEditor,
type HandlebarsAIMessageContext,
type HandlebarsEditorOrientation,
type HandlebarsOutputMode
} from '@docyrus/ui/components/handlebars-editor';
import { Label } from '@docyrus/ui/primitives/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@docyrus/ui/primitives/ui/select';
import { Switch } from '@docyrus/ui/primitives/ui/switch';
import { Settings2 } from 'lucide-react';
/*
* Demo data with nested objects and an array of objects so the editor's
* autocomplete surfaces paths like `customer.address.city` and
* `orders.[0].product` — type `customer.` or open `{{#each orders}}…{{/each}}`
* to use them.
*/
const DEMO_INPUT = {
customer: {
id: 'CUS-4821',
name: 'Acme Corporation',
email: 'hello@acme.example',
isActive: true,
address: {
street: '100 Main St',
city: 'Springfield',
country: 'USA',
postalCode: '01234'
}
},
orders: [
{
id: 'ORD-1001',
product: 'Widget Pro',
quantity: 3,
price: 29.99,
tags: ['featured', 'sale']
},
{
id: 'ORD-1002',
product: 'Gadget Lite',
quantity: 1,
price: 79.5,
tags: ['new']
},
{
id: 'ORD-1003',
product: 'Sprocket',
quantity: 5,
price: 12.25,
tags: []
}
],
total: 261.97
};
const DEMO_TEMPLATE = `
Hello, {{customer.name}}!
Shipping to {{customer.address.city}}, {{customer.address.country}}.
{{#each orders}}
- {{product}} × {{quantity}} — \${{price}}
{{/each}}
Total: \${{total}}
`;
/** Canned demo replies — wire `onSendMessage` to a real backend in your app. */
async function demoAIReply(message: string, context: HandlebarsAIMessageContext): Promise {
await new Promise(resolve => setTimeout(resolve, 400));
return [
`_(Demo response)_ I'd help you with: **${message}**.`,
'',
`Your current template is \`${context.template || '(empty)'}\`.`,
'Wire `aiAssistant.onSendMessage` to your LLM to get real answers.'
].join('\n');
}
export function HandlebarsEditorDemo() {
const [showPanel, setShowPanel] = useState(false);
const [orientation, setOrientation] = useState
)}
);
}
```
## Installation
```bash
pnpm dlx @docyrus/cli add @docyrus/ui-handlebars-editor
```
**Dependencies:**
- [handlebars](https://www.npmjs.com/package/handlebars)
- [@uiw/react-codemirror](https://www.npmjs.com/package/@uiw/react-codemirror)
- [@uiw/codemirror-extensions-langs](https://www.npmjs.com/package/@uiw/codemirror-extensions-langs)
- [@codemirror/autocomplete](https://www.npmjs.com/package/@codemirror/autocomplete)
- [@codemirror/language](https://www.npmjs.com/package/@codemirror/language)
- [@codemirror/lint](https://www.npmjs.com/package/@codemirror/lint)
- [@codemirror/state](https://www.npmjs.com/package/@codemirror/state)
- [@codemirror/view](https://www.npmjs.com/package/@codemirror/view)
- [@lezer/highlight](https://www.npmjs.com/package/@lezer/highlight)
- [lucide-react](https://www.npmjs.com/package/lucide-react)
## Overview
`HandlebarsEditor` is a self-contained workbench for the
[Handlebars](https://handlebarsjs.com) templating language. It has two
tab-panel panes, each using the `variant="line"` style:
| Pane | Tabs |
|------|------|
| **Left pane** | **TEMPLATE** — Handlebars editor with syntax highlighting, helper autocomplete, hover docs and inline parse-error linting
**DATA** — JSON context the template renders against |
| **Right pane** | **OUTPUT** — read-only rendered string, with HTML or plain-text highlighting
**PREVIEW** — sandboxed iframe rendering the output as a live HTML document |
Rendering happens automatically as you type (debounced). Flip between TEMPLATE
and DATA from the left-pane tabs; flip between OUTPUT and PREVIEW from the
right-pane tabs.
## Features
- **Live rendering** — debounced re-render on every input or template change.
- **IntelliSense** — autocomplete with snippet expansion for Handlebars built-in block helpers (`#if`, `#each`, `#with`, `#unless`), inline helpers (`lookup`, `log`), and common comparison / math / formatting helpers.
- **Context path completion** — paths extracted from the JSON input (e.g. `user.name`, `orders.[0].total`) are surfaced in autocomplete as you type.
- **Hover documentation** — signatures, parameter docs and examples on hover.
- **Inline linting** — parse errors and unmatched block tags are underlined.
- **OUTPUT / PREVIEW tabs** — read the rendered string in CodeMirror, or flip to the PREVIEW tab to see it rendered inside a sandboxed iframe.
- **Custom helpers and partials** — pass `helpers` and `partials` to register your own at render time. Registrations are scoped to a private Handlebars instance so the global one is never mutated.
- **Samples menu** — load ready-made input + template pairs.
- **AI Assistant drawer** — opt-in chat panel built on ai-elements that slides in from the left and wires to your own LLM via `onSendMessage`.
- **Controlled or uncontrolled** — drive `template` / `input` yourself or let the component own them.
- **Flexible layout** — `horizontal` or `vertical`, with the input and output panes individually toggleable.
- **Theme-aware** — follows the Docyrus light / dark theme.
## Usage
```tsx
import { HandlebarsEditor } from "@docyrus/ui/components/handlebars-editor";
export function Example() {
return (
console.log(html)}
/>
);
}
```
### Controlled
Drive both panes from your own state:
```tsx
const [template, setTemplate] = useState("");
const [input, setInput] = useState("{}");
```
### Custom helpers
Register additional helpers — they'll be available inside the template **and**
surfaced in the autocomplete dropdown:
```tsx
new Intl.NumberFormat("en-US", {
style: "currency",
currency: String(currency ?? "USD")
}).format(Number(value))
}}
/>
```
> Memoize the `helpers` / `partials` objects if they're dynamic — the editor
> reads them on each render but does not re-render when only those references
> change.
### Custom partials
Partials let you split a template into reusable fragments:
```tsx
```
### Output rendering
The **OUTPUT** tab highlights the rendered result according to `outputMode`:
HTML syntax highlighting (`outputMode="html"`, the default), Markdown syntax
highlighting (`outputMode="markdown"`), or plain text (`outputMode="text"`).
The **PREVIEW** tab renders the same string as formatted content — an HTML
iframe for `html` / `text`, and formatted Markdown (via the shared
`react-markdown` renderer) for `markdown`:
```tsx
```
### Markdown output
Set `outputMode="markdown"` for templates that render Markdown. The OUTPUT tab
highlights the Markdown source and the PREVIEW tab shows it as formatted content
(headings, lists, tables, blockquotes) using the same renderer that powers
comments and chat — no extra dependency:
```tsx
```
## AI Assistant
Pass an `aiAssistant` config to add an **AI Assistant** button to the toolbar.
Clicking it slides a chat drawer in from the left of the editor, built on
ai-elements (`Conversation`, `Message`). Wire `onSendMessage` to your LLM and
return the reply (sync or async):
```tsx
{
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ message, template, input, history })
});
const { reply } = await response.json();
return reply;
},
suggestions: [
"Explain this template",
"Show me an #each example",
"How do I conditionally render?"
]
}}
/>
```
Pass `aiAssistant={true}` to mount the chat shell without a backend (useful
while wiring one — messages are stored locally but no reply arrives).
## Standalone template editor
`HandlebarsCodeEditor` is the template pane on its own — drop it into a form
or an automation node config when you only need to capture a Handlebars
template.
```tsx
import { HandlebarsCodeEditor } from "@docyrus/ui/components/handlebars-editor";
const [value, setValue] = useState("");
```
`contextPaths` and `helperNames` feed the autocomplete with project-specific
suggestions in addition to the built-ins.
## Headless rendering
`useHandlebars` debounces and renders a template against an already-parsed
context value, with no UI:
```tsx
import { useHandlebars } from "@docyrus/ui/components/handlebars-editor";
function useGreeting(user: unknown) {
const state = useHandlebars("Hello, {{name}}", user);
return state.status === "success" ? state.result : "";
}
```
For a one-off render outside React, use `renderHandlebars`:
```tsx
import { renderHandlebars } from "@docyrus/ui/components/handlebars-editor";
const state = renderHandlebars(
"{{count}} items",
{ count: 3 }
);
// → { status: "success", result: "3 items" }
```
## EditorAgent integration
Pair `useApplyHandlebars` with `renderAiAssistant` to give an LLM a writable template pane. The hook owns the editor's controlled state and ships an `applyHandlebars` tool that the agent calls to commit a new template — the tool also renders against the current input JSON and returns the rendered output preview so the agent can self-correct.
```tsx
import { EditorAgent } from '@docyrus/ui/components/editor-agent';
import { HandlebarsEditor, useApplyHandlebars } from '@docyrus/ui/components/handlebars-editor';
export function HandlebarsPlayground({ client, user, agentId }) {
const handlebars = useApplyHandlebars();
const [aiOpen, setAiOpen] = useState(false);
return (
(
)}
/>
);
}
```
The matching backend agent must register a tool named `applyHandlebars` whose input schema mirrors `{ template: string (required), input?: any, explanation?: string }`.
## API Reference
### HandlebarsEditor
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `template` | `string` | — | Controlled Handlebars template. |
| `defaultTemplate` | `string` | — | Initial template for uncontrolled usage. |
| `onTemplateChange` | `(template: string) => void` | — | Fired whenever the template changes. |
| `input` | `string \| unknown` | — | Controlled JSON input — a string or any JSON-serializable value. |
| `defaultInput` | `string \| unknown` | — | Initial JSON input for uncontrolled usage. |
| `onInputChange` | `(input: string) => void` | — | Fired whenever the input text changes. |
| `helpers` | `Record` | — | Custom helpers registered before compilation. |
| `partials` | `Record` | — | Custom partials registered before compilation. |
| `noEscape` | `boolean` | `false` | Disable HTML-escaping for all interpolations. |
| `onResult` | `(result: string) => void` | — | Fired after a successful render. |
| `onError` | `(error: HandlebarsEvaluationError) => void` | — | Fired when parsing the input or rendering the template fails. |
| `onEvaluate` | `(state: HandlebarsEvaluationState) => void` | — | Fired after every render, regardless of outcome. |
| `samples` | `HandlebarsSample[]` | — | Pre-defined input + template pairs for the samples menu. |
| `aiAssistant` | `boolean \| HandlebarsAIAssistantConfig` | — | When provided, mounts the built-in AI Assistant drawer on the left. Pass `true` for the UI shell only, or a config object with `onSendMessage` to wire a backend. |
| `aiAssistantOpen` | `boolean` | — | Controlled open state for the AI Assistant drawer. When provided the editor stops managing the open state internally — pair with `onAiAssistantOpenChange`. |
| `onAiAssistantOpenChange` | `(open: boolean) => void` | — | Fired when the AI Assistant toolbar button toggles the drawer. |
| `renderAiAssistant` | `(ctx: IHandlebarsAiAssistantRenderContext) => ReactNode` | — | Replaces the built-in drawer body. When set, the toolbar button is shown even if `aiAssistant` is omitted, the drawer animates in as usual, and this render fn supplies the body. Use this to mount a custom agent (e.g. ``) inside the drawer. |
| `showInput` | `boolean` | `true` | Show the **DATA** tab inside the left pane. |
| `showResult` | `boolean` | `true` | Show the right **OUTPUT / PREVIEW** pane. |
| `showToolbar` | `boolean` | `true` | Show the header toolbar. |
| `title` | `string` | `'Handlebars'` | Title shown in the toolbar. |
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Pane arrangement. |
| `outputMode` | `'text' \| 'html' \| 'markdown'` | `'html'` | How the rendered string is highlighted in the **OUTPUT** tab and rendered in the **PREVIEW** tab. `html` / `text` preview in a sandboxed iframe; `markdown` previews as formatted Markdown. |
| `debounceMs` | `number` | `300` | Debounce before rendering, in ms. |
| `readOnly` | `boolean` | `false` | Disables editing of both panes. |
| `height` | `number \| string` | `'28rem'` | Overall editor height. |
| `placeholder` | `string` | — | Placeholder shown in the empty template pane. |
| `className` | `string` | — | Root element className. |
### HandlebarsCodeEditor
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `string` | — | Current template text. |
| `onChange` | `(value: string) => void` | — | Fired on every edit. |
| `readOnly` | `boolean` | `false` | Disables editing. |
| `placeholder` | `string` | — | Placeholder shown when empty. |
| `autoFocus` | `boolean` | `false` | Focus the editor on mount. |
| `minHeight` | `string` | `'2.5rem'` | Minimum editor height. |
| `maxHeight` | `string` | `'12rem'` | Maximum editor height. |
| `height` | `string` | — | Fixed editor height — overrides `minHeight` / `maxHeight`. |
| `lineNumbers` | `boolean` | `false` | Show line numbers. |
| `lint` | `boolean` | `true` | Enable the parse-error linter. |
| `helperNames` | `string[]` | — | Extra helper names to surface in autocomplete (in addition to built-ins). |
| `contextPaths` | `string[]` | — | Suggest these context paths in autocomplete (e.g. extracted from input). |
| `basicSetup` | `BasicSetupOptions` | — | CodeMirror `basicSetup` overrides. |
| `extensions` | `Extension[]` | — | Extra CodeMirror extensions appended after the Handlebars language. |
| `className` | `string` | — | Wrapper className. |
### useHandlebars
```ts
useHandlebars(template: string, context: unknown, options?: UseHandlebarsOptions): HandlebarsEvaluationState
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `debounceMs` | `number` | `300` | Debounce before rendering, in ms. |
| `enabled` | `boolean` | `true` | When `false`, rendering is paused. |
| `helpers` | `Record` | — | Custom helpers registered before compilation. |
| `partials` | `Record` | — | Custom partials registered before compilation. |
| `noEscape` | `boolean` | `false` | Disable HTML-escaping for all interpolations. |
| `strict` | `boolean` | `false` | Treat compile-time warnings as errors. |
## Components
| Component | Description |
|-----------|-------------|
| `HandlebarsEditor` | The three-pane input / template / output workbench. |
| `HandlebarsCodeEditor` | The standalone Handlebars template editor. |
| `useApplyHandlebars` | Hook that owns `template` + `input` state and exposes the `applyHandlebars` client-side tool for `` — the tool writes the agent's template AND renders it, returning a rendered-output preview or a categorized error (input-empty / parse / render). See **EditorAgent integration** below. |
## Type Exports
| Type | Description |
|------|-------------|
| `HandlebarsEditorProps` | Props for `HandlebarsEditor`. |
| `HandlebarsCodeEditorProps` | Props for `HandlebarsCodeEditor`. |
| `HandlebarsEditorOrientation` | `'horizontal' \| 'vertical'`. |
| `HandlebarsOutputMode` | `'text' \| 'html' \| 'markdown'`. Controls the OUTPUT tab's highlighting and the PREVIEW tab's rendering. |
| `HandlebarsSample` | A `{ name, description?, input, template }` sample pair. |
| `HandlebarsHelperFn` | Signature for a user-supplied helper function. |
| `HandlebarsRenderOptions` | Options for `renderHandlebars` (`helpers`, `partials`, `noEscape`, `strict`). |
| `HandlebarsAIAssistantConfig` | Config for the AI Assistant drawer — `onSendMessage`, `suggestions`, `title`, `defaultOpen`, `width`, `placeholder`, `emptyStateDescription`. |
| `IHandlebarsAiAssistantRenderContext` | Argument passed to `renderAiAssistant` — `{ open, width, onClose, template, input }`. |
| `IUseApplyHandlebarsResult` | Return shape of `useApplyHandlebars` — `{ template, setTemplate, input, setInput, tools }`. |
| `HandlebarsAIMessageContext` | Context passed to `onSendMessage`: current `template`, `input` and `history`. |
| `HandlebarsChatMessage` | A `{ id, role: 'user' \| 'assistant', content }` chat entry. |
| `HandlebarsEvaluationState` | Discriminated union describing a render outcome. |
| `HandlebarsEvaluationError` | Normalized parse / render error (`phase`, `message`, `code?`, `line?`, `column?`). |
| `HandlebarsErrorPhase` | `'input' \| 'parse' \| 'render'`. |
| `UseHandlebarsOptions` | Options for the `useHandlebars` hook. |
| `HandlebarsHelper` | A built-in helper descriptor used by IntelliSense. |
| `HandlebarsHelperParam` | A single parameter of a `HandlebarsHelper`. |
## Helpers
| Export | Description |
|--------|-------------|
| `useHandlebars` | Headless hook — debounced render against a parsed context. |
| `renderHandlebars` | Compiles and runs a template; never throws. |
| `parseJsonInput` | Parses the JSON input text into a value or an error. |
| `handlebarsExtensions` | CodeMirror extension bundle (language, autocomplete, hover, linter). |
| `handlebarsLanguage` | CodeMirror `LanguageSupport` for Handlebars. |
| `HANDLEBARS_HELPERS` | The built-in helper catalog. |
| `HANDLEBARS_HELPER_MAP` | `Map` of helper name → descriptor. |
| `HANDLEBARS_HELPER_NAMES` | `Set` of built-in helper names. |
| `HANDLEBARS_BLOCK_HELPERS` | `Set` of names that are block helpers (`#if`, `#each`, `#with`, `#unless`). |
| `HANDLEBARS_DATA_VARIABLES` | The `@data` variables surfaced in autocomplete (`@index`, `@key`, `@first`, `@last`, `@root`). |
| `HANDLEBARS_KEYWORDS` | Reserved Handlebars context keywords. |
| `HANDLEBARS_SAMPLES` | Ready-made sample input + template pairs. |