Custom templates
Palette entries are Templates โ factories that turn a TemplateContext (id, layer, position, z-order) into one element. Register your own and it appears in the palette next to the built-ins, draggable and searchable.
A "service card" templateโ
The richest route is a type: "template" blueprint: a flex-laid node tree with data bindings and connection ports, packed into a single resizable element. templateFromSpec turns the JSON spec into a callable Template; the <Editor> templates prop registers it in the shared defaultRegistry that the palette reads.
import { Editor, registerInteractiveHitTester } from "@oh-just-another/editor";
import { rich, templateFromSpec } from "@oh-just-another/templates";
import "@oh-just-another/react-ui/styles.css";
// Rich templates add an element type ("template") beyond the built-in six โ
// register its renderer and hit-tester once at module load.
rich.installTemplateShapeRenderer();
registerInteractiveHitTester("template", rich.templateInteractiveHitTester);
const serviceCard = templateFromSpec({
id: "custom.service-card",
name: "Service card",
category: "services", // any string; new categories become palette sections
tags: ["service", "api", "card"], // palette search keywords
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
blueprint: {
type: "template",
width: 220,
height: 96,
minWidth: 160,
minHeight: 72,
defaults: { title: "Payments API", status: "healthy" }, // bound data
schema: { title: "string", status: "string" },
root: {
type: "container",
style: { fill: "#ffffff", stroke: "#3b82f6", strokeWidth: 2 },
layout: { flexDirection: "column", padding: 12, gap: 4 },
children: [
{
type: "text",
id: "title",
text: { bind: "title" },
style: { fontSize: 14, fontWeight: "bold", color: "#1f2937" },
},
{
type: "text",
id: "status",
text: { bind: "status" },
style: { fontSize: 11, color: "#6b7280" },
},
// Named connection ports โ links snap to them; ratio anchors
// survive resize.
{ type: "port", id: "in", layout: { position: "spot", anchor: "left" } },
{ type: "port", id: "out", layout: { position: "spot", anchor: "right" } },
],
},
},
});
export const App = () => <Editor templates={[serviceCard]} />;
Drag "Service card" from the new services palette section onto the canvas; the two texts render from defaults, and drawing a link near the left/right edge snaps to the in / out ports.
Notesโ
- Simple shapes need less. For a single rectangle / ellipse / polygon / path / text / image, use the matching
ShapeBlueprinttypeinstead of"template"โ no renderer registration required, the element is a plain built-in shape. - JSON libraries.
loadTemplateLibrary(input, registry?)validates and registers a whole{ format: "oh-just-another/template-library", version: 1, templates: [...] }bundle (object or JSON string) in one call. Invalid input throwsTemplateLibraryError; duplicate ids throw unless{ replace: true }. - Registry. The palette defaults to the
defaultRegistrysingleton (registerthrows on duplicate ids,replaceoverwrites,byCategory/categoriesdrive the sections). Pass a customTemplateRegistryvia the palette'sregistryprop to scope templates per instance. - Fully custom factory. A
Templateis just{ id, name, category, icon, factory(ctx) }โ you can write the factory by hand and return any element your renderers understand.
Full API reference: Templates. For a new element type (custom geometry, hit-testing, rendering) rather than a palette preset, see defineShape in the shape model.