Skip to content

Has And Belongs To Many

The HasAndBelongsToMany association works similarly to HasMany.

ruby
field :users, as: :has_and_belongs_to_many

Options

searchable

Turns the attach field/modal from a <select> into a search-as-you-type picker.

ruby
class Avo::Resources::CourseLink < Avo::BaseResource
  def fields
    field :links,
      searchable: true
  end
end

See Searchable associations for setup requirements, the hash form (searchable: { query:, item:, enabled: }), proc locals, and precedence rules.

Default

false

Possible values

true, false, Hash

attach_scope

Scope out the records the user sees on the Attach modal.

Default

nil

Possible values

ruby
field :user,
  as: :belongs_to,
  attach_scope: -> { query.non_admins }

Pass in a block where you attach scopes to the query object and parent object, which is the actual record where you want to assign the association. The block is executed in the ExecutionContext.

WARNING

The attach_scope will not filter the records in the listing from has_many or has_and_belongs_to_many associations. Use scope or a Pundit policy Scope for that.

ruby
field :members,
  as: :has_and_belongs_to_many,
  attach_scope: -> { query.where.not(team_id: parent.id) }

In this example, in the attach_scope, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options.

scope

Scope out the records displayed in the table.

Default

nil

Possible values

ruby
field :user,
  as: :belongs_to,
  scope: -> { query.approved }

Pass in a block where you attach scopes to the query object. The block gets executed in the ExecutionContext.

With version 2.5.0, you'll also have access to the parent record so that you can use that to scope your associated models even better.

Starting with version 3.12, access to resource and parent_resource was additionally provided.

name

Changes the text displayed as association name.

Default

Plural association name.

Possible values

Any string or any zero arity lambda function.

Within lambda, you have access to all attributes of Avo::ExecutionContext.

description

Changes the text displayed under the association name.

Avo

Default

nil

Possible values

Any string or any zero arity lambda function.

Within lambda, you have access to query and all attributes of Avo::ExecutionContext.

Evaluated on page load, even with loading: :manual

The description lambda is evaluated to render the association header — including the placeholder shown for a loading: :manual association. So a lambda that touches the database (e.g. -> { "#{query.count} posts" }) runs on every page load, before the user clicks Load. If you reached for loading: :manual to defer that query, keep the description cheap (or omit it) — only the framed content is fetched on demand.

loading

Controls how the association's content frame is loaded on the Show page.

By default an association loads its content as soon as its turbo-frame is revealed (the lazy behavior). The loading option lets you defer that fetch until the user explicitly asks for it, which is useful for heavy associations you don't want to load on every page view.

The default for every association is set globally via config.associations (loading: :lazy out of the box). Set loading: on a field to override that default per association.

Possible values

ValueBehavior
:manualRenders a placeholder with a Load button. Nothing is fetched until the user clicks it. Once opened, the frame is remembered for 15 minutes by default (see auto_load_for).
{ mode: :manual }Same as :manual.
{ mode: :manual, auto_load_for: 5.minutes }Manual with a custom sliding memory window — once opened, the frame auto-loads (no placeholder, no button) on return visits for the given duration.
{ mode: :manual, auto_load_for: 0 }Manual with no memory — the placeholder returns on every visit (0 or nil opts out).
:lazy / { mode: :lazy }Native lazy loading (loads when the frame is revealed).

Manual loading

ruby
field :orders, as: :has_many, loading: :manual

The placeholder shows the association title, the (optional) description, and a Load button. On click, the real content is fetched into the frame. If the request fails (500, 404, network error), an inline error with a Retry button is rendered inside the frame instead of redirecting to the global failed-to-load page.

For a has_one association whose value is nil, the existing attach/create empty state is shown instead of a placeholder.

Remembering an opened frame

Once the user opens a manual frame, Avo remembers it for 15 minutes by default and skips the placeholder on return visits — the frame auto-loads directly. Pass auto_load_for to change that window:

ruby
field :orders, as: :has_many, loading: { mode: :manual, auto_load_for: 5.minutes }

The window is a sliding memory, not a delay: a short-lived cookie scoped per record + frame remembers the opened frame, and every return visit within the window refreshes it. Once the window lapses, the placeholder + Load button return.

Set auto_load_for: 0 (or nil) to opt out entirely — the placeholder then returns on every visit:

ruby
field :orders, as: :has_many, loading: { mode: :manual, auto_load_for: 0 }

The default window is configurable globally via config.associations.

Keep the description cheap with loading: :manual

The description lambda is evaluated to render the placeholder, before the user clicks Load — so a description that touches the database (e.g. -> { "#{query.count} orders" }) defeats the purpose of deferring the load. Branch on the loading_type attribute to skip the expensive work on the placeholder:

ruby
field :orders, as: :has_many, loading: :manual,
  description: -> { loading_type == :manual ? "Orders" : "#{query.count} orders" }

loading_type is :manual while the placeholder is shown and nil in every other render context.

use_resource

Sets a different resource to be used when displaying (or redirecting to) the association table.

Default

nil. When nothing is selected, Avo infers the resource type from the reflected association.

Possible values

Avo::Resources::Post, Avo::Resources::PhotoComment, or any Avo resource class.

The value can be the actual class or a string representation of that class.

ruby
# the class
Avo::Resources::Post

# the string representation of the class
"Avo::Resources::Post"

attach_using

By default, non-searchable has_and_belongs_to_many attach modals use a select input.

Use attach_using: :checkbox_list to render the attach modal with a checkbox list instead. Users can select more than one record and attach them in one submit.

The attach modal uses the same option shape and row presentation as the CheckboxList field.

ruby
field :users,
  as: :{{ $frontmatter.field_type }},
  attach_using: :checkbox_list

The checkbox list respects attach_scope and the global association lookup limit. It includes inline client-side search over the records already loaded in the modal.

If the related resource defines self.search[:item], Avo uses its title, description, image_url or avatar_url, image_format, and image alt values for each row.

ruby
class Avo::Resources::User < Avo::BaseResource
  self.search = {
    item: -> do
      {
        title: record.name,
        description: record.email,
        image_url: record.avatar_url,
        image_format: :circle
      }
    end
  }
end

discreet_pagination

Hides the pagination details when only there's only one page for that association.

Default

false

Possible values

true, false

hide_search_input

Hides the search input displayed on the association table.

Default

false. When nothing is selected and the target resource's self.search[:query] is configured, Avo displays the search input.

Possible values

true, false.

hide_filter_button

Hides the filters button displayed on the association table.

Default

false. The filters button is displayed by default and only hidden when hide_filter_button is set to true

Possible values

true, false.

Show on edit screens

By default, the has_and_belongs_to_many field is only visible in the show view. To make it available in the edit view as well, include the show_on: :edit option. This ensures that the has_and_belongs_to_many show view component is also rendered within the edit view.

Nested in Forms

You can use "Show on edit screens" to make the has_and_belongs_to_many field available in the edit view. However, this will render it using the show view component.

To enable nested creation for the has_and_belongs_to_many field, allowing it to be created and / or edited alongside its parent record within the same form, use the nested option which is a hash with configurable option.

The avo-nested gem

Nested association forms are provided by the avo-nested gem. Add it to your Gemfile using the same private gem source as your other Avo paid gems:

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

Run bundle install. If you have not set up packager.dev access yet, see Gem server authentication.

Keep in mind that this will display the field’s resource as it appears in the edit view.

nested

Enables this field as a nested form in the specified views.

Default value

{}

Possible values

A hash with the following options:

  • on: Views in which to enable nesting. Accepted values:
    • :new - Enables nesting in the new view.
    • :edit - Enables nesting in the edit view.
    • :forms - Enables nesting in the new and edit views.
  • limit: (Only for has_many and has_and_belongs_to_many fields) Hides the "Add" button when the specified limit is reached.

TIP

Setting nested: true is a shortcut for nested: { on: :forms }.

Example

ruby
# app/avo/resources/book.rb
class Avo::Resources::Book < Avo::BaseResource
  def fields
    # Shortcut for full nesting
    field :authors, as: :has_and_belongs_to_many, nested: true

    # Explicit nesting on new only
    field :authors, as: :has_and_belongs_to_many, nested: { on: :new }

    # Explicit nesting on edit only
    field :authors, as: :has_and_belongs_to_many, nested: { on: :edit }

    # Explicit nesting on both new and edit
    field :authors, as: :has_and_belongs_to_many, nested: { on: :forms }

    # Limit nested creation (for has_many or has_and_belongs_to_many only)
    field :authors,
      as: :has_and_belongs_to_many,
      nested: { on: [:new, :edit], limit: 2 }
  end
end

Add scopes to associations

Watch the demo video

When displaying has_many associations, you might want to scope out some associated records. For example, a user might have multiple comments, but on the user's Show page, you don't want to display all the comments, but only the approved ones.

ruby
# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :user, optional: true

  scope :approved, -> { where(approved: true) }
end

# app/models/user.rb
class User < ApplicationRecord
  has_many :comments
end

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

The comments query on the user Index page will have the approved scope attached.

Association scope

With version 2.5.0, you'll also have access to the parent record so that you can use that to scope your associated models even better.

Starting with version 3.12, access to resource and parent_resource was additionally provided.

All the has_many associations have the attach_scope option available too.

Show/hide buttons

You will want to control the visibility of the attach/detach/create/destroy/actions buttons visible throughout your app. You can use the policy methods to do that.

Find out more on the authorization page.

Associations authorization

Reloadable

The reloadable option adds a reload icon next to the association title so users can easily reload just that turbo-frame instead of doing a full page reload.

Usage

To enable the reloadable feature, you have two options:

  1. Direct Boolean Value:

Provide a boolean value directly to the reloadable option. This sets a default behavior where the reloadable feature is either enabled or disabled based on this boolean value.

ruby
field :reviews, as: :has_and_belongs_to_many, reloadable: true
  1. Dynamic Conditions with a Block:

For more dynamic behavior, you can provide a block to the reloadable option. Within this block, you can specify conditions under which the reloadable should be displayed.

ruby
field :reviews, as: :has_and_belongs_to_many,
  reloadable: -> {
    current_user.is_admin?
  }

In the above example, the reloadable will be visible if the current_user is an admin.

ExecutionContext

The reloadable block executes within the ExecutionContext, granting access to all default methods and attributes.

Reloadable

association

The for_attribute option allows to specify the association used for a certain field. This option make possible to define same association with different scopes and different name several times on the same resource.

Usage

ruby
field :reviews,
  as: :has_and_belongs_to_many

field :special_reviews,
  as: :has_and_belongs_to_many,
  for_attribute: :reviews,
  scope: -> { query.special_reviews }