Has Many API
Per-option reference for has_many fields; see the Has Many guide for tasks and examples.
Options are passed to field in the resource's fields method in app/avo/resources/user.rb:
field :projects, as: :has_many, loading: :manualCommon options such as visibility are documented in shared field options. Rails association options, including through:, belong on the model; Avo detects through associations from reflection rather than a field-level through: option.
Listing and resource selection
-> scope
Transforms the policy-scoped association relation used for table rows. It does not filter attach choices.
field :comments, as: :has_many, scope: -> { query.where(approved: true) }- Type:
Proc/Lambdareturning an Active Record relation. - Default:
nil. - Context:
Avo::ExecutionContext. - Locals:
query(policy-scoped association),parent(parent record),resource(associated resource),parent_resource(parent's resource).
-> for_attribute
Maps the field ID to another model association, allowing multiple panels with different scopes for the same association.
field :approved_comments, as: :has_many, for_attribute: :comments,
scope: -> { query.where(approved: true) }- Type:
Symbol/Stringnaming a model association. - Default:
nil, using the field ID.
-> use_resource
Overrides the resource used to display or redirect to the association table.
field :comments, as: :has_many, use_resource: Avo::Resources::PhotoComment- Type: Avo resource class / its class name as a
String. - Default:
nil, inferring the resource from the reflected model.
-> link_to_child_resource
Controls whether STI row links resolve to the child model's resource. See STI associations.
field :users, as: :has_many, link_to_child_resource: true- Type:
Boolean. - Default:
nil, falling back to the associated resource'slink_to_child_resourcesetting (normallyfalse). An explicitfalseoverrides a resource-leveltrue.
Attach picker
-> attach_scope
Transforms the policy-scoped relation offered by the attach picker. The save resolves submitted IDs against the same relation; excluded records are refused as missing.
field :members, as: :has_many,
attach_scope: -> { query.where.not(team_id: parent.id) }- Type:
Proc/Lambdareturning an Active Record relation. - Default:
nil. - Context:
Avo::ExecutionContext. - Locals:
query(policy-scoped target relation),parent(parent record).
WARNING
A fresh relation can widen beyond the policy-scoped query, including at save time. This option does not filter the association table; that is the role of scope.
-> searchable
Enables the search-as-you-type attach picker. Configuration, licensing, query/item locals, and precedence are described in Searchable associations and its API.
field :projects, as: :has_many, searchable: true- Type:
Boolean/Hashwithquery:,item:,enabled:overrides. - Default:
nil(searchable picker disabled). - Values:
true,false, or the hash form documented in the searchable API.
-> attach_using
Selects the non-searchable attach input. The checkbox list supports multiple selections and client-side filtering of the loaded choices. It respects attach_scope, the global lookup limit, and the target resource's search[:item] row presentation.
field :users, as: :has_many, attach_using: :checkbox_list| Value | Behavior |
|---|---|
nil | Standard select input. |
:checkbox_list | Checkbox list with multi-select. |
- Type:
Symbol/String. - Default:
nil. - Validation: The checkbox-list check calls
to_sym; other symbol values use the select input rather than raising an enum validation error.
-> attach_fields
Declares additional fields displayed in the attach modal and persisted on the through join record. Plain has_many associations have no join record to receive these values.
field :patrons, as: :has_many, attach_fields: -> {
field :review, as: :text
}- Type:
Proc/Lambdacontaining field declarations. - Default:
nil. - Context:
Avo::FieldsExecutionContext, not the hook execution context. Declared fields are hydrated with a new through-model record. - Locals: No callback-specific locals are supplied.
WARNING
Polymorphic join models require their type field when building a join with extra attributes; see the through-association example.
Attach and detach hooks
Sinceavo >= v4.3.11All four hooks run in Avo::ExecutionContext, with current_user, params, and these additional locals:
| Local | Value |
|---|---|
parent | Parent record whose page holds the association. |
record | Record being attached or detached. |
resource | Parent record's resource. |
field | Association field. |
Hooks run only for Attach and Detach requests, not Create, nested forms, deletes, model changes, actions, or API writes. Return values are ignored. Selecting several records runs attach hooks once per record. Attaching an already attached record still runs attach hooks; a through association can create another join row unless a constraint or validation prevents it. Detaching a record not attached to this parent runs no hooks.
WARNING
Raise a regular error to stop a before hook, not ActiveRecord::Rollback, which Rails swallows. After hooks run after commit: errors cannot undo the change, produce an error response, and stop subsequent hooks. Rescue failed side effects inside the hook if they must not reach the user.
-> before_attach
Runs before each attachment inside the transaction for the entire selection. Raising rolls back all attachments in the request and displays the error message.
field :members, as: :has_many,
before_attach: -> { raise "Archived member" if record.archived? }- Type:
Proc/Lambda. - Default:
nil. - Context / Locals: Shared hook contract.
-> after_attach
Runs once per attached record after the transaction commits. It is skipped when attachment fails.
field :members, as: :has_many,
after_attach: -> { Rails.logger.info "Attached #{record.id}" }- Type:
Proc/Lambda. - Default:
nil. - Context / Locals: Shared hook contract.
-> before_detach
Runs before detachment inside its transaction. Raising rolls back detachment but produces an error response rather than the attach flow's flash message.
field :members, as: :has_many,
before_detach: -> { raise "Locked member" if record.locked? }- Type:
Proc/Lambda. - Default:
nil. - Context / Locals: Shared hook contract.
-> after_detach
Runs after the detach transaction commits, and is skipped on failure. For direct has_many associations with dependent: :destroy or :delete_all, record has already been deleted. Through associations remove only the join row.
field :members, as: :has_many,
after_detach: -> { Rails.logger.info "Detached #{record.id}" }- Type:
Proc/Lambda. - Default:
nil. - Context / Locals: Shared hook contract.
Panel presentation and loading
-> name
Overrides the panel title. Without an override, field translations or the generated name apply; use_resource can supply the resource name.
field :comments, as: :has_many, name: "User feedback"- Type:
String/ zero-arityProc/Lambdareturning a string. - Default:
nil(translated or generated association name). - Context:
Avo::ExecutionContext; no additional locals are supplied.
-> description
Text beneath the association title, including on an unloaded manual placeholder.
field :comments, as: :has_many,
description: -> { loading_type == :manual ? "Comments" : "#{query.count} comments" }- Type:
String/ zero-arityProc/Lambda. - Default:
nil(unless a class-level description is defined). - Context:
Avo::ExecutionContext. - Locals:
query(parent's association relation),loading_type(:manualon the placeholder, otherwisenil). This relation is not the table's policy-scopedscoperesult.
WARNING
Calling query.count or otherwise querying inside the placeholder description runs SQL before Load is clicked.
-> loading
Controls when the association frame loads. Manual frames show Load and support inline Retry on a failed fetch. Their opened state is remembered by a per-record, per-frame cookie with a sliding expiry.
field :orders, as: :has_many,
loading: { mode: :manual, auto_load_for: 5.minutes }| Value | Behavior |
|---|---|
:lazy / { mode: :lazy } | Loads when the frame becomes visible. |
:manual / { mode: :manual } | Loads on demand, remembering opened frames for the default window. |
{ auto_load_for: 5.minutes } | Hash without mode implies manual mode. |
- Type:
Symbol/String/Hash. - Default:
nil, inheritingconfig.associations[:frames][:loading], normally:lazy. - Validation: Mode strings are converted with
to_sym; there is no strict enum validation.
-> mode
The hash form's loading mode, with the same lazy/manual behavior as the shorthand.
field :orders, as: :has_many, loading: { mode: :manual }- Type:
Symbol/String. - Default:
:manualwhen a loading hash omitsmode.
-> auto_load_for
Sliding memory window for an opened manual frame, not a delay before loading. Each return within the window refreshes it.
field :orders, as: :has_many, loading: { mode: :manual, auto_load_for: 0 }- Type:
ActiveSupport::Duration/ integer seconds /nil. - Default: Global
config.associations[:frames][:auto_load_for], falling back to15.minutes. - Values: Positive durations remember the frame;
0,nil, or negative values disable memory. - Validation: Converted with
to_i; positive seconds are retained.
-> reloadable
Displays a reload icon for refreshing only the association frame.
field :reviews, as: :has_many, reloadable: -> { current_user.admin? }- Type:
Boolean/Proc/Lambda. - Default:
nil(icon hidden). - Context:
Avo::ExecutionContext; no additional locals are supplied.
-> linkable
Displays an open-in-new-tab icon next to the title, linking to the association table's standalone page.
field :memberships, as: :has_many, linkable: true- Type:
Boolean/Proc/Lambda. - Default:
nil(icon hidden). - Context:
Avo::ExecutionContext; no additional locals are supplied.
Table controls
-> per_page
Sets the association's page size ahead of config.via_per_page. A request's per_page parameter and a persisted session choice take precedence. The dropdown includes the field's page size.
field :certifications, as: :has_many, per_page: 24- Type: Positive
Integer. - Default:
nil, usingconfig.via_per_page.
INFO
With session persistence enabled for pagination, the saved size is keyed by parent resource and associated resource, not field. Two fields targeting the same resource share it.
-> discreet_pagination
Hides pagination details when the association has only one page.
field :projects, as: :has_many, discreet_pagination: true- Type:
Boolean. - Default:
nil(pagination details are not suppressed).
-> hide_search_input
Hides table search, independently of attach picker search. Without this flag, table search still requires the target resource's search query configuration.
field :projects, as: :has_many, hide_search_input: true- Type:
Boolean. - Default:
nil(search not explicitly hidden).
-> hide_filter_button
Hides filter controls on the association table.
field :projects, as: :has_many, hide_filter_button: true- Type:
Boolean. - Default:
nil(filter controls not explicitly hidden).
-> refresh_on_destroy
Reloads the whole page after deleting an associated record, rather than only the table frame. The current URL and success message are retained.
field :reviews, as: :has_many, refresh_on_destroy: true- Type:
Boolean. - Default:
nil(only the table reloads).
Nested forms
-> nested
Enables nested child forms in selected views. Rendering and saving nested forms requires avo-nested and Rails accepts_nested_attributes_for; see the nested form guide.
field :authors, as: :has_many, nested: { on: :forms, limit: 2 }- Type:
Boolean/Hash. - Default:
{}(disabled). - Values:
trueis shorthand for{ on: [:new, :edit] };falsedisables nesting.
-> on
Views that render nested forms instead of the association panel.
field :authors, as: :has_many, nested: { on: :new }| Value | Behavior |
|---|---|
:new | New form only. |
:edit | Edit form only. |
:forms | New and Edit forms. |
[:new, :edit] | Explicit list of views. |
- Type:
Symbol/ array of view names. - Default:
nilfor a hash withouton(no nested form).
-> limit
Hides Add when the initialized nested record count reaches the limit. This is a form control, not a model validation.
field :authors, as: :has_many, nested: { on: :forms, limit: 2 }- Type: Non-negative
Integer. - Default:
0(unlimited) forhas_many.