Skip to content
Apexify.jsDocs
Apexify.js version v5.4.5

Advanced / Composition · guide

Templates (createTemplate)

Current Apexify.js 6.0.0 documentation for Templates (createTemplate).

apexify.jsRuntime: nodeCURRENTSince 6.0.0

ApexPainter#createTemplate(definition, options?) captures an immutable copy of a scene-shaped definition and returns a TemplateHandle.

MethodReturnsPurpose
toRenderInput(data, opts?)Promise<SceneRenderInput>Resolve the template without rasterizing; useful for tests and scene→GIF/video workflows.
render(data, opts?)Promise<Buffer>Resolve, validate, then render one PNG.

TemplateHandle snapshots both the definition and each render's data before asynchronous layout work. Mutating caller-owned objects after createTemplate() or after starting a render does not change that render.


Resolution order

The supported pipeline is deterministic:

  1. Apply render-time insertions.
  2. Validate unique literal layer id values.
  3. Apply overrides by id.
  4. Evaluate visible and remove hidden subtrees.
  5. Resolve {{placeholders}}.
  6. Resolve $assets.
  7. Normalize numeric layout fields and shorthand layers.
  8. Expand flex/grid layouts.
  9. Validate the resulting SceneRenderInput with the normal scene limits.

Hidden layers are removed before their other placeholders/assets are resolved, so a missing value inside an intentionally hidden layer does not fail the render.


Placeholders

  • {{key}} — required. Missing or null/undefined values throw TemplateResolveError.
  • {{key | default}} — the explicit default is used only for missing/nullish values.
  • Dotted own-property paths such as {{user.name}} are supported.
  • A placeholder that occupies the entire string field preserves the native value. 0, false, and "" are not treated as missing.
  • An embedded placeholder inside a longer string is stringified.

Preserving an explicit empty string does not bypass the final scene contract. If "" resolves into a field that requires a non-empty string, such as a text layer's text, toRenderInput() / render() rejects the final scene during validation. Apexify.js does not silently substitute the placeholder default or drop the layer; use visible when an empty value should intentionally suppress content.

SOURCEtypescript
typescript
Studio
const data = {  position: { x: 0 },  enabled: false,  label: "",  user: { name: "Ada" },};

Unsafe prototype-path segments are not traversed.


Shorthand layers

Template definitions may use document-style shorthand:

  • { type: "text", text: "…", … } → scene texts form.
  • { type: "image", source: "…", … } → scene images form.

The final SceneRenderInput contains normal scene layers.


Minimal example

SOURCEtypescript
typescript
Studio
import { ApexPainter } from "apexify.js";import fs from "node:fs"; const painter = new ApexPainter({ type: "buffer" });painter.assets.loadPalette("ui", { bg: "#0f172a", fg: "#f8fafc" }); const card = painter.createTemplate({  width: 560,  height: 220,  background: { colorBg: "$ui.bg" },  layers: [    {      id: "title",      type: "text",      text: "{{title}}",      x: 32,      y: 48,      fontSize: 28,      color: "$ui.fg",    },    {      id: "body",      type: "text",      visible: "{{showBody}}",      text: "{{body | Nothing to show}}",      x: 32,      y: 110,      fontSize: 16,      color: "$ui.fg",    },  ],}); const png = await card.render({  title: "Ada",  showBody: true,  body: "Your graph is ready.",}); fs.writeFileSync("template.png", png);

Visibility

visible accepts booleans, numbers, literal strings, or placeholder strings. Common string values are interpreted explicitly: true, 1, yes are true; false, 0, no, and the empty string are false.

For a whole placeholder such as visible: "{{feature.enabled}}", the native boolean/number/string value drives visibility.


Overrides and insertions

Every targetable layer id must be a unique, non-empty literal string. Placeholder ids are rejected.

Deep overrides

SOURCEtypescript
typescript
Studio
await card.render(  { title: "Build", showBody: false },  {    overrides: {      title: { color: "#4ade80", fontSize: 32 },    },  });

Overrides use the authored template-layer shape and deep-merge plain nested objects. Unknown ids are rejected instead of being silently ignored.

Deterministic insertions

SOURCEtypescript
typescript
Studio
await card.render(  { title: "Build", showBody: false },  {    insertions: [      {        targetId: "title",        position: "after",        layers: {          id: "status",          type: "text",          text: "Passed",          x: 32,          y: 82,          fontSize: 14,          color: "#4ade80",        },      },    ],  });

Each insertion target must exist exactly once, and insertions may not introduce duplicate ids.


Flex layout

A type: "layout" node with layout.type: "flex" expands to absolutely positioned children.

FieldMeaning
directionrow (default) or column
gap, paddingNon-negative pixels
alignCross axis: start, center, end
justifyMain axis: start, center, end, space-between

Text children are measured with measureText; image children require positive explicit width/height. Layout containers also require positive width/height.

SOURCEtypescript
typescript
Studio
const hero = painter.createTemplate({  width: 640,  height: 200,  background: { colorBg: "#020617" },  layers: [    {      type: "layout",      x: 40,      y: 32,      width: 560,      height: 136,      layout: {        type: "flex",        direction: "column",        gap: 10,        padding: 12,        align: "start",        justify: "center",      },      children: [        { type: "text", text: "{{headline}}", x: 0, y: 0, fontSize: 26, color: "#f8fafc" },        { type: "text", text: "{{tagline}}", x: 0, y: 0, fontSize: 15, color: "#94a3b8" },      ],    },  ],});

Grid layout

layout.type: "grid" supports positive integer columns, non-negative gap / padding, and start / center / end alignment and justification. Cell width is derived from the container; row height is the tallest child in that row. Invalid geometry that leaves no positive cell area throws TemplateResolveError.

SOURCEtypescript
typescript
Studio
{  type: "layout",  x: 20,  y: 20,  width: 400,  height: 220,  layout: { type: "grid", columns: 2, gap: 12, padding: 8, align: "center", justify: "center" },  children: [    { type: "image", source: "rectangle", width: 120, height: 60, shape: { fill: true, color: "#334155" } },    { type: "image", source: "rectangle", width: 120, height: 60, shape: { fill: true, color: "#475569" } },  ],}

Asset resolution

After placeholders, the shared asset-reference engine resolves $name, dotted paths, $$ escaping, whole-field structured/Buffer references, and scalar embedded references. Unknown assets become TemplateResolveError with the asset token and original cause.

Pass createTemplate(definition, { resolveAssetRef }) when a template needs a custom resolver instead of painter.assets.

render() deliberately calls the final scene render with resolveAssetRefs: false because the template stage already resolved its assets.


toRenderInput

SOURCEtypescript
typescript
Studio
const input = await hero.toRenderInput({ headline: "Ship faster", tagline: "One scene, many rows." }); await painter.renderSceneToGIF(input, {  options: { width: 640, height: 200, outputFormat: "buffer" },});

The returned object is an isolated, validated SceneRenderInput snapshot. Mutating it does not mutate the template definition or later renders.


Next steps