Skip to content

Customizing Fields

Terrazzo ships with 19 field types. Each field knows how to serialize itself for JSON and has three display modes: index (table cell), show (detail view), and form (input).

Setting Options

Use .with_options to configure any field:

ruby
ATTRIBUTE_TYPES = {
  name: Field::String.with_options(truncate: 30),
  price: Field::Number.with_options(prefix: "$", decimals: 2),
  status: Field::Select.with_options(collection: -> { MyModel.statuses.keys }),
  author: Field::BelongsTo.with_options(scope: -> { User.where(role: :author) }),
}

Field Types

Field::String

ruby
name: Field::String
  • Index: Truncated to 50 characters (configurable with truncate option)
  • Show: Full text
  • Form: Text input

Options: truncate (default: 50)

Field::Text

ruby
description: Field::Text
  • Index: Truncated
  • Show: Full text
  • Form: Textarea

Field::Number

ruby
price: Field::Number.with_options(prefix: "$", decimals: 2)

Options: prefix, suffix, decimals, multiplier, format

The format option uses ActiveSupport::NumberHelper for advanced formatting:

ruby
price: Field::Number.with_options(
  format: { formatter: :number_to_currency, formatter_options: { unit: "€" } }
)
percentage: Field::Number.with_options(
  format: { formatter: :number_to_percentage, formatter_options: { precision: 1 } }
)

Field::Money

ruby
price: Field::Money
amount: Field::Money.with_options(prefix: "€", decimals: 2)

Like Number but defaults to 2 decimal places. Options: prefix, suffix, decimals. Money uses the number frontend renderer, so eject fields/number when you want to customize built-in money rendering.

Field::Boolean

ruby
active: Field::Boolean
  • Index/Show: Check or X badge
  • Form: Checkbox

Field::Date / Field::DateTime / Field::Time

ruby
published_on: Field::Date
created_at: Field::DateTime
starts_at: Field::Time

Options:

  • format — Ruby strftime format string (e.g., "%b %d, %Y"). When set, the value is formatted server-side.
  • timezone — Timezone name (e.g., "Eastern Time (US & Canada)") to convert the value before formatting. Defaults to Time.zone.

Field::Email

ruby
email: Field::Email

Renders as a clickable mailto: link on index and show pages.

Field::Url

ruby
website: Field::Url

Renders as an external link on index and show pages.

Field::Password

ruby
password: Field::Password

Renders as •••••••• on index and show pages. Password input on forms.

Field::Select

ruby
status: Field::Select.with_options(
  collection: %w[draft published archived]
)

Options: collection — array, proc, or ActiveRecord enum name.

Field::RichText

ruby
body: Field::RichText

For Action Text fields. Renders HTML on show, textarea on form.

Field::BelongsTo

ruby
customer: Field::BelongsTo
  • Index/Show: Link to the associated record
  • Form: Select dropdown with all available records

Options: scope (proc to filter records), include_blank, order (e.g., order: :name to sort dropdown options)

Field::HasMany

ruby
orders: Field::HasMany
  • Index: Count badge
  • Show: Table with collection attributes and per-row actions
  • Form: Multi-select

Options:

  • collection_attributes — array of attributes to show in the has_many table on the show page. Defaults to the associated dashboard's COLLECTION_ATTRIBUTES. Use this to show fewer or different columns:
    ruby
    orders: Field::HasMany.with_options(
      collection_attributes: [:id, :address_city, :created_at]
    )
  • scope — proc to filter records
  • per_page — number of rows per page in the nested show-page table. Defaults to 5; invalid values fall back to 5.
  • sort_by — attribute to sort related items on show page (e.g., sort_by: :created_at)
  • direction — sort direction, :asc (default) or :desc
  • includes — eager-load nested associations (e.g., includes: [:author])

Field::HasOne

ruby
profile: Field::HasOne
  • Index/Show: Link to the associated record
  • Form: Read-only

Options: scope

Field::Polymorphic

ruby
commentable: Field::Polymorphic.with_options(
  classes: ["Post", "Comment"]
)
  • Form: Grouped select (by type, then by record)

Options:

  • classes — array of model class names this association can point to
  • order — sort candidate resources per class (e.g., order: :name)

Field::Hstore

ruby
metadata: Field::Hstore
settings: Field::Hstore.with_options(truncate: 50)

Renders PostgreSQL hstore columns as interactive key-value pair editors. Index shows a truncated preview, show displays each key-value pair with badges, and the form provides an add/remove row editor.

Options: truncate (default: 80)

Field::Asset

ruby
avatar: Field::Asset

For Active Storage has_one_attached fields. Shows the filename on index and show pages, file input on forms. The dashboard generator auto-detects has_one_attached declarations.

All field types default to searchable: false. Enable search on specific fields using .with_options(searchable: true):

ruby
ATTRIBUTE_TYPES = {
  name:  Field::String.with_options(searchable: true),
  email: Field::Email.with_options(searchable: true),
  owner: Field::BelongsTo.with_options(searchable: true, searchable_fields: ["name"]),
}

For association fields, searchable_fields lists columns on the associated table. If you omit it, Terrazzo searches the first conventional display column it finds: name, title, or email. Association searches return unique parent records even when multiple associated rows match. Search treats %, _, and backslashes as literal characters, not SQL wildcards.

Note: Unlike Administrate, Terrazzo does not auto-enable search on String/Text/Email fields. This avoids accidentally exposing sensitive data in search queries. Always opt in explicitly.

Field Capabilities

TypeClassSearchableSortableEager Load
StringField::StringNoYesNo
TextField::TextNoYesNo
NumberField::NumberNoYesNo
MoneyField::MoneyNoYesNo
BooleanField::BooleanNoYesNo
DateField::DateNoYesNo
DateTimeField::DateTimeNoYesNo
TimeField::TimeNoYesNo
EmailField::EmailNoYesNo
URLField::UrlNoYesNo
PasswordField::PasswordNoNoNo
SelectField::SelectNoYesNo
Rich TextField::RichTextNoNoYes
BelongsToField::BelongsToNoYesYes
HasManyField::HasManyNoNoYes
HasOneField::HasOneNoNoYes
PolymorphicField::PolymorphicNoNoYes
HstoreField::HstoreNoNoNo
AssetField::AssetNoNoYes

Creating a Custom Field

Use the field generator to scaffold a custom field type:

bash
rails g terrazzo:field Gravatar

This creates:

  • app/fields/terrazzo/field/gravatar.rb — Ruby field class
  • app/views/admin/fields/gravatar/IndexField.jsx — table cell component
  • app/views/admin/fields/gravatar/ShowField.jsx — detail view component
  • app/views/admin/fields/gravatar/FormField.jsx — form input component

It also creates or updates the app-level field and UI barrels, then registers the components in app/views/admin/fields/index.js so both packaged pages and ejected pages can render the custom field. The generated admin entrypoint imports this barrel before Superglue renders.

The Ruby class

The generated class inherits from Field::Base. Override serialize_value(mode) to control what data is sent to the frontend for each mode (:index, :show, :form):

ruby
module Terrazzo
  module Field
    class Gravatar < Base
      def serialize_value(mode)
        return nil if data.blank?

        email = data.downcase.strip
        hash = Digest::MD5.hexdigest(email)
        case mode
        when :index
          { url: "https://gravatar.com/avatar/#{hash}?s=32", email: email }
        when :show
          { url: "https://gravatar.com/avatar/#{hash}?s=128", email: email }
        when :form
          email
        end
      end

      class << self
        def searchable?
          false
        end

        def sortable?
          false
        end
      end
    end
  end
end

Use serializable_options to pass field configuration to the frontend:

ruby
def serializable_options
  { size: options.fetch(:size, 128) }
end

The JSX components

Each component receives the serialized value and options as props. Form components also receive attribute, label, hint, input, and required. Index components receive cellOptions when the dashboard returns metadata from collection_cell_options.

For example, ShowField.jsx:

jsx
import React from "react";

export function ShowField({ value, options }) {
  if (!value) return null;
  return (
    <img
      src={value.url}
      alt={value.email}
      className="rounded-full"
      width={options.size || 128}
    />
  );
}

Using the field

Reference it in your dashboard like any built-in field:

ruby
ATTRIBUTE_TYPES = {
  email: Field::Gravatar,
  # or with options:
  email: Field::Gravatar.with_options(size: 64),
}

Base Field API

All field types inherit from Field::Base.

MethodDescription
#field_typeReturns the underscored type name (e.g., "string", "belongs_to")
#serialize_value(mode)Returns the serialized value for :index, :show, or :form mode
#serializable_optionsReturns a hash of options sent to the React component
#required?true if the model has a presence validator on this attribute
.searchable?Whether this field type supports search (default: false)
.sortable?Whether this field type supports sorting (default: true)
.eager_load?Whether to eager-load this association (default: false)
.with_options(opts)Returns a deferred field with merged options
.permitted_attributeReturns the attribute name for strong parameters

Released under the MIT License.