Formex

Formex is an abstract layer that helps to build forms in Phoenix and Ecto. With this library you don’t write changeset, but a separate module that declares fields of form (like in Symfony). Formex will build changeset and additional Ecto queries (to get options for <select>) for itself.

Formex also comes with helper functions for templating. For now there is only a Bootstrap 3 form template, but you can easily create your own templates.

It doesn’t require a database, although there is an Ecto dependency. You can use Ecto’s embedded_schema (instead of schema) to use it without database.

TL;DR

Installation

mix.exs

def deps do
  [{:formex, "~> 0.4.0"}]
end

def application do
  [applications: [:formex]]
end

config/config.exs

config :formex,
  repo: App.Repo,
  translate_error: &App.ErrorHelpers.translate_error/1,
  template: Formex.Template.BootstrapHorizontal, # optional, can be overridden in a .eex template
  template_options: [ # optional, also can be overridden in the template
    left_column: "col-sm-2",
    right_column: "col-sm-10"
  ]

web/web.ex

def model do
  quote do
    use Formex.Schema
  end
end

def controller do
  quote do
    use Formex.Controller
  end
end

def view do
  quote do
    use Formex.View
  end
end

Usage

We have models Article, Category and Tag:

schema "articles" do
  field :title, :string
  field :content, :string
  field :hidden, :boolean

  belongs_to :category, App.Category
  many_to_many :tags, App.Tag, join_through: "articles_tags" #...
end
schema "categories" do
  field :name, :string
end
schema "tags" do
  field :name, :string
end

Let’s create a form for Article using Formex:

# /web/form/article_type.ex
defmodule App.ArticleType do
  use Formex.Type
  alias Formex.CustomField.SelectAssoc

  def build_form(form) do
    form
    |> add(:title, :text_input, label: "Title")
    |> add(:content, :textarea, label: "Content", phoenix_opts: [
      rows: 4
    ])
    |> add(:category_id, SelectAssoc, label: "Category", phoenix_opts: [
      prompt: "Choose a category"
    ])
    |> add(:tags, SelectAssoc, label: "Tags")
    |> add(:hidden, :checkbox, label: "Is hidden?", required: false)
    |> add(:save, :submit, label: "Submit", phoenix_opts: [
      class: "btn-primary"
    ])
  end
end

We need to slightly modify a controller:

def new(conn, _params) do
  form = create_form(App.ArticleType, %Article{})
  render(conn, "new.html", form: form)
end

def create(conn, %{"article" => article_params}) do
  App.ArticleType
  |> create_form(%Article{}, article_params)
  |> insert_form_data
  |> case do
    {:ok, _article} ->
      conn
      |> put_flash(:info, "Article created successfully.")
      |> redirect(to: article_path(conn, :index))
    {:error, form} ->
      render(conn, "new.html", form: form)
  end
end

A template:

form.html.eex

<%= formex_form_for @form, @action, fn f -> %>
  <%= if @form.changeset.action do %>Oops, something went wrong!<% end %>

  <%= formex_row f, :name %>
  <%= formex_row f, :content %>
  <%= formex_row f, :category_id %>
  <%= formex_row f, :tags %>
  <%= formex_row f, :hidden %>
  <%= formex_row f, :save %>

  <%# or generate all fields at once: formex_rows f %>
<% end %>

Also replace changeset: @changeset with form: @form in new.html.eex

Put an asterisk to required fields:

.required .control-label:after {
  content: '*';
  margin-right: 3px;
}

The final effect:

It’s very simple, isn’t it? You don’t need to create any changeset nor write a query to get options for a Category select. Furthermore, the form code is separated from the template.

Documentation

https://hexdocs.pm/formex

Basic usage

Custom fields

Templating

Tests

Run this command to migrate:

MIX_ENV=test mix ecto.migrate -r Formex.TestRepo

Now you can use tests via mix test.

TODO

  • [x] more options for Formex.CustomField.SelectAssoc
  • [x] choice_label
  • [x] query
  • [x] GROUP BY
  • [x] multiple_select
  • [x] validate if sent <option> exists in generated :select
  • [x] nested forms
  • [x] _to_one
  • [x] _to_many
  • [x] templating
  • [x] tests
  • [x] submit button
  • [x] usage without schemas
  • [ ] make it work without Ecto so everyone will be happy