/formex_ecto

Ecto integration for Formex

Primary LanguageElixirMIT LicenseMIT

Formex Ecto

Library that integrates Ecto with Formex.

It also has an Ecto.Changeset validator adapter for those who wants to use validation functions from Ecto.Changeset.

Instalation

def deps do
  [{:formex_ecto, "~> 0.2.0"}]
end

config/config.exs

config :formex,
  repo: App.Repo

web/web.ex

def model do
  quote do
    use Formex.Ecto.Schema
  end
end

def controller do
  quote do
    use Formex.Ecto.Controller
  end
end

In every form type that uses Ecto:

defmodule App.ArticleType do
  use Formex.Type
  use Formex.Ecto.Type # <- add this

Optional Ecto.Changeset validator

config/config.exs

config :formex,
  validator: Formex.Ecto.ChangesetValidator

More info about this validator

Usage

Model

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

Form Type

Let's create a form for Article using Formex. For validation we will use Ecto.Changeset validator

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

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

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

def edit(conn, %{"id" => id}) do
  article = Repo.get!(Article, id)
  form = create_form(App.ArticleType, article)
  render(conn, "edit.html", article: article, form: form)
end

def update(conn, %{"id" => id, "article" => article_params}) do
  article = Repo.get!(Article, id)

  App.ArticleType
  |> create_form(article, article_params)
  |> update_form_data
  |> case do
    {:ok, article} ->
      conn
      |> put_flash(:info, "Article updated successfully.")
      |> redirect(to: article_path(conn, :show, article))
    {:error, form} ->
      render(conn, "edit.html", article: article, form: form)
  end
end

Template

form.html.eex

<%= formex_form_for @form, @action, fn f -> %>
  <%= if @form.submitted? 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 and edit.html.eex

The final effect after submit:

Collections of forms

Every schema used in collections of forms should call formex_collection_child:

schema "user_addresses" do
  field       :street, :string
  field       :postal_code, :string
  field       :city, :string
  belongs_to  :user, App.User

  formex_collection_child() # <- add this
end

This macro adds :formex_id and :formex_delete virtual fields.

Automation

This library does few things automatically.

Nested forms and collections

def build_form(form) do
  form
  |> add(:user_info, App.UserInfoType, struct_module: App.UserInfo)
end

You don't need to pass :struct_module option, it is taken from schema information.

Method

<%= formex_form_for @form, article_path(@conn, :create), [method: :post], fn f -> %>

You don't need to pass :method option, it's set basing on struct.id value.

Changeset modification

There is a callback modify_changeset. Examples:

Add something to an user during registration

You can add additional changes while user creation, such as hash of a password.

def build_form(form) do
  form
  |> add(:email, :text_input)
  |> add(:password, :password_input)
  |> add(:save, :submit, label: "Register")
end

# Put additional changes that will be saved to database.
def modify_changeset(changeset, _form) do
  changeset
  |> User.put_pass_hash
end

Assign current logged user to a data which he creates

Controller

Get the current user and pass it to a form

user = Guardian.Plug.current_resource(conn) # or similar

ArticleType
|> create_form(%Article{}, article_params, author: user) # store current logged user in opts
|> insert_form_data
|> case do
  {:ok, _user_employee} ->
    #
  {:error, form} ->
    #
end

Form type

Assign user to a new article (and don't do it if it's an update action)

def build_form(form) do
  #
end

def modify_changeset(changeset, form) do
  # check if it's a create action
  if !form.struct.id do
    changeset
    |> Ecto.Changeset.put_assoc(:author, form.opts[:author]) # access author via form.opts[:author]
  else
    changeset
  end
end

Tests

Test database

Use config/test.secret.example.exs to create config/test.secret.exs

Run this command to migrate:

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

Now you can use tests via mix test.

Creating a new migration

MIX_ENV=test mix ecto.gen.migration migration_name -r Formex.Ecto.TestRepo

Troubleshooting

Repo is nil

Do you have some weird "nil.insert/1 is undefined or private" error?

It happens when you forgot about the repo option in the configuration or you set it after package compilation. To recompile the whole package use: mix deps.compile formex_ecto --force

Docs

Custom fields

Guides