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.

Client Only

Installation

pnpm dlx @docyrus/cli add @docyrus/ui-handlebars-editor
Required Packages(9 packages)
pnpm add handlebars @uiw/react-codemirror @uiw/codemirror-extensions-langs @codemirror/autocomplete @codemirror/language @codemirror/lint @codemirror/state @codemirror/view @lezer/highlight
UI Primitives(4 components)
npx shadcn@latest add button dropdown-menu tooltip textarea

Overview

HandlebarsEditor is a self-contained workbench for the Handlebars templating language. It has two tab-panel panes, each using the variant="line" style:

PaneTabs
Left paneTEMPLATE — Handlebars editor with syntax highlighting, helper autocomplete, hover docs and inline parse-error linting
DATA — JSON context the template renders against
Right paneOUTPUT — 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

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 / 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:

<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

PropTypeDefaultDescription
templatestring—Controlled Handlebars template.
defaultTemplatestring—Initial template for uncontrolled usage.
onTemplateChange(template: string) => void—Fired whenever the template changes.
inputstring | unknown—Controlled JSON input — a string or any JSON-serializable value.
defaultInputstring | unknown—Initial JSON input for uncontrolled usage.
onInputChange(input: string) => void—Fired whenever the input text changes.
helpersRecord<string, HandlebarsHelperFn>—Custom helpers registered before compilation.
partialsRecord<string, string>—Custom partials registered before compilation.
noEscapebooleanfalseDisable 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.
samplesHandlebarsSample[]—Pre-defined input + template pairs for the samples menu.
aiAssistantboolean | 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.
aiAssistantOpenboolean—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.
showInputbooleantrueShow the DATA tab inside the left pane.
showResultbooleantrueShow the right OUTPUT / PREVIEW pane.
showToolbarbooleantrueShow the header toolbar.
titlestring'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.
debounceMsnumber300Debounce before rendering, in ms.
readOnlybooleanfalseDisables editing of both panes.
heightnumber | string'28rem'Overall editor height.
placeholderstring—Placeholder shown in the empty template pane.
classNamestring—Root element className.

HandlebarsCodeEditor

PropTypeDefaultDescription
valuestring—Current template text.
onChange(value: string) => void—Fired on every edit.
readOnlybooleanfalseDisables editing.
placeholderstring—Placeholder shown when empty.
autoFocusbooleanfalseFocus the editor on mount.
minHeightstring'2.5rem'Minimum editor height.
maxHeightstring'12rem'Maximum editor height.
heightstring—Fixed editor height — overrides minHeight / maxHeight.
lineNumbersbooleanfalseShow line numbers.
lintbooleantrueEnable the parse-error linter.
helperNamesstring[]—Extra helper names to surface in autocomplete (in addition to built-ins).
contextPathsstring[]—Suggest these context paths in autocomplete (e.g. extracted from input).
basicSetupBasicSetupOptions—CodeMirror basicSetup overrides.
extensionsExtension[]—Extra CodeMirror extensions appended after the Handlebars language.
classNamestring—Wrapper className.

useHandlebars

useHandlebars(template: string, context: unknown, options?: UseHandlebarsOptions): HandlebarsEvaluationState
OptionTypeDefaultDescription
debounceMsnumber300Debounce before rendering, in ms.
enabledbooleantrueWhen false, rendering is paused.
helpersRecord<string, HandlebarsHelperFn>—Custom helpers registered before compilation.
partialsRecord<string, string>—Custom partials registered before compilation.
noEscapebooleanfalseDisable HTML-escaping for all interpolations.
strictbooleanfalseTreat compile-time warnings as errors.

Components

ComponentDescription
HandlebarsEditorThe three-pane input / template / output workbench.
HandlebarsCodeEditorThe standalone Handlebars template editor.
useApplyHandlebarsHook 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

TypeDescription
HandlebarsEditorPropsProps for HandlebarsEditor.
HandlebarsCodeEditorPropsProps for HandlebarsCodeEditor.
HandlebarsEditorOrientation'horizontal' | 'vertical'.
HandlebarsOutputMode'text' | 'html' | 'markdown'. Controls the OUTPUT tab's highlighting and the PREVIEW tab's rendering.
HandlebarsSampleA { name, description?, input, template } sample pair.
HandlebarsHelperFnSignature for a user-supplied helper function.
HandlebarsRenderOptionsOptions for renderHandlebars (helpers, partials, noEscape, strict).
HandlebarsAIAssistantConfigConfig for the AI Assistant drawer — onSendMessage, suggestions, title, defaultOpen, width, placeholder, emptyStateDescription.
IHandlebarsAiAssistantRenderContextArgument passed to renderAiAssistant — { open, width, onClose, template, input }.
IUseApplyHandlebarsResultReturn shape of useApplyHandlebars — { template, setTemplate, input, setInput, tools }.
HandlebarsAIMessageContextContext passed to onSendMessage: current template, input and history.
HandlebarsChatMessageA { id, role: 'user' | 'assistant', content } chat entry.
HandlebarsEvaluationStateDiscriminated union describing a render outcome.
HandlebarsEvaluationErrorNormalized parse / render error (phase, message, code?, line?, column?).
HandlebarsErrorPhase'input' | 'parse' | 'render'.
UseHandlebarsOptionsOptions for the useHandlebars hook.
HandlebarsHelperA built-in helper descriptor used by IntelliSense.
HandlebarsHelperParamA single parameter of a HandlebarsHelper.

Helpers

ExportDescription
useHandlebarsHeadless hook — debounced render against a parsed context.
renderHandlebarsCompiles and runs a template; never throws.
parseJsonInputParses the JSON input text into a value or an error.
handlebarsExtensionsCodeMirror extension bundle (language, autocomplete, hover, linter).
handlebarsLanguageCodeMirror LanguageSupport for Handlebars.
HANDLEBARS_HELPERSThe built-in helper catalog.
HANDLEBARS_HELPER_MAPMap of helper name → descriptor.
HANDLEBARS_HELPER_NAMESSet of built-in helper names.
HANDLEBARS_BLOCK_HELPERSSet of names that are block helpers (#if, #each, #with, #unless).
HANDLEBARS_DATA_VARIABLESThe @data variables surfaced in autocomplete (@index, @key, @first, @last, @root).
HANDLEBARS_KEYWORDSReserved Handlebars context keywords.
HANDLEBARS_SAMPLESReady-made sample input + template pairs.

On this page