# 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}}.

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. |