Render Strategies Guide

Render strategies are per-field UI modifiers that change how a datatype is displayed, for example how a binding's editor is displayed in the recipe editor. They don't change a field's value or type — only how it looks and behaves on screen.

Every datatype carries an optional renderStrategies array. Each strategy is an object with a type (the strategy name) and a config (strategy-specific options):

  {
    "type": "S",
    "name": "First Name",
    "renderStrategies": [
      {
        "type": "addClasses",
        "config": { "classes": ["w-33"] }
      }
    ]
  }

Strategies cover three broad concerns: how the input control looks (e.g. valueAsSlider, valueAsTextArea, passwordMasking), how a container of fields is laid out (e.g. templatedEditor, templatedLayout, inlineLayout), and styling/visibility on the field's DOM (e.g. addClasses, hidden, important). A field can list more than one strategy — they're applied in order.

Contents

Layout strategies

Strategies that control how a container of fields (usually O or A types, which represent strongly typed objects (where each field and its value type are defined), and strongly typed homogenous arrays respectively) is laid out.

templatedEditor

Custom editable HTML layout for an O type. Child editors are injected into elements matched by CSS class, preserving full edit and validation behaviour. See the two-column form worked example below for a full demonstration. In this example, the HTML with class hooks represents HTML where elements within the HTML who have classes which exactly match a field name (case sensitive) have their contents replaced with the editor for that field.

  { "type": "templatedEditor", "config": { "template": "<HTML with class hooks>" } }

↑ Contents

templatedLayout

Read-only sibling of templatedEditor — same class-name injection mechanic but renders read-only. Use when the binding is manual: false (computed) and you want a custom display.

  { "type": "templatedLayout", "config": { "template": "<HTML with class hooks>" } }

Current value:

{}

↑ Contents

templatedArrayLayout

Custom HTML for an A type. Provides separate templates for the array container and for each item, with CSS selectors telling the editor where to inject items and where the child editor sits inside an item.

  {
    "type": "templatedArrayLayout",
    "config": {
      "template": "<div class='itemList'></div>",
      "itemTemplate": "<div class='item'><span class='value'></span></div>",
      "itemTemplateValueSelector": ".value",
      "childrenContainerSelector": ".itemList"
    }
  }

Current value:

{}

↑ Contents

replacementLayout

Read-only HTML template with {{ fieldName }} token substitution. Lighter than templatedLayout when you just want to interpolate values into a string.

  { "type": "replacementLayout", "config": { "template": "Hello {{ name }}!" } }

Current value:

{}

↑ Contents

inlineLayout

Renders the field (and, for O/A types, its children) inline rather than as block-level. Useful for compact horizontal layouts.

  { "type": "inlineLayout", "config": {} }

Current value:

{}

↑ Contents

child

Applies a list of strategies to a single named child of an O-type binding, rather than to the binding itself. Useful when the strategies you want belong on a sub-field but you want to keep them grouped with the parent's definition.

  {
    "type": "child",
    "config": { "field": "email", "strategies": [ { "type": "placeholder", "config": {} } ] }
  }

Current value:

{}

↑ Contents

Input control strategies

Strategies that replace the default editor for a type with a different control.

valueAsSlider

Renders a numeric field (I, L, F, D) as an HTML range slider with optional tick marks.

  { "type": "valueAsSlider", "config": { "min": 0, "max": 100, "tickEvery": 10, "displayTicks": true } }

Current value:

{}

↑ Contents

valueAsGauge

Read-only numeric display rendered as a gauge / progress bar between min and max.

  { "type": "valueAsGauge", "config": { "min": 0, "max": 100 } }

Current value:

{}

↑ Contents

valueAsCodeMirror

String field rendered as a CodeMirror editor — the same JS editor used for Js fields. Useful when the string is code or another structured language.

  { "type": "valueAsCodeMirror", "config": {} }

Current value:

{}

↑ Contents

valueAsTextArea

String field rendered as a multi-line textarea instead of a single-line input.

  { "type": "valueAsTextArea", "config": {} }

Current value:

{}

↑ Contents

valueAsRichText

String field rendered as a rich text (WYSIWYG) editor. Output is HTML.

  { "type": "valueAsRichText", "config": {} }

↑ Contents

valueAsRadioButton

Boolean or choice field rendered as radio buttons rather than a checkbox or dropdown.

  { "type": "valueAsRadioButton", "config": {} }

Current value:

{}

↑ Contents

valueAsDropdown

Boolean field rendered as a select / dropdown rather than a checkbox.

  { "type": "valueAsDropdown", "config": {} }

Current value:

{}

↑ Contents

valueAsDatePicker

Timestamp / numeric field rendered as a date picker (date portion only).

  { "type": "valueAsDatePicker", "config": {} }

Current value:

{}

↑ Contents

valueAsDateTimePicker

Timestamp / numeric field rendered as a datetime picker (date + time of day).

  { "type": "valueAsDateTimePicker", "config": {} }

Current value:

{}

↑ Contents

inlineReadOnly

Read-only inline display of the field's value. Combines read-only with an inline (non-block) layout.

  { "type": "inlineReadOnly", "config": {} }

Current value:

{}

↑ Contents

simpleBoolean

Boolean rendered as a checkmark / cross icon rather than a checkbox.

  { "type": "simpleBoolean", "config": { "size": 24 } }

Current value:

{}

↑ Contents

valueOnlyLayout / Simple

Renders only the field's value, with no surrounding label or chrome. The Simple strategy is an alias for valueOnlyLayout.

  { "type": "valueOnlyLayout", "config": {} }

Current value:

{}

↑ Contents

Styling & visibility strategies

Strategies that decorate the field's DOM — CSS classes, placeholder text, prefixes / suffixes, or hide/highlight states.

important

Marks the field visually as required / important (adds the .required class to the field's label).

  { "type": "important", "config": {} }

Current value:

{}

↑ Contents

placeholder

Promotes the field's default value to the input's HTML placeholder attribute, so the user sees it as ghost text rather than a pre-filled value.

  { "type": "S", "name": "Email", "default": "you@example.com", "renderStrategies": [{ "type": "placeholder", "config": {} }] }

Current value:

{}

↑ Contents

hidden

Hides the field from the UI. The value is still part of the binding's data, just not visible or editable. Useful for fields populated by upstream auto-bindings.

  { "type": "hidden", "config": {} }

↑ Contents

prefix

Inserts the field's configured prefix HTML before the input control. The prefix text comes from a sibling prefix property on the datatype.

  { "type": "S", "name": "Path", "prefix": "/", "renderStrategies": [{ "type": "prefix", "config": {} }] }

Current value:

{}

↑ Contents

suffix

Inserts the field's configured suffix HTML after the input control.

  { "type": "S", "name": "Domain", "suffix": ".com", "renderStrategies": [{ "type": "suffix", "config": {} }] }

Current value:

{}

↑ Contents

passwordMasking

Renders a string field as a password input (characters masked).

  { "type": "passwordMasking", "config": {} }

Current value:

{}

↑ Contents

revealablePasswordMasking

Like passwordMasking, but with an eye / reveal toggle so the user can see the value they're typing.

  { "type": "revealablePasswordMasking", "config": {} }

Current value:

{}

↑ Contents

bindingHint

Wraps the field's hint text in a collapsible container, hiding long descriptions until the user expands them.

  { "type": "bindingHint", "config": {} }

Current value:

{}

↑ Contents

noValidation

Suppresses the validation UI for this field. Validation rules still run, but failures don't show inline errors. Use sparingly.

  { "type": "noValidation", "config": {} }

↑ Contents

addClasses

Adds CSS classes to the field's root element. Often used to set widths (w-33, w-66) when many fields share a row.

  { "type": "addClasses", "config": { "classes": ["w-33", "text-bold"] } }

↑ Contents

addClassesTo

Adds CSS classes to one or more descendants of the field's root, selected by CSS selector. Use when you need to style the inner input, label, or other sub-elements specifically.

  {
    "type": "addClassesTo",
    "config": { "targets": [ { "selector": ".configurableArgumentName", "classes": ["text-uppercase"] } ] }
  }

↑ Contents

removeClasses

Inverse of addClasses — removes CSS classes from the field's root element.

  { "type": "removeClasses", "config": { "classes": ["mb-2"] } }

↑ Contents

removeClassesFrom

Inverse of addClassesTo — removes CSS classes from descendants matched by selector.

  {
    "type": "removeClassesFrom",
    "config": { "targets": [ { "selector": ".configurableArgumentName", "classes": ["text-bold"] } ] }
  }

↑ Contents

Data formatting strategies

Strategies that change how a value is displayed — conversions and presentations rather than edit controls.

numericAsTime

Formats a numeric (epoch ms) field as a human-readable date / time.

  { "type": "numericAsTime", "config": {} }

Current value:

{}

↑ Contents

valueAsTimeSpan

Formats a numeric duration as a human-readable timespan (e.g. 2h 15m). The timeUnit tells the editor what unit the underlying number is in.

  { "type": "valueAsTimeSpan", "config": { "timeUnit": "ms", "precision": 2 } }

Current value:

{}

↑ Contents

colorCodedReadOnly

Read-only display of a numeric value (I, L, D, F), coloured according to numeric threshold-keyed colour mappings. The colour applied is the one whose threshold key is the largest the value meets or exceeds.

  { "type": "colorCodedReadOnly", "config": { "colors": { "0": "#f00", "30": "#fa0", "70": "#0c0" } } }

Current value:

{}

↑ Contents

colorCodedNumeric

Numeric variant of colorCodedReadOnly — thresholds are numeric and the value's colour follows the threshold band it falls into.

  { "type": "colorCodedNumeric", "config": { "colors": { "0": "#0f0", "100": "#f00" } } }

Current value:

{}

↑ Contents

htmlSubstitution

Replaces a substring inside an HTML template with the field's value. Lighter than replacementLayout — single-token substitution rather than a full template.

  { "type": "htmlSubstitution", "config": { "baseString": "Score: __SCORE__", "replacementString": "__SCORE__" } }

↑ Contents

Container behaviour strategies

Strategies that change how a container field (usually A or O) manages its children — collapse state, length limits, or layout direction.

collapsible

Wraps the field's content in a collapsible container with configurable collapsed and expanded labels. Good for O types with many fields you don't want to show at all times.

  { "type": "collapsible", "config": { "isCollapsed": true, "collapsedContent": "Show advanced", "expandedContent": "Hide advanced" } }

Current value:

{}

↑ Contents

lengthConstrainedA

Enforces minimum and / or maximum size on an A (array) type. The editor shows add / remove buttons only when constraints allow.

  { "type": "lengthConstrainedA", "config": { "minSize": 1, "maxSize": 5 } }

Current value:

{}

↑ Contents

horizontallyStackedA

Lays out an A type's items horizontally rather than the default vertical stack.

  { "type": "horizontallyStackedA", "config": {} }

Current value:

{}

↑ Contents

horizontallyStackedLengthConstrainedA

Combination of horizontallyStackedA and lengthConstrainedA — horizontal layout with min / max constraints.

  { "type": "horizontallyStackedLengthConstrainedA", "config": { "minSize": 1, "maxSize": 3 } }

Current value:

{}

↑ Contents

Default per-type strategies

These strategies are what runs when no renderStrategies are supplied for a given type. You rarely set them explicitly — they're the baseline editors. Listed here for completeness.

  • SDefault — default string editor (single-line input).
  • NumericDefault — default editor for numeric types (I, L, F, D).
  • BDefault — default boolean editor (checkbox).
  • ODefault — default object editor (labelled fields stacked vertically).
  • ADefault — default array editor (vertical list with add / remove).
  • ChoiceDefault — default editor for CHOICE types (variant selector + nested editor).
  • JSDefault — default JavaScript editor (CodeMirror).
  • JvDefault — default editor for Jv (raw JSON).
  • DscDefault — default editor for Dsc (description) types — renders text or HTML.
  • BYTSDefault — default editor for BYTS (byte array) — file upload control.
  • EDefault — default editor for E (exception) types — read-only structured error display.
  • LazyDefault — defers editor selection to a JS function that computes the effective type at render time.

↑ Contents

Worked Examples

Two-column form

Suppose a recipe needs a binding called Attribute Mapping that asks the user to fill in four named attributes (first name, last name, email, role) and produces an object the rest of the recipe can consume. The default rendering of an O-type binding stacks each field vertically with its prompt above the input. Useful but verbose — for four short text fields we'd rather show labels on the left and inputs on the right.

That's what the templatedEditor strategy is for. It accepts an HTML template; child editors are injected into elements whose CSS class matches the child's name. Anything else in the template (labels, layout containers, dividers) is left alone.

The binding

  {
    "type": "O",
    "name": "Attribute Mapping",
    "renderStrategies": [
      {
        "type": "templatedEditor",
        "config": {
          "template": "<div style='display:grid; grid-template-columns: 1fr 2fr; gap: 8px 16px; align-items: center;'><label>First Name</label><div class='firstName'></div><label>Last Name</label><div class='lastName'></div><label>Email</label><div class='email'></div><label>Role</label><div class='role'></div></div>"
        }
      }
    ],
    "fields": [
      { "type": "S", "name": "firstName" },
      { "type": "S", "name": "lastName" },
      { "type": "S", "name": "email" },
      { "type": "S", "name": "role" }
    ]
  }

Reading the template with the HTML un-escaped:

  <div style="display:grid; grid-template-columns: 1fr 2fr; gap: 8px 16px; align-items: center;">
    <label>First Name</label> <div class="firstName"></div>
    <label>Last Name</label>  <div class="lastName"></div>
    <label>Email</label>      <div class="email"></div>
    <label>Role</label>       <div class="role"></div>
  </div>

Each <div class="..."> is an injection site. When the editor renders, the firstName child editor is placed inside <div class="firstName">, the lastName child inside <div class="lastName">, and so on. CSS Grid arranges them as a two-column form.

Try it

The exact binding above, rendered live below. Edit the fields and watch the value update.

Current value:

{}

The resulting value

The binding's value is the same shape you'd get from a plain O binding, keyed by the child field names. If you want the output keyed by the human-readable label rather than a code-friendly identifier, name the field that way directly: "name": "First Name" is a valid field name.

Things to watch

  • Class name must equal field name. A <div class="firstname"> won't match a field named firstName — the match is case-sensitive.
  • Missing injection sites are silent. If your template has no element with a class matching a field's name, that field's editor is dropped from the UI. The field still exists in the binding's value, but the user can't edit it.
  • Inline styles are fine, but classes scale better. The example above uses inline styles to keep it self-contained. For anything bigger than a small form, add a stylesheet entry and apply a class.
  • Templates can also be functions. Instead of a string, template can be a JavaScript function function(dataType, value) { ... } that returns the HTML. Useful when the layout depends on the current value.

↑ Contents