Fields

As of 0.4.0, every field type — built-in or unknown — resolves through a single rendering path (a total registry keyed by type:). An unrecognized type: no longer errors: it renders a plain text field, the same fallback built-ins like :text and :string use. This is a mechanism change only; recognized types render exactly as before.

Fields are defined in the resource form do ... end block:

form do
  field :name
  field :status, type: :select, collection: [["Active", "active"], ["Inactive", "inactive"]]
end

Common options

All fields support:

  • type: (defaults to :text)
  • required: (true/false)
  • label: (String)
  • help: (String)
  • placeholder: (String)
  • readonly: (true/false)
  • if: Proc (render only if truthy)
  • unless: Proc (render only if falsy)

Example conditional field:

field :admin_notes, type: :textarea, if: ->(record) { record.admin? }

Supported field types

Text-like

  • :text (default)
  • :textarea (rows: supported)
  • :email
  • :url
  • :number
  • :date
  • :time
  • :datetime

Toggle

  • :toggle (renders a switch)
field :enabled, type: :toggle

Select

  • :select (uses Rails select)

Options:

  • collection: Array of [label, value] or simple values
field :status, type: :select, collection: [["Active", "active"], ["Inactive", "inactive"]]

Searchable select

  • :searchable_select (Stimulus-powered searchable dropdown)

Options:

  • collection: either:
    • an Array (static options), or
    • a String URL (advanced; used by the JS controller as a “search URL”, always wins over resource: below when present)
  • resource: (Symbol/String, new in 0.5.0) — the AdminSuite resource key to search remotely (e.g. resource: :companies); see Remote search via resource: below
  • create_url: (String) enables “creatable” behavior in the UI
field :company_id,
  type: :searchable_select,
  collection: Company.order(:name).pluck(:name, :id),
  placeholder: "Search companies..."

Remote search via resource:

As of 0.5.0, AdminSuite provides its own JSON search endpoint, so hosts no longer need to hand-build a search action for searchable_select. Give the field a resource: instead of a static collection: array:

field :company_id, type: :searchable_select, resource: :companies

This resolves (at render time, by looking up Admin::Base::Resource .registered_resources for a matching plural or singular resource name) to GET <mount>/:portal/:resource_name/search?q=<term> — the same route the resource’s own admin pages are served from. If resource: doesn’t resolve (a typo, an unregistered key, or a resource with no portal_name), the field renders with no remote search configured rather than raising — it degrades the same way a bad collection: URL would.

A String collection: still overrides this unconditionally — set both and collection: wins, matching pre-0.5.0 behavior exactly.

The endpoint’s contract (GET .../:resource_name/search?q=term):

  • Requires authentication (the engine’s existing admin_suite_authenticate!) and config.authorize with action: :read — a denied actor gets 403, same as any other resource action.
  • Only searches the resource’s declared searchable fields (index do searchable :name, :email end) — never an arbitrary column, and never the whole table.
  • Hard-capped at 25 results, regardless of how many rows match.
  • An unknown/unregistered resource name in the URL 404s, same as every other resource route.
  • A blank or missing q returns [] — never the unfiltered table (this differs slightly from the index page’s own search box, which falls back to the unfiltered scope on a blank query; a raw JSON endpoint must not).
  • A resource with no searchable fields declared (or no index block at all) also returns [].
  • Response shape is a bare JSON array of { "id": ..., "name": ... } objects — name falls back to title, then to_s, a smaller version of the same “try a few label-ish methods” idea item_display_title uses elsewhere in the gem (that one also tries display_title and a truncated content before falling back to "##{id}"; this endpoint doesn’t need that many rungs, since a record with none of name/title just falls through to its own #to_s). This is exactly what searchable_select_controller.js’s fetchOptions already expects (item.id, item.name), so no JS change is required.

The search term is always bound (never interpolated) into the SQL, so the endpoint carries the same injection-safety guarantee as the index’s existing search box. It also inherits that same box’s ILIKE characteristic that %/_ in the value act as SQL wildcards — worth knowing if this field is exposed to less-trusted input than the index page’s own search box.

Multi-select & tags

  • :multi_select
  • :tags

Options:

  • collection: Array of options (used for suggestions)
  • create_url: enables “creatable” behavior
  • multiple: boolean (reserved; arrays are permitted automatically)

Notes:

  • These submit arrays and are permitted automatically by AdminSuite.
  • For :tags, AdminSuite uses a tag_list parameter by default (or #{field_name}_list if your model exposes it).
field :tag_list, type: :tags, placeholder: "Add tags..."
field :roles, type: :multi_select, collection: %w[admin editor viewer]

File uploads / attachments

  • :file
  • :attachment
  • :image

Options:

  • accept: MIME accept string (e.g. "image/*", "application/pdf")

These assume your host app uses Active Storage.

field :avatar, type: :image, accept: "image/*"
field :resume, type: :file, accept: "application/pdf"

Rich text

  • :trix
  • :rich_text

These assume your host app uses Action Text.

field :bio, type: :rich_text

Markdown

  • :markdown (textarea enhanced by EasyMDE, vendored with the engine — no CDN, works air-gapped and under a strict CSP)
field :prompt_template, type: :markdown, rows: 16

JSON editor

  • :json (renders the engine’s JSON editor partial)
field :settings, type: :json

Code editor

  • :code (monospace editor container; enhanced by engine JS)
field :ruby_code, type: :code, rows: 20

Label (read-only)

  • :label renders a badge-like value (useful for status fields)

Options:

  • label_color: Symbol or Proc
  • label_size: :sm/:md or Proc
field :status, type: :label, label_color: ->(r) { r.active? ? :emerald : :slate }, label_size: :sm

Layout helpers

Inside form do ... end you can group fields:

section

section "Billing", description: "Payment settings", collapsible: true do
  field :stripe_customer_id, readonly: true
end

row

row cols: 2 do
  field :first_name
  field :last_name
end