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