AshFormBuilder ๐
AshFormBuilder = AshPhoenix.Form + Auto UI + Smart Components + Themes
A declarative form generation engine for Ash Framework and Phoenix LiveView.
Define your form structure in 1-3 lines inside your Ash Resource, and get a complete, policy-compliant LiveView form with:
-
โ
Auto-inferred fields from your action's
acceptlist - โ Searchable combobox for many-to-many relationships
- โ Creatable combobox (create related records on-the-fly)
- โ Dynamic nested forms for has_many relationships
- โ Pluggable theme system (Default, MishkaChelekom, or custom)
- โ Full Ash policy and validation enforcement
๐ฏ The Pitch: Why AshFormBuilder?
| Layer | AshPhoenix.Form | AshFormBuilder |
|---|---|---|
| Form State |
โ
Provides AshPhoenix.Form |
โ
Uses AshPhoenix.Form |
| Field Inference | โ Manual field definition | โ Auto-infers from action.accept |
| UI Components | โ You render everything | โ Smart components per field type |
| Themes | โ No theming | โ Pluggable theme system |
| Combobox | โ Build your own | โ Searchable + Creatable built-in |
| Nested Forms | โ Manual setup | โ Auto nested forms with add/remove |
| Lines of Code | ~20-50 lines | ~1-3 lines |
In short: AshPhoenix.Form gives you the engine. AshFormBuilder gives you the complete car.
โก 3-Line Quick Start
1. Add to mix.exs
{:ash_form_builder, "~> 0.2.0"}2. Configure Theme (config/config.exs)
config :ash_form_builder, :theme, AshFormBuilder.Themes.Default3. Add Extension to Resource
defmodule MyApp.Todos.Task do
use Ash.Resource,
domain: MyApp.Todos,
extensions: [AshFormBuilder] # โ Add this
attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false
attribute :description, :text
attribute :completed, :boolean, default: false
end
actions do
create :create do
accept [:title, :description, :completed]
end
end
form do
action :create # โ That's it! Fields auto-inferred
end
end4. Use in LiveView
defmodule MyAppWeb.TaskLive.Form do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
form = MyApp.Todos.Task.Form.for_create(actor: socket.assigns.current_user)
{:ok, assign(socket, form: form)}
end
def render(assigns) do
~H"""
<.live_component
module={AshFormBuilder.FormComponent}
id="task-form"
resource={MyApp.Todos.Task}
form={@form}
/>
"""
end
def handle_info({:form_submitted, MyApp.Todos.Task, task}, socket) do
{:noreply, push_navigate(socket, to: ~p"/tasks/#{task.id}")}
end
endResult: A complete, validated form with title (text input), description (textarea), and completed (checkbox) - all from 1 line of configuration.
โจ Key Features
๐ Searchable Many-to-Many Combobox
Automatically renders searchable multi-select for relationships:
relationships do
many_to_many :tags, MyApp.Todos.Tag do
through MyApp.Todos.TaskTag
end
end
actions do
create :create do
accept [:title]
manage_relationship :tags, :tags, type: :append_and_remove
end
end
form do
action :create
field :tags do
type :multiselect_combobox
opts [
search_event: "search_tags",
debounce: 300,
label_key: :name,
value_key: :id
]
end
endLiveView Search Handler:
def handle_event("search_tags", %{"query" => query}, socket) do
tags = MyApp.Todos.Tag
|> Ash.Query.filter(contains(name: ^query))
|> MyApp.Todos.read!()
{:noreply, push_event(socket, "update_combobox_options", %{
field: "tags",
options: Enum.map(tags, &{&1.name, &1.id})
})}
endโจ Creatable Combobox (Create On-the-Fly)
Allow users to create new related records without leaving the form:
form do
field :tags do
type :multiselect_combobox
opts [
creatable: true, # โ Enable creating
create_action: :create,
create_label: "Create \"",
search_event: "search_tags"
]
end
endWhat happens:
- User types "Urgent" in combobox
- No results found โ "Create 'Urgent'" button appears
- Click โ Creates new Tag record via Ash
- New tag automatically added to selection
- All Ash validations and policies enforced
๐ Dynamic Nested Forms (has_many)
Manage child records with dynamic add/remove:
relationships do
has_many :subtasks, MyApp.Todos.Subtask
end
form do
nested :subtasks do
label "Subtasks"
cardinality :many
add_label "Add Subtask"
remove_label "Remove"
field :title, required: true
field :completed, type: :checkbox
end
endRenders:
- Fieldset with "Subtasks" legend
- Existing subtasks rendered with all fields
- "Add Subtask" button โ adds new subtask form
- "Remove" button on each subtask โ removes from form
- Full validation support for nested fields
๐จ Theme System
Built-in Themes
# Default theme (semantic HTML, no dependencies)
config :ash_form_builder, :theme, AshFormBuilder.Themes.Default
# MishkaChelekom theme (requires mishka_chelekom dependency)
config :ash_form_builder, :theme, AshFormBuilder.Theme.MishkaThemeCustom Theme Example
defmodule MyAppWeb.CustomTheme do
@behaviour AshFormBuilder.Theme
use Phoenix.Component
@impl AshFormBuilder.Theme
def render_field(assigns, opts) do
case assigns.field.type do
:text_input -> render_text_input(assigns)
:multiselect_combobox -> render_combobox(assigns)
# ... etc
end
end
defp render_text_input(assigns) do
~H"""
<div class="form-group">
<label for={Phoenix.HTML.Form.input_id(@form, @field.name)}>
{@field.label}
</label>
<input
type="text"
id={Phoenix.HTML.Form.input_id(@form, @field.name)}
class="form-control"
/>
</div>
"""
end
end๐ Documentation
- Installation Guide - Complete setup instructions
- Todo App Tutorial - Step-by-step integration guide
- Relationships Guide - has_many vs many_to_many deep dive
- API Reference - Complete module docs
๐ฆ Installation
Requirements
- Elixir ~> 1.17
- Phoenix ~> 1.7
- Phoenix LiveView ~> 1.0
- Ash ~> 3.0
- AshPhoenix ~> 2.0
Steps
Add dependency to
mix.exs:defp deps do [ {:ash, "~> 3.0"}, {:ash_phoenix, "~> 2.0"}, {:ash_form_builder, "~> 0.2.0"}, # Optional: For MishkaChelekom theme {:mishka_chelekom, "~> 0.0.8"} ] endFetch dependencies:
mix deps.getConfigure theme in
config/config.exs:config :ash_form_builder, :theme, AshFormBuilder.Themes.DefaultAdd extension to your Ash Resource:
use Ash.Resource, domain: MyApp.Todos, extensions: [AshFormBuilder]
๐ง Field Type Inference
AshFormBuilder automatically maps Ash types to UI components:
| Ash Type | Constraint | UI Type | Example |
|---|---|---|---|
:string | - | :text_input | Text fields |
:text | - | :textarea | Multi-line text |
:boolean | - | :checkbox | Toggle switches |
:integer / :float | - | :number | Numeric inputs |
:date | - | :date | Date picker |
:datetime | - | :datetime | DateTime picker |
:atom | one_of: | :select | Dropdown |
:enum module | - | :select | Enum dropdown |
many_to_many | - | :multiselect_combobox | Searchable multi-select |
has_many | - | :nested_form | Dynamic nested forms |
๐งช Testing
defmodule MyAppWeb.TaskLiveTest do
use MyAppWeb.ConnCase
import Phoenix.LiveViewTest
test "renders form with auto-inferred fields", %{conn: conn} do
{:ok, _view, html} = live_isolated(conn, MyAppWeb.TaskLive.Form)
assert html =~ "Task Title"
assert html =~ "Description"
assert html =~ "Completed"
end
test "creates task and redirects", %{conn: conn} do
{:ok, view, _html} = live_isolated(conn, MyAppWeb.TaskLive.Form)
assert form(view, "#task-form", task: %{
title: "Test Task",
description: "Test description"
}) |> render_submit()
assert_redirect(view, ~p"/tasks/*")
end
endโ ๏ธ Version Status
v0.2.0 - Production-Ready Beta
This version includes:
- โ Zero-config field inference
- โ Searchable/creatable combobox
- โ Dynamic nested forms
- โ Pluggable theme system
- โ Full Ash policy enforcement
- โ Comprehensive test suite
Known Limitations:
- Deeply nested forms (3+ levels) require manual path handling
- i18n support planned for v0.3.0
- Field-level permissions planned for v0.3.0
๐ค Contributing
Contributions welcome! Please:
- Fork the repository
-
Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests
-
Run
mix testandmix format - Submit a pull request
Development Setup
git clone https://github.com/nagieeb0/ash_form_builder.git
cd ash_form_builder
mix deps.get
mix test๐ License
MIT License - see LICENSE file for details.
๐ Acknowledgments
- Ash Framework - The excellent Elixir framework
- Phoenix LiveView - Real-time HTML without JavaScript
- MishkaChelekom - UI component library
Built with โค๏ธ using Ash Framework and Phoenix LiveView