Skip to content
Looking for every option?See the full API reference →

Has Many ​

The has_many field displays a model's associated records in a separate panel, with controls to browse, attach, create, and remove records according to the user's permissions.

ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
  def fields
    field :projects, as: :has_many
  end
end

By default, the panel appears below the regular fields on the Show view, loads lazily, and uses the resource inferred from the Rails association. If your app changes config.resource_default_view to :edit, the panel follows that setting. The attach modal uses a select input unless you enable another picker.

See the association overview for model setup and shared field options for visibility and other common settings.

Choose which records to display ​

Use scope to narrow the association table, for example to show only approved comments. Build on query, which already includes the target resource's policy scope.

Watch the demo video
ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
  def fields
    field :comments, as: :has_many, scope: -> { query.where(approved: true) }
  end
end

The block can also use the parent record to select the rows. It does not change which records appear in the attach picker.

A User feedback association scoped to matching comments on a User show page.

To display the same association more than once, use different field IDs and point for_attribute at the original association:

ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
  def fields
    field :comments, as: :has_many
    field :approved_comments, as: :has_many,
      for_attribute: :comments,
      scope: -> { query.where(approved: true) }
  end
end

Help users attach records ​

Use attach_scope to limit the attach picker, independently of the panel's rows. For example, offer only members not already assigned to this team:

ruby
# app/avo/resources/team.rb
class Avo::Resources::Team < Avo::BaseResource
  def fields
    field :members, as: :has_many,
      attach_scope: -> { query.where.not(team_id: parent.id) }
  end
end

Build on query to preserve the policy scope. A fresh relation can widen that scope, and the attach save accepts exactly the records returned by the block.

For large collections, enable searchable and follow Searchable associations to configure the target resource's search. For selecting several records without server-side search, set attach_using to :checkbox_list. Its inline search only filters records already loaded, subject to the global association lookup limit. Customize rows through the target resource's self.search[:item], using its title, description, image or avatar URL, image format, and image alt values. The picker uses the CheckboxList presentation.

Run code after attaching or detaching ​

Since avo >= v4.3.11

Use before_attach or before_detach to reject a change by raising a regular error. Use after_attach and after_detach for notifications or audit entries after the transaction commits.

ruby
# app/avo/resources/team.rb
class Avo::Resources::Team < Avo::BaseResource
  def fields
    field :members, as: :has_many,
      after_attach: -> { Rails.logger.info "#{current_user&.id} added #{record.id} to #{parent.id}" }
  end
end

These hooks run only through the association's Attach and Detach controls, not through Create, nested forms, deletes, actions, API calls, or model code. See the hook contracts for transaction and error behavior.

Attach through a join model ​

Avo recognizes has_many :through from the Rails model's reflection; a through: option on the Avo field is not needed. Attach and detach respect a scope on the through association:

ruby
# app/models/team.rb
class Team < ApplicationRecord
  has_many :admin_memberships, -> { where(level: :admin) }, class_name: "TeamMembership"
  has_many :admins, through: :admin_memberships, source: :user
end

Attaching a user stamps level: "admin" on the join record. Detaching removes that row without touching a level: "member" row linking the same records.

If the join model has extra attributes, declare them with attach_fields:

ruby
# app/avo/resources/store.rb
class Avo::Resources::Store < Avo::BaseResource
  def fields
    field :patrons, as: :has_many,
      attach_fields: -> {
        field :review, as: :text
      }
  end
end
Since avo >= v4.3.5

The fields describe the join record, so an association the join model declares belongs there too. With the join model declaring belongs_to :referrer, class_name: "User", optional: true, a belongs_to among the fields renders its usual picker in the attach modal and writes the join record's own foreign key:

ruby
field :patrons,
  as: :has_many,
  through: :patronships,
  attach_fields: -> {
    field :review, as: :text
    field :referrer, as: :belongs_to
  }

The picker offers what the target resource's policy scope holds, narrowed by that field's own attach_scope. A value matching no record there fails the whole attach, naming the field (Referrer is invalid), and writes nothing: no join record is created.

WARNING

If the through model uses polymorphism, include the required type as a hidden field inside attach_fields, for example field :patronship_type, as: :hidden, default: "TheType". Extra attach fields persist only for a through association.

Customize the panel ​

Change name for a custom panel title, and add description for explanatory text below it.

An association panel with its custom title highlighted.
An association panel with explanatory text highlighted beneath its title.

For a heavy collection, set loading to :manual. Users see a Load button instead of fetching the table immediately. An opened frame is remembered for 15 minutes by default; change auto_load_for to adjust this sliding window, or set it to 0 or nil to show the placeholder again on every visit. Failed requests display an inline Retry button. Global defaults live in config.associations.

WARNING

A callable description runs even on the manual placeholder. Avoid queries there, or branch on loading_type:

ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
  def fields
    field :orders, as: :has_many, loading: :manual,
      description: -> { loading_type == :manual ? "Orders" : "#{query.count} orders" }
  end
end

Add reloadable for a reload icon, or linkable for an icon that opens the association table on its own page in a new tab.

An association header with its reload icon highlighted.
A Memberships association panel with its open-in-new-tab icon highlighted.

Set per_page for a different page size, discreet_pagination to hide pagination details on a single page, hide_search_input to hide table search, or hide_filter_button to hide filters. These controls affect the table, not the attach picker.

If deleting a child changes a parent counter or total, enable refresh_on_destroy to reload the whole page instead of only the association table. The URL and success message are preserved.

Choose the resource for associated records ​

Set use_resource when the association should use a different Avo resource. For STI rows, use link_to_child_resource to route links to the matching child resource. See STI association setup, including configuring self.model_class on child resources.

Show the panel while editing ​

Add show_on: :edit to display the association panel on Edit as well as the default view. This still renders the show component, not an inline association editor. See field visibility.

Edit children with their parent ​

Install avo-nested to enable nested forms:

ruby
# Gemfile
gem "avo-nested", source: "https://packager.dev/avo-hq/"

Run bundle install; see Gem server authentication if needed. Configure Rails nested attributes on the parent model and enable nested on the field:

ruby
# app/models/book.rb
class Book < ApplicationRecord
  has_many :authors
  accepts_nested_attributes_for :authors, allow_destroy: true
end
ruby
# app/avo/resources/book.rb
class Avo::Resources::Book < Avo::BaseResource
  def fields
    field :authors, as: :has_many, nested: { on: :forms, limit: 2 }
  end
end

The child resource's edit fields appear within the parent form. nested: true enables both New and Edit; use on: :new or on: :edit to restrict it. The limit hides Add when reached, rather than validating a database constraint.

Delete children when saving the form ​

Since avo-nested >= v4.3.3

Unsaved rows can always be removed. Existing rows get Delete only when the model enables allow_destroy: true. Confirmation marks the row for deletion, dims it, and offers Undo; the record is destroyed only when the parent form saves. Translate the confirmation and pending notice through avo.nested.mark_for_deletion_confirmation and avo.nested.pending_deletion in your locale files.

Authorize nested changes ​

Existing children must be inside the child resource's policy scope and pass update?, both before and after submitted values are assigned. Rows outside the scope or initially denied updates are skipped; a refusal after assigning submitted values prevents the save. A deletion must pass destroy?; refusal prevents the save. New children need both the child policy's create? and the parent policy's create_authors?; refusal prevents the save and the parent check also hides Add.

With explicit_authorization enabled, define the parent's association creation method explicitly:

ruby
# app/policies/book_policy.rb
class BookPolicy < ApplicationPolicy
  def create_authors?
    user.admin?
  end
end

Control association actions ​

Use association policy methods to control attach, detach, create, destroy, and action controls. Hiding a search or filter control is not authorization.

An association panel with its header controls and row actions highlighted.

Options reference ​

See the Has Many API for every association-specific option and callback contract, shared field options for common field settings, and Searchable associations for picker configuration.

OptionPurpose
scopeFilter listed rows.
attach_scopeFilter attach choices.
searchableSearch-as-you-type picker.
attach_usingCheckbox-list attach picker.
attach_fieldsExtra join-model fields.
namePanel title.
descriptionPanel explanatory text.
loadingLazy or manual frame loading.
use_resourceChoose an Avo resource.
link_to_child_resourceSTI row links.
linkableOpen the table in another tab.
reloadableReload the frame.
for_attributeReuse a model association.
per_pageAssociation page size.
discreet_paginationHide single-page pagination details.
hide_search_inputHide table search.
hide_filter_buttonHide table filters.
refresh_on_destroyReload the page after deletion.
nestedEdit children within the parent form.