Skip to content

Field Renderers

Every model field type maps to a built-in renderer that handles both form (editable) and list (read-only) contexts. You can override any renderer with a custom widget.

Built-in renderers

Form context

Field TypeRenders as
stringText input
textTextarea (auto-resize)
intNumber input (integer step)
decimalNumber input (decimal)
moneyNumber with currency prefix
booleanCheckbox
enumDropdown (searchable when > 8 options)
dateDate picker
datetimeDate + time picker
linkSearchable select (fetches from linked model)

List context

Field TypeDisplays as
stringPlain text (truncated)
textFirst line only
intFormatted number
decimalFormatted decimal
moneyWith currency symbol
booleanCheck icon
enumColored badge
dateLocale-formatted date
datetimeDate + time
linkClickable record label

Writing a custom renderer

Custom field renderers are widgets that live in modules/{module}/widgets/:

tsx
// modules/inventory/widgets/color-picker.tsx
import { defineWidget, registerWidget } from 'rangka';

const meta = defineWidget({
  name: 'inventory.color-picker',
  label: 'Color Picker',
  category: 'input',
  schema: {},
  binding: 'field',
  triggers: [],
  container: false,
});

registerWidget(meta, ({ bind }) => {
  const { value, setValue, meta: fieldMeta } = bind;

  if (!setValue) {
    return (
      <span className="inline-flex items-center gap-1.5">
        <span
          className="inline-block w-3 h-3 rounded-sm border border-default"
          style={{ backgroundColor: value || 'transparent' }}
        />
        {value || '—'}
      </span>
    );
  }

  return (
    <div className="flex items-center gap-2">
      <input
        type="color"
        value={value || '#000000'}
        onChange={(e) => setValue(e.target.value)}
        aria-label={fieldMeta?.label}
      />
      <input
        type="text"
        value={value || ''}
        onChange={(e) => setValue(e.target.value)}
        placeholder="#000000"
        readOnly={!setValue}
        className="input-base w-28"
      />
    </div>
  );
});

Assigning a renderer

Per field

Set renderer on the field in your model:

typescript
fields: {
  color: field.string({ renderer: 'inventory.color-picker' }),
}

Per type (global)

Register a widget that replaces the built-in for all fields of a given type:

typescript
import { registerFieldRenderer } from '@rangka/client';
import RichTextEditor from './widgets/rich-text-editor';

registerFieldRenderer('rich-text', RichTextEditor, { forTypes: ['LongText'] });

Resolution order

  1. Field-specific renderer property
  2. Global registry (registerFieldRenderer())
  3. Built-in renderer for the field type

Accessibility

Custom renderers should include:

  • aria-label or associated <label>
  • aria-invalid="true" when error is present
  • aria-describedby pointing to the error message
  • Keyboard operability
  • Visible focus ring