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.
Installation
pnpm dlx @docyrus/cli add @docyrus/ui-handlebars-editorpnpm add handlebars @uiw/react-codemirror @uiw/codemirror-extensions-langs @codemirror/autocomplete @codemirror/language @codemirror/lint @codemirror/state @codemirror/view @lezer/highlightnpx shadcn@latest add button dropdown-menu tooltip textareaOverview
HandlebarsEditor is a self-contained workbench for the
Handlebars 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
helpersandpartialsto 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/inputyourself or let the component own them. - Flexible layout —
horizontalorvertical, with the input and output panes individually toggleable. - Theme-aware — follows the Docyrus light / dark theme.
Usage
import { HandlebarsEditor } from "@docyrus/ui/components/handlebars-editor";
export function Example() {
return (
<HandlebarsEditor
defaultTemplate="<p>Hello, {{firstName}} {{lastName}}!</p>"
defaultInput={{ firstName: "Fred", lastName: "Smith" }}
onResult={(html) => console.log(html)}
/>
);
}Controlled
Drive both panes from your own state:
const [template, setTemplate] = useState("");
const [input, setInput] = useState("{}");
<HandlebarsEditor
template={template}
onTemplateChange={setTemplate}
input={input}
onInputChange={setInput}
/>Custom helpers
Register additional helpers — they'll be available inside the template and surfaced in the autocomplete dropdown:
<HandlebarsEditor
defaultTemplate="{{formatCurrency total 'USD'}}"
defaultInput={{ total: 1250 }}
helpers={{
formatCurrency: (value, currency) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: String(currency ?? "USD")
}).format(Number(value))
}}
/>Memoize the
helpers/partialsobjects 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:
<HandlebarsEditor
defaultTemplate="{{> greeting }}, welcome back."
defaultInput={{ name: "Ada" }}
partials={{
greeting: "Hello, {{name}}"
}}
/>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:
<HandlebarsEditor
defaultTemplate="<button style='color:white;background:#2563eb;padding:6px 12px;border-radius:4px;border:0'>{{label}}</button>"
defaultInput={{ label: "Click me" }}
outputMode="html"
/>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:
<HandlebarsEditor
defaultTemplate={"# {{title}}\n\n**Owner:** {{owner}}\n\n{{#each tasks}}- {{name}}\n{{/each}}"}
defaultInput={{ title: "Sprint 12", owner: "Ada", tasks: [{ name: "Ship editor" }] }}
outputMode="markdown"
/>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):
<HandlebarsEditor
defaultTemplate="<p>Hello, {{name}}</p>"
aiAssistant={{
onSendMessage: async (message, { template, input, history }) => {
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.
import { HandlebarsCodeEditor } from "@docyrus/ui/components/handlebars-editor";
const [value, setValue] = useState("");
<HandlebarsCodeEditor
value={value}
onChange={setValue}
placeholder="Hello, {{name}}"
contextPaths={["name", "email", "company.title"]}
/>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:
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:
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.
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 (
<HandlebarsEditor
template={handlebars.template}
onTemplateChange={handlebars.setTemplate}
input={handlebars.input}
onInputChange={handlebars.setInput}
aiAssistantOpen={aiOpen}
onAiAssistantOpenChange={setAiOpen}
renderAiAssistant={({ open, onClose }) => (
<EditorAgent
agentId={agentId}
client={client}
user={user}
open={open}
onClose={onClose}
clientTools={handlebars.tools}
/>
)}
/>
);
}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<string, HandlebarsHelperFn> | — | Custom helpers registered before compilation. |
partials | Record<string, string> | — | 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. <EditorAgent>) 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
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<string, HandlebarsHelperFn> | — | Custom helpers registered before compilation. |
partials | Record<string, string> | — | 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 <EditorAgent> — 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. |
Gantt
A Gantt chart component with drag-and-drop task scheduling, timeline zoom, custom markers, and sidebar navigation.
HTML Template Editor
WYSIWYG document editor for authoring Handlebars-aware HTML templates (quotes, invoices, reports). Word-style A4 page surface, four tabs (Visual / Code / Data / Preview), variable & helper chips, slash-style triggers, and a data-driven Table dialog where users discover JSON paths, configure columns, and write free-form sum / aggregate expressions per row.