Has And Belongs To Many API
Per-option reference for has_and_belongs_to_many fields; see the guide for tasks and worked examples.
Options are keyword arguments to field inside a resource's fields method in app/avo/resources/team.rb:
field :users, as: :has_and_belongs_to_many, attach_using: :checkbox_listShared field options, including visibility, are documented in Field options. All callbacks below run in ExecutionContext, with current_user, params, and the standard context helpers. Listed locals are additional named methods available inside zero-argument blocks, not positional arguments.
Attach picker
-> searchable
Enables the searchable attach picker. Its query, item rendering, enabled callback, licensing requirements, and precedence rules are defined in Searchable associations and its API.
field :users, as: :has_and_belongs_to_many, searchable: true- Type:
BooleanorHashwithquery,item, andenabledoverrides. - Default:
nil(non-searchable). - Behavior: When enabled, takes precedence over
attach_using.
-> attach_using
Selects the input for a non-searchable attach modal. The checkbox list supports attaching several records at once and client-side search of loaded options. It respects attach_scope and the global association lookup limit, and uses the target resource's self.search[:item] presentation when available.
field :users, as: :has_and_belongs_to_many, attach_using: :checkbox_list| Value | Behavior |
|---|---|
nil | Standard select input. |
:checkbox_list | Multi-selection checkbox list. |
- Type:
SymbolorString. - Default:
nil. - Validation: The value is converted with
to_sym; only:checkbox_listselects the checkbox list. Other symbolizable values fall back to the select input.
-> attach_scope
Filters the attach candidates and the records accepted on attach submission, not the existing association table. Building on query preserves its policy scope; returning a fresh relation can widen it, including the records accepted on save.
field :users, as: :has_and_belongs_to_many,
attach_scope: -> { query.where.not(id: parent.users.select(:id)) }- Type:
Proc/Lambdareturning an Active Record relation. - Default:
nil. - Locals:
query(policy-scoped candidate relation),parent(owning record). - Validation: A submitted record outside the returned relation is rejected as not found.
Displayed records and resource selection
-> scope
Narrows the association table's relation after the child policy scope has been applied. Does not constrain the attach picker.
field :users, as: :has_and_belongs_to_many, scope: -> { query.where(active: true) }- Type:
Proc/Lambdareturning an Active Record relation. - Default:
nil. - Locals:
query(policy-scoped associated records),parent(owning record),resource(associated resource),parent_resource(owning resource).
-> use_resource
Overrides the Avo resource used for the association table and related navigation.
field :users, as: :has_and_belongs_to_many, use_resource: "Avo::Resources::User"- Type: Avo resource class or its
Stringclass name. - Default:
nil(inferred from the reflected association model).
-> for_attribute
Maps the field ID to a different Rails association name, allowing several scoped fields for the same association.
field :active_users, as: :has_and_belongs_to_many,
for_attribute: :users, scope: -> { query.where(active: true) }- Type:
SymbolorString. - Default:
nil(uses the field ID).
-> link_to_child_resource
Chooses child-resource links for STI records when that resource exists. See STI associations.
field :users, as: :has_and_belongs_to_many, link_to_child_resource: true- Type:
Boolean. - Default:
nil(inherits the associated resource'slink_to_child_resourcesetting). - Values:
trueuses the child resource when available;falsekeeps the association resource.
Panel presentation
-> name
Overrides the association label. The panel uses its plural form, with plural translations taking precedence when available.
field :users, as: :has_and_belongs_to_many, name: "Team members"- Type:
StringorProc/Lambdareturning a string. - Default:
nil(resolved from field translations, the selected resource's name, or the humanized field ID). - Locals: Standard ExecutionContext only; no additional record or resource local is supplied to the name callback.
-> description
Text beneath the association title, including on a manual-loading placeholder.
field :users, as: :has_and_belongs_to_many,
description: -> { loading_type == :manual ? "Team members" : "#{query.count} members" }- Type:
StringorProc/Lambdareturning a string. - Default:
nil. - Locals:
query(the parent's association relation),loading_type(:manualfor the placeholder, otherwisenil). Thisqueryis not the policy-scoped table query.
WARNING
A description that counts or loads query executes that database work before the user clicks Load. Manual loading defers only the framed table, not the header description.
-> per_page
Sets the association's page size and includes it in the table's per-page dropdown. An explicit request parameter takes precedence, then a saved session value when pagination persistence is enabled, then this option, then config.via_per_page.
field :users, as: :has_and_belongs_to_many, per_page: 24- Type: Positive
Integer. - Default:
nil(usesconfig.via_per_page). - Persistence: Saved sizes are keyed by parent and associated resource, not field; fields with the same pair share the saved value.
-> discreet_pagination
Hides pagination details when the table has only one page.
field :users, as: :has_and_belongs_to_many, discreet_pagination: true- Type:
Boolean. - Default:
nil(disabled).
-> hide_search_input
Hides the association table's search input, independently of the attach picker. When not hidden, the input still requires the associated resource's search query configuration.
field :users, as: :has_and_belongs_to_many, hide_search_input: true- Type:
Boolean. - Default:
nil(not hidden).
-> hide_filter_button
Hides the association table's filter button.
field :users, as: :has_and_belongs_to_many, hide_filter_button: true- Type:
Boolean. - Default:
nil(not hidden).
-> linkable
Adds an open-in-new-tab icon beside the title, linking to the association table on its own page.
field :users, as: :has_and_belongs_to_many, linkable: true- Type:
BooleanorProc/Lambda. - Default:
nil(disabled). - Locals: Standard ExecutionContext only; no additional locals.
Loading and refreshing
-> loading
Controls how the table's Turbo Frame loads. A field value overrides the global association frame defaults.
field :users, as: :has_and_belongs_to_many,
loading: { mode: :manual, auto_load_for: 5.minutes }| Value | Behavior |
|---|---|
:lazy or { mode: :lazy } | Loads when the frame is revealed. |
:manual or { mode: :manual } | Shows Load until opened; remembers the opened frame. |
{ auto_load_for: 5.minutes } | Hash without mode defaults to manual. |
- Type:
Symbol,String, orHashwith symbol or string keys. - Default:
nil(inheritsconfig.associations[:frames][:loading], initially:lazy). - Validation: Mode values are converted to symbols, without enum validation. Supported modes are
:manualand:lazy.
-> mode
Selects manual or lazy loading in the Hash form.
field :users, as: :has_and_belongs_to_many, loading: { mode: :manual }- Type:
SymbolorString, converted to a symbol. - Default:
:manualwhen the Hash omitsmode.
-> auto_load_for
Sliding memory window for an opened manual frame, in seconds. A cookie scoped per record and frame refreshes on return visits within the window. This is not a delay; outside the window the placeholder returns. It has no effect on lazy frames.
field :users, as: :has_and_belongs_to_many,
loading: { mode: :manual, auto_load_for: 0 }- Type:
Integerseconds,ActiveSupport::Duration, ornil. - Default:
config.associations[:frames][:auto_load_for], falling back to15.minutes. - Values: A positive duration remembers the frame;
0,nil, and negative values disable memory. Values are converted usingto_i.
-> reloadable
Adds an icon beside the title to reload only the association frame.
field :users, as: :has_and_belongs_to_many, reloadable: -> { current_user.admin? }- Type:
BooleanorProc/Lambda. - Default:
nil(disabled). - Locals: Standard ExecutionContext only; no additional locals.
-> refresh_on_destroy
Refreshes the entire page after a record is deleted from the table, keeping the URL and success message. This is deletion, not detachment.
field :users, as: :has_and_belongs_to_many, refresh_on_destroy: true- Type:
Boolean. - Default:
nil(only the association table refreshes).
Attach and detach callbacks
Sinceavo >= v4.3.11All four hooks apply only to the association Attach and Detach controls. They do not run for Create, Delete, nested forms, actions, or model changes. Each receives parent (owning record), record (attached or detached record), resource (parent resource), and field (association field), plus the standard ExecutionContext. Return values are ignored.
-> before_attach
Runs once per selected record inside the transaction attaching all selected records. Raising a regular error rolls back the whole batch and displays its message. Attaching an already-attached record runs the hook again and can add another join row unless a database constraint refuses it.
field :users, as: :has_and_belongs_to_many,
before_attach: -> { raise "Inactive user" unless record.active? }- Type:
Proc/Lambda. - Default:
nil. - Locals:
parent,record,resource,field, as defined above.
-> after_attach
Runs after the attach transaction commits, once per attached record. Skipped when attachment fails. An error does not undo the attach, produces an error response, and prevents later hooks in the batch from running.
field :users, as: :has_and_belongs_to_many,
after_attach: -> { Rails.logger.info "Attached #{record.id} to #{parent.id}" }- Type:
Proc/Lambda. - Default:
nil. - Locals:
parent,record,resource,field, as defined above.
-> before_detach
Runs inside the detach transaction before removing the join row. Raising a regular error rolls back the detach but produces an error response rather than a friendly message. Detaching a record not attached to this parent changes nothing and runs no hooks.
field :users, as: :has_and_belongs_to_many,
before_detach: -> { raise "Cannot remove the last member" if parent.users.count == 1 }- Type:
Proc/Lambda. - Default:
nil. - Locals:
parent,record,resource,field, as defined above.
-> after_detach
Runs after the detach commits; the associated record still exists because only the join row was removed. Skipped on failure. Errors produce an error response without undoing the detach.
field :users, as: :has_and_belongs_to_many,
after_detach: -> { Rails.logger.info "Detached #{record.id} from #{parent.id}" }- Type:
Proc/Lambda. - Default:
nil. - Locals:
parent,record,resource,field, as defined above.
WARNING
ActiveRecord::Rollback is swallowed inside before-hook transactions, so the reason is not shown. After hooks have no transaction left to roll back. Errors in after hooks leave the change committed.
Nested forms
-> nested
Enables editing child records in the parent form through the avo-nested gem. Rails must define accepts_nested_attributes_for on the association. See the guide for setup, deletion behavior, and authorization.
field :users, as: :has_and_belongs_to_many, nested: { on: :forms, limit: 5 }- Type:
BooleanorHash. - Default:
{}(disabled). - Values:
trueis shorthand for{ on: [:new, :edit] };falseornildisables nesting. - Validation: Without the model's nested-attributes writer, the nested component raises outside production and is hidden in production.
-> on
Views in which the nested component replaces the association table. Create and update requests use the New and Edit configurations respectively.
field :users, as: :has_and_belongs_to_many, nested: { on: :new }| Value | Behavior |
|---|---|
:new | New form only. |
:edit | Edit form only. |
:forms | Both New and Edit. |
[:new, :edit] | Explicit list of form views. |
- Type:
SymbolorArrayof symbols. - Default:
nil(disabled when omitted from a Hash).
-> limit
Hides Add once the nested form reaches the limit; this is a UI limit, not a model validation.
field :users, as: :has_and_belongs_to_many, nested: { on: :forms, limit: 5 }- Type: Non-negative
Integer. - Default:
0(unlimited).