Custom fields
Avo ships with 20+ polished, ready-to-use fields out of the box.
When you need a field that isn't provided by default, Avo makes it easy to build your own.
Generate a new field
Every new field comes with three view components, Edit (which is also used in the New view), and Show and Index. There's also a Field configuration file.
bin/rails generate avo:field progress_bar generates the files for you.
INFO
Please restart your rails server after adding a new custom field.
▶ bin/rails generate avo:field progress_bar
create app/components/avo/fields/progress_bar_field
create app/components/avo/fields/progress_bar_field/edit_component.html.erb
create app/components/avo/fields/progress_bar_field/edit_component.rb
create app/components/avo/fields/progress_bar_field/index_component.html.erb
create app/components/avo/fields/progress_bar_field/index_component.rb
create app/components/avo/fields/progress_bar_field/show_component.html.erb
create app/components/avo/fields/progress_bar_field/show_component.rb
create app/avo/fields/progress_bar_field.rbThe ProgressBarField file is what registers the field in your admin.
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
def initialize(name, **args, &block)
super(name, **args, &block)
end
endNow you can use your field like so:
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :progress, as: :progress_bar
end
end
The generated view components are basic text fields for now.
<%# app/components/avo/fields/progress_bar_field/edit_component.html.erb %>
<%= field_wrapper **field_wrapper_args do %>
<%= @form.text_field @field.id,
class: classes("w-full"),
placeholder: @field.placeholder,
disabled: disabled?,
autofocus: @autofocus %>
<% end %>
<%# app/components/avo/fields/progress_bar_field/index_component.html.erb %>
<%= index_field_wrapper **field_wrapper_args do %>
<%= @field.value %>
<% end %>
<%# app/components/avo/fields/progress_bar_field/show_component.html.erb %>
<%= field_wrapper **field_wrapper_args do %>
<%= @field.value %>
<% end %>The field_wrapper_args, classes, and disabled? helpers come from the base components your field inherits from (Avo::Fields::EditComponent, ShowComponent, and IndexComponent), so they're available in every generated component.
You can customize them and add as much or as little content as needed. More on customization below.
Start from an existing field
There may be times when you want to duplicate an existing field and start from there.
To achieve this behavior, use the --field_template argument and pass the original field as a value.
Now, all components will have the exact same code (except the name) as the original field.
$ bin/rails generate avo:field super_text --field_template text
create app/components/avo/fields/super_text_field
create app/components/avo/fields/super_text_field/edit_component.html.erb
create app/components/avo/fields/super_text_field/edit_component.rb
create app/components/avo/fields/super_text_field/index_component.html.erb
create app/components/avo/fields/super_text_field/index_component.rb
create app/components/avo/fields/super_text_field/show_component.html.erb
create app/components/avo/fields/super_text_field/show_component.rb
create app/avo/fields/super_text_field.rbWe can verify that all components have the text field code. From here there are endless possibilities to extend the original field features.
# app/avo/fields/super_text_field.rb
module Avo
module Fields
class SuperTextField < BaseField
attr_reader :link_to_record
attr_reader :as_html
attr_reader :protocol
def initialize(id, **args, &block)
super(id, **args, &block)
add_boolean_prop args, :link_to_record
add_boolean_prop args, :as_html
add_string_prop args, :protocol
end
end
end
end
# lib/avo/fields/text_field.rb
module Avo
module Fields
class TextField < BaseField
attr_reader :link_to_record
attr_reader :as_html
attr_reader :protocol
def initialize(id, **args, &block)
super(id, **args, &block)
add_boolean_prop args, :link_to_record
add_boolean_prop args, :as_html
add_string_prop args, :protocol
end
end
end
endField options
This file is where you may add field-specific options.
# app/avo/fields/progress_bar_field.rb
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max
attr_reader :step
attr_reader :display_value
attr_reader :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = 100
@step = 1
@display_value = false
@value_suffix = nil
end
endThe field-specific options can come from the field declaration as well.
# app/avo/fields/progress_bar_field.rb
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max
attr_reader :step
attr_reader :display_value
attr_reader :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = args[:max] || 100
@step = args[:step] || 1
@display_value = args[:display_value] || false
@value_suffix = args[:value_suffix] || nil
end
end
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :progress, as: :progress_bar, step: 10, display_value: true, value_suffix: "%"
end
endField Visibility
If you need to hide the field in some view, you can use the visibility helpers.
# app/avo/fields/progress_bar_field.rb
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max
attr_reader :step
attr_reader :display_value
attr_reader :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = args[:max] || 100
@step = args[:step] || 1
@display_value = args[:display_value] || false
@value_suffix = args[:value_suffix] || nil
hide_on :forms
end
endWhat's available in your components
Each of the three generated components inherits from an Avo base component — Avo::Fields::EditComponent, Avo::Fields::ShowComponent, and Avo::Fields::IndexComponent (see the generated *_component.rb files). Those base classes expose the objects and helpers you use when rendering.
The most useful ones, available in the .html.erb templates:
| Variable / helper | Available in | What it is |
|---|---|---|
@field | all three | The field instance — your Avo::Fields::ProgressBarField. Read its value with @field.value and its id, placeholder, name, type, plus every attr_reader you add (@field.max, @field.step, …). |
@resource | all three | The resource the field belongs to. |
@index | all three | The field's position on the page. |
@view | all three | An Avo::ViewInquirer — @view.index?, @view.show?, @view.edit?, @view.new?. |
field_wrapper_args | all three | The hash to splat into the field wrapper: field_wrapper **field_wrapper_args. Passes @field, @resource, @view, and layout flags through for you. |
@form | Edit | The Rails form builder. Build inputs with @form.text_field @field.id, @form.range_field @field.id, etc. |
classes("extra") | Edit | Input CSS classes with error state, size, and any HTML overrides already applied. Pass extra classes as a string. |
disabled? | Edit, Show | true when the field is readonly or disabled. Prefer it over @field.readonly — it covers both cases. |
@stacked, @full_width, @density | vary by view | Layout flags forwarded from the resource; usually you just pass them through via field_wrapper_args. |
How the field wrapper ties it together
The first thing every generated component does is wrap your content in the field wrapper — field_wrapper on Show/Edit, index_field_wrapper on Index. This is the piece that makes a custom field look native: the wrapper renders the label, the required asterisk, the help text, the validation error, and the blank-— placeholder, and it applies the stacked / full_width / density layout around whatever you put inside the block. You render the value; the wrapper renders everything around it.
That's why you splat field_wrapper_args into it instead of building the label and spacing yourself — the base component already collected everything the wrapper needs. You can still pass extra options alongside it (field_wrapper **field_wrapper_args, full_width: true); the full list lives in the field wrappers reference.
WARNING
The wrapper renders a dash (—) instead of your block whenever @field.value is blank — it checks the value, not what your block actually renders. If your custom field draws something meaningful even when the value is nil, pass dash_if_blank: false or your content will never show.
Customize the views
Now let's do something about those views. Let's add a progress bar to the Index and Show views.
<%# app/components/avo/fields/progress_bar_field/show_component.html.erb %>
<%= field_wrapper **field_wrapper_args do %>
<!-- If display_value is set to true, show the value above the progress bar -->
<% if @field.display_value %>
<div class="text-center text-sm font-semibold w-full leading-none mb-1">
<!-- Add the suffix if value_suffix is set -->
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
</div>
<% end %>
<!-- Show the progress input with the settings we passed to the field. -->
<progress max="<%= @field.max %>" value="<%= @field.value %>" class="block w-full"></progress>
<% end %>
<%# app/components/avo/fields/progress_bar_field/index_component.html.erb %>
<%= index_field_wrapper **field_wrapper_args do %>
<!-- If display_value is set to true, show the value above the progress bar -->
<% if @field.display_value %>
<div class="text-center text-sm font-semibold w-full leading-none mb-1">
<!-- Add the suffix if value_suffix is set -->
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
</div>
<% end %>
<!-- Show the progress input with the settings we passed to the field. -->
<progress max="<%= @field.max %>" value="<%= @field.value %>" class="block w-24"></progress>
<% end %>
For the Edit view, we're going to do something different. We'll implement a range input. Pass autofocus: @autofocus to it so the field honors the autofocus option.
<%# app/components/avo/fields/progress_bar_field/edit_component.html.erb %>
<%= field_wrapper **field_wrapper_args do %>
<!-- Show the progress input with the settings we passed to the field. -->
<% if @field.display_value %>
<div class="text-center text-sm font-semibold w-full leading-none mb-1">
<!-- Add the suffix if value_suffix is set -->
<span class="js-progress-bar-value-<%= @field.id %>"><%= @field.value %></span><%= @field.value_suffix if @field.value_suffix.present? %>
</div>
<% end %>
<!-- Add the range input with the settings we passed to the field -->
<%= @form.range_field @field.id,
class: 'w-full',
placeholder: @field.placeholder,
disabled: disabled?,
autofocus: @autofocus,
min: 0,
# add the field-specific options
max: @field.max,
step: @field.step,
%>
<% end %>
<script>
// Get the input and value elements
var input = document.getElementById('project_progress');
// Scope the selector to the current field. You might have more than one progress field on the page.
var log = document.querySelector('.js-progress-bar-value-<%= @field.id %>');
// Add an event listener for when the input is updated
input.addEventListener('input', updateValue);
// Update the value element with the value from the input
function updateValue(e) {
log.textContent = e.target.value;
}
</script>
Field assets
Because there isn't just one standardized way of handling assets in Rails, we decided we won't provide asset loading support for custom fields for now. That doesn't mean that you can't use custom assets (javascript or CSS files), but you will have to load them in your own pipeline in dedicated Avo files.
In the example above, we added javascript on the page just to demonstrate the functionality. In reality, you might add that to a stimulus controller inside your own Avo dedicated pipeline.
Some styles were added in the asset pipeline directly.
progress {
@apply h-2 bg-white border border-gray-400 rounded shadow-inner;
}
progress[value]::-webkit-progress-bar {
@apply bg-white border border-gray-500 rounded shadow-inner;
}
progress[value]::-webkit-progress-value {
@apply bg-green-600 rounded;
}
progress[value]::-moz-progress-bar {
@apply bg-green-600 rounded appearance-none;
}Collapse long content on Show
If your field renders something tall — rich text, a rendered document, a long code listing — hand collapsable: true to the field wrapper. Avo clips the value to a short, faded preview with a More content link that expands it (and a Less content link that collapses it back), the same wrapper every WYSIWYG and markdown field uses, so your field reads like the built-in ones.
Give users a way out of the collapsing by accepting the always_show option on your field, the way the built-in rich text fields do:
# app/avo/fields/color_picker_field.rb
class Avo::Fields::ColorPickerField < Avo::Fields::BaseField
attr_reader :always_show
def initialize(id, **args, &block)
super(id, **args, &block)
@always_show = args[:always_show] || false
end
endThen pass it through, inverted, from the field's Show component:
<%# app/components/avo/fields/color_picker_field/show_component.html.erb %>
<%= field_wrapper(**field_wrapper_args, collapsable: !@field.always_show) do %>
<div style="background-color: <%= @field.value %>"
class="h-6 px-1 rounded-md text-white text-sm flex items-center justify-center leading-none"
>
<%= @field.value %>
</div>
<% end %>The wrapper only reveals the toggle when the content is actually taller than the preview, so short values render in full with no link.
INFO
collapsable applies on the Show view only — the field wrapper ignores it on Index and Edit.
Make an editor field resizable
If your field renders an editor on forms, declare the CSS selector of its scrollable viewport. Avo turns that element into a vertically resizable one with a drag handle, and remembers the height per field in the browser's local storage:
# app/avo/fields/my_editor_field.rb
class Avo::Fields::MyEditorField < Avo::Fields::BaseField
resizable_editor target: ".my-editor__content"
endThe selector is resolved inside the field wrapper after the editor boots, so editors that render themselves client-side work too.
Use pre-built Stimulus controllers
Avo ships with a few Stimulus controllers that help you build more dynamic fields.
Hidden input controller
This controller hides content behind a trigger that reveals it — a one-way reveal, unlike the collapsable wrapper above, and it makes no assumptions about how tall the content is. Avo uses it on the getting started screen.
Wrap the field in the controller tag, add the trigger, and give the value's container the content target plus the hidden class when always_show is false:
<%# app/components/avo/fields/color_picker_field/show_component.html.erb %>
<%= field_wrapper **field_wrapper_args do %>
<div data-controller="hidden-input">
<% unless @field.always_show %>
<%= link_to t('avo.show_content'), 'javascript:void(0);', class: 'font-bold inline-block', data: { action: 'click->hidden-input#showContent' } %>
<% end %>
<div <% unless @field.always_show %> class="hidden" <% end %> data-hidden-input-target="content">
<div style="background-color: <%= @field.value %>"
class="h-6 px-1 rounded-md text-white text-sm flex items-center justify-center leading-none"
>
<%= @field.value %>
</div>
</div>
</div>
<% end %>Non existing model field
To ensure proper rendering of a custom field that lacks getters and setters at the model level, you must implement these methods within the model.
def custom_field
end
def custom_field=(value)
endField methods
We won't be able to list all the methods available for a field here, but we've added a few methods to help you build better fields.
-> table_header_class
This adds a class to the th element of the table header. We added it when we needed to force a certain column to be a certain size, but you can use it for any purpose.
It defaults to nil
def table_header_class
"w-32"
end