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

Forms & Pages ​

Build structured configuration and settings interfaces in your Avo admin using standalone forms and organized page hierarchies — no database models required.

Forms are model-agnostic screens for anything that isn't CRUD on a record — application settings, user preferences, data imports, or any custom workflow. You declare a form's fields and decide what happens when it's submitted. Pages organize those forms into a sidebar-navigable interface with a main page and sub-pages.

A form is a class under app/avo/forms/ that defines its fields and handles its own submission:

ruby
# app/avo/forms/app_settings.rb
class Avo::Forms::AppSettings < Avo::Forms::Core::Form
  self.title = "Application Settings"
  self.description = "Manage your application configuration"

  def fields
    field :app_name, as: :text
    field :maintenance_mode, as: :boolean
  end

  def handle
    flash[:notice] = "Settings updated successfully"
    default_response
  end
end

A form needs only two things: a fields method describing what to show, and a handle method describing what to do on submit. Everything else — title, description, routing — has a sensible default.

Install the add-on ​

Forms & Pages ships as the avo-forms add-on and requires Avo >= 4.0 with an active license that has the add-on enabled.

Add the gem to your Gemfile:

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

Then install it:

bash
bundle install

The engine registers itself automatically via Avo.plugin_manager — no initializer changes are required. Start creating forms and pages right away.

Quick start ​

Generate a form:

bash
rails generate avo:form general_settings

This creates app/avo/forms/general_settings.rb. Add fields and handle submission:

ruby
# app/avo/forms/general_settings.rb
class Avo::Forms::GeneralSettings < Avo::Forms::Core::Form
  self.title = "General Settings"
  self.description = "Configure your application"

  def fields
    field :app_name, as: :text, required: true
    field :maintenance_mode, as: :boolean, default: false
  end

  def handle
    # params contains submitted form data
    flash[:notice] = "Settings saved"
    default_response
  end
end

Generate a page to host it:

bash
rails generate avo:page settings

Wire the form into a sub-page. The main page declares navigation; the sub-page declares content:

ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
  self.title = "Settings"
  self.description = "Manage your application settings"

  def navigation
    page Avo::Pages::Settings::General, default: true
  end
end
ruby
# app/avo/pages/settings/general.rb
class Avo::Pages::Settings::General < Avo::Forms::Core::Page
  self.title = "General"

  def content
    form Avo::Forms::GeneralSettings
  end
end

Finally, add the page to your Avo menu:

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  config.main_menu = -> {
    section "Configuration", icon: "cog" do
      page "Avo::Pages::Settings", icon: "adjustments"
    end
  }
end

Users can now navigate to Settings → General in the sidebar and submit the form.

How it works ​

  • Forms inherit from Avo::Forms::Core::Form. They define fields via fields and handle submissions via handle. The handle method runs in the controller context, so params, current_user, flash, and cookies are all available directly.
  • Pages inherit from Avo::Forms::Core::Page. A main page (one namespace level deep, e.g. Avo::Pages::Settings) acts as a container and defines navigation. Sub-pages (deeper, e.g. Avo::Pages::Settings::General) define the actual content.
  • Routes are resolved dynamically at request time — no manual route declarations. Pages live at <root_path>/pages/<id> and forms at <root_path>/forms/<id>, where id defaults to the class path (e.g. Avo::Pages::Settings::General → settings/general) and can be overridden with self.id.
  • Rendering: pages render with a sidebar navigation listing all sub-pages. Each sub-page shows its title, description, and all registered forms in order.

Generate forms and pages ​

Avo Forms ships generators for both building blocks. Use them to scaffold a class with the right structure, then fill in the fields, content, and navigation.

Generate a form:

bash
rails generate avo:form your_form_name

Creates app/avo/forms/your_form_name.rb:

ruby
# app/avo/forms/your_form_name.rb
class Avo::Forms::YourFormName < Avo::Forms::Core::Form
  self.title = "Your Form Name"
  self.description = "Manage your your form name"
  # Applies when this form is opened on its own URL; on a page the page decides.
  # self.container_width = :lg # :md, :lg or :full

  def fields
    field :example, as: :text, default: "Hello World"
  end

  def handle
    flash[:success] = { body: "Form submitted successfully", timeout: :forever }
    flash[:notice] = params[:example]

    default_response
  end
end

Generate a page:

bash
rails generate avo:page your_page_name

Creates app/avo/pages/your_page_name.rb, with commented examples of every navigation and content declaration:

ruby
# app/avo/pages/your_page_name.rb
class Avo::Pages::YourPageName < Avo::Forms::Core::Page
  self.title = "Your Page Name"
  self.description = "A page for your page name"
  # self.navigation_label = "Your Page Name"
  # self.container_width = :lg # :md, :lg or :full

  def content
    # form Avo::Forms::AnyFormClass
  end

  def navigation
    # Reference an existing page class:
    # page Avo::Pages::AnySubPageClass

    # Show a form directly in the navigation:
    # form Avo::Forms::AnyFormClass

    # Declare a virtual page inline with its content:
    # page "Custom Page",
    #   description: "A page for custom page",
    #   content: -> { form Avo::Forms::SomeForm }
  end
end

To create a sub-page, generate the parent first and namespace the child under it — see how pages are organized.

Parsed once at boot

content and navigation are evaluated a single time during application boot. Keep them static — no conditional or dynamic logic inside them.

Build a form's fields ​

Declare what the form shows inside fields, using the same field syntax as resources and actions. Every Avo field type and option works here.

ruby
# app/avo/forms/user_preferences.rb
class Avo::Forms::UserPreferences < Avo::Forms::Core::Form
  def fields
    field :email, as: :text, required: true
    field :notifications, as: :boolean, default: true
    field :theme, as: :select, options: { light: "Light", dark: "Dark" }
  end
end

Group fields with cards and panels ​

Wrap related fields in a card or panel to organize them. Both take title: and description: as keyword arguments.

ruby
def fields
  card title: "Personal Information" do
    field :first_name, as: :text
    field :last_name, as: :text
  end

  panel title: "Preferences", description: "Customize your experience" do
    field :theme, as: :select, options: { light: "Light", dark: "Dark" }
    field :notifications, as: :boolean
  end
end

Place fields side by side ​

Use the width field option to lay fields out inline. with_options applies it to a group at once.

ruby
def fields
  card do
    with_options width: 50 do
      field :first_name, as: :text
      field :last_name, as: :text
    end
  end
end

Prefill from a record ​

Bind a field to an existing record to prefill its value. with_options record: binds a whole group.

ruby
def fields
  field :first_name, record: Avo::Current.user
  field :last_name, record: Avo::Current.user
  field :email, record: Avo::Current.user
end

Handle a form's submission ​

When the form is submitted, handle runs. It executes in the controller's context, so the submitted data is in params and every controller helper — current_user, flash, cookies, redirect_to — is available. Call default_response to send the standard redirect-back response.

ruby
def handle
  current_user.update(params.permit(:first_name, :last_name, :email))
  flash[:notice] = "Profile updated successfully"
  default_response
end

Because it's plain controller code, you can do anything Rails can — update several models, kick off a background job, or branch on the input:

ruby
def handle
  ImportService.new(params[:file]).process if params[:import_data]
  Post.create(title: params[:title], body: params[:body])
  default_response
end

Flash messages ​

Set a flash before returning to surface feedback. Pass a string for a simple message, or a Hash to control the timeout (:forever keeps it until dismissed).

ruby
def handle
  flash[:notice] = "Operation completed successfully"
  flash[:error] = "Something went wrong"
  flash[:success] = { body: "Saved successfully", timeout: 3000 }
  flash[:warning] = { body: "Heads up", timeout: :forever }

  default_response
end

Restrict who can use a form ​

Since avo-forms >= v4.3.1

By default, every signed-in Avo user can open and submit every form. Set self.authorize to limit a form to some users:

ruby
# app/avo/forms/app_settings.rb
class Avo::Forms::AppSettings < Avo::Forms::Core::Form
  self.authorize = -> { current_user.admin? }
end

Everyone else is refused: opening or submitting the form redirects back with a "not authorized" message, and handle never runs. The form also disappears from pages, .component renders, and the menu.

WARNING

Hiding a form's menu link is not access control. A user who knows the form's URL can still open it unless self.authorize refuses them.

Reusable vs. inline forms ​

The generator gives each form its own file under app/avo/forms/ — the default, and the right choice for anything you'll show on more than one page or render as a component. The class name is its public handle.

For a form that only ever belongs to one page, you can define it inline, nested in the page class:

ruby
# app/avo/pages/settings/integrations.rb
class Avo::Pages::Settings::Integrations < Avo::Forms::Core::Page
  self.title = "Integrations"

  def content
    form ApiConfiguration
  end

  class ApiConfiguration < Avo::Forms::Core::Form
    self.title = "API Configuration"

    def fields
      field :api_key, as: :text, required: true
      field :webhook_url, as: :text
    end

    def handle
      flash[:success] = "API configuration updated"
      default_response
    end
  end
end

Nesting hides intent

A nested Avo::Pages::Settings::Integrations::ApiConfiguration doesn't read as a form, which makes the code harder to navigate. Reach for inline definitions only for simple, page-specific forms — give a form its own file the moment you want to reuse it elsewhere.

Render a form anywhere ​

Forms are usually shown on a page, but a form can also be dropped into any view as a standalone component via its .component method:

erb
<%# app/views/some/view.html.erb %>
<%= render Avo::Forms::Settings::General.component %>

Organize pages ​

Pages give you structured, hierarchical screens for organizing forms and sub-pages. A page works like a resource, but instead of records it presents forms and child pages. With nothing but a class name, a page already has a title and URL derived from its namespace; you add structure by declaring a navigation of sub-pages, a content of forms, or both.

Pages form a shallow hierarchy based on their namespace depth:

  • Main pages sit one level under Avo::Pages (e.g. Avo::Pages::Settings). They act as containers, get a left-sidebar menu entry, and can declare a navigation of children.
  • Sub-pages are nested deeper (e.g. Avo::Pages::Settings::General). They hold the actual forms and are reached through their parent's navigation.

When a user opens a main page, Avo redirects to its default sub-page if one is set; otherwise it shows the main page's own content. Sub-pages appear in a sidebar for easy switching.

Build a page's navigation ​

Declare a main page's children inside navigation. There are three ways to register an entry, and you can mix them freely.

Reference an existing page class — the common case — and optionally mark one as the default:

ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
  def navigation
    page Avo::Pages::Settings::General, default: true
    page Avo::Pages::Settings::Security
  end
end

For a simple section, skip the file and declare a virtual page inline with its content:

ruby
def navigation
  page "Integrations",
    description: "Connect third-party services",
    content: -> do
      form Avo::Forms::Settings::Slack
      form Avo::Forms::Settings::Email
    end
end

To surface a single form without a page of its own, register it directly with form — it becomes a navigation entry that shows the form inline:

ruby
def navigation
  form Avo::Forms::Profiles
end

Put forms on a page ​

A sub-page shows its forms by declaring them in content with form. They render in the order you list them.

ruby
# app/avo/pages/settings/general.rb
class Avo::Pages::Settings::General < Avo::Forms::Core::Page
  self.title = "General Settings"

  def content
    form Avo::Forms::Settings::AppSettings
    form Avo::Forms::Settings::CompanyInfo
  end
end

Each form renders with its own header — its title and description. If you're already on a page that provides context, drop the header for that placement with show_header: false:

ruby
def content
  form Avo::Forms::Settings::AppSettings, show_header: false
end

Set the container width ​

Pages and forms render inside Avo's page container, which is :md by default. Widen or narrow it per screen with container_width — :sm, :md, :lg or :full, the same values as config.container_width.

ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
  self.title = "Settings"
  self.container_width = :lg # applies to this page and its sub-pages
end

A sub-page's own value wins over its main page's, so one wide area can still hold a narrow screen:

ruby
# app/avo/pages/settings/notifications.rb
class Avo::Pages::Settings::Notifications < Avo::Forms::Core::Page
  self.container_width = :md
end

On a form, container_width applies when the form is opened on its own URL (linked from the menu, or visited directly). When the form is placed on a page, the page owns the container and the form's value is ignored.

ruby
# app/avo/forms/settings/app_settings.rb
class Avo::Forms::Settings::AppSettings < Avo::Forms::Core::Form
  self.container_width = :full
end

Leave it unset to keep whatever config.container_width gives the app.

No navigation, no sidebar

A page whose navigation is empty renders without the sidebar column, so its content spans the full container instead of sitting off to one side.

Name the menu entry ​

By default a page's menu label is its title. Set navigation_label when you want a shorter or different label in the menu than the title shown on the page.

ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
  self.title = "Application Settings"
  self.navigation_label = "App Config"
end

Add pages to the main menu ​

Pages don't appear in Avo's main navigation until you add them. Reference them individually in config.main_menu, or pull in all of them with all_pages:

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  config.main_menu = -> {
    section "Configuration", icon: "cog" do
      page "Avo::Pages::Settings", icon: "adjustments"

      # Or add every page at once:
      all_pages
    end
  }
end

Full example ​

A complete settings area: a form that groups fields into panels and persists them in handle, and a page that wires it up with a default sub-page, an inline form, and a virtual page.

ruby
# app/avo/forms/app_settings.rb
class Avo::Forms::AppSettings < Avo::Forms::Core::Form
  self.title = "Application Settings"
  self.description = "Configure global application settings"

  def fields
    card do
      field :app_name, as: :text,
            default: -> { Rails.application.class.module_parent_name },
            required: true
      field :app_url, as: :text, placeholder: "https://yourapp.com"
      field :maintenance_mode, as: :boolean, default: false
    end

    panel title: "Feature Flags" do
      field :enable_registrations, as: :boolean, default: true
      field :max_file_upload_size, as: :number, default: 10, help_text: "In MB"
    end
  end

  def handle
    ApplicationSettings.update_all(
      params.permit(:app_name, :app_url, :maintenance_mode,
                    :enable_registrations, :max_file_upload_size)
    )

    flash[:success] = { body: "Application settings updated", timeout: 5000 }
    default_response
  end
end
ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
  self.title = "Settings"
  self.description = "Manage your application settings"
  self.navigation_label = "App Settings"

  def navigation
    page Avo::Pages::Settings::General, default: true
    form Avo::Forms::UserProfiles
    page "Integrations",
      content: -> do
        form Avo::Forms::Settings::SlackIntegration
        form Avo::Forms::Settings::EmailIntegration
      end
    page Avo::Pages::Settings::Security
  end
end
ruby
# app/avo/pages/settings/general.rb
class Avo::Pages::Settings::General < Avo::Forms::Core::Page
  self.title = "General Settings"
  self.description = "Basic application configuration"

  def content
    form Avo::Forms::AppSettings
    form Avo::Forms::Settings::CompanyInfo
  end
end

Best practices ​

  • Keep each form focused on one related set of functionality rather than doing everything at once.
  • Group fields with panels so long forms stay scannable.
  • Validate and permit input in handle before acting on it.
  • Give feedback with flash messages so users know the result.
  • Wrap risky work in begin/rescue and surface failures via flash[:error].
  • Offload long-running work to a background job and tell the user it's processing.
  • Keep the page hierarchy shallow — a main page plus one level of sub-pages covers most cases.
  • Set a default sub-page when the main page is mainly a container, so it lands somewhere useful.
  • Pick the right page type: class-based pages for anything with custom logic or several forms; virtual pages with forms for a single-form section; virtual pages with content to group related forms without extra files.
  • Use navigation_label to keep long titles readable in the menu.