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:
# 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
endA 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:
# Gemfile
gem "avo-forms", source: "https://packager.dev/avo-hq/"Then install it:
bundle installThe 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:
rails generate avo:form general_settingsThis creates app/avo/forms/general_settings.rb. Add fields and handle submission:
# 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
endGenerate a page to host it:
rails generate avo:page settingsWire the form into a sub-page. The main page declares navigation; the sub-page declares content:
# 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# 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
endFinally, add the page to your Avo menu:
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
section "Configuration", icon: "cog" do
page "Avo::Pages::Settings", icon: "adjustments"
end
}
endUsers 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 viafieldsand handle submissions viahandle. Thehandlemethod runs in the controller context, soparams,current_user,flash, andcookiesare 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 definesnavigation. Sub-pages (deeper, e.g.Avo::Pages::Settings::General) define the actualcontent. - 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>, whereiddefaults to the class path (e.g.Avo::Pages::Settings::General→settings/general) and can be overridden withself.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:
rails generate avo:form your_form_nameCreates app/avo/forms/your_form_name.rb:
# 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"
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
endGenerate a page:
rails generate avo:page your_page_nameCreates app/avo/pages/your_page_name.rb, with commented examples of every navigation and content declaration:
# 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"
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
endTo 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.
# 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
endGroup fields with cards and panels
Wrap related fields in a card or panel to organize them. Both take title: and description: as keyword arguments.
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
endPlace fields side by side
Use the width field option to lay fields out inline. with_options applies it to a group at once.
def fields
card do
with_options width: 50 do
field :first_name, as: :text
field :last_name, as: :text
end
end
endPrefill from a record
Bind a field to an existing record to prefill its value. with_options record: binds a whole group.
def fields
field :first_name, record: Avo::Current.user
field :last_name, record: Avo::Current.user
field :email, record: Avo::Current.user
endHandle 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.
def handle
current_user.update(params.permit(:first_name, :last_name, :email))
flash[:notice] = "Profile updated successfully"
default_response
endBecause it's plain controller code, you can do anything Rails can — update several models, kick off a background job, or branch on the input:
def handle
ImportService.new(params[:file]).process if params[:import_data]
Post.create(title: params[:title], body: params[:body])
default_response
endFlash 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).
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
endReusable 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:
# 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
endNesting 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:
<%# 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 anavigationof 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:
# 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
endFor a simple section, skip the file and declare a virtual page inline with its content:
def navigation
page "Integrations",
description: "Connect third-party services",
content: -> do
form Avo::Forms::Settings::Slack
form Avo::Forms::Settings::Email
end
endTo 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:
def navigation
form Avo::Forms::Profiles
endPut 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.
# 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
endEach 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:
def content
form Avo::Forms::Settings::AppSettings, show_header: false
endName 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.
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
self.title = "Application Settings"
self.navigation_label = "App Config"
endAdd 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:
# 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
}
endFull 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.
# 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# 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# 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
endBest 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
handlebefore acting on it. - Give feedback with flash messages so users know the result.
- Wrap risky work in
begin/rescueand surface failures viaflash[: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_labelto keep long titles readable in the menu.