Packages
formex
0.4.1
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Form library for Phoenix with Ecto support
Current section
Files
Jump to
Current section
Files
lib/formex/builder.ex
defmodule Formex.Builder do
import Ecto.Changeset
import Ecto.Query
alias Formex.Form
alias Formex.Utils.Counter
@repo Application.get_env(:formex, :repo)
@moduledoc """
The form builder to be used in a controller. Imported by `Formex.Controller`.
Example:
```
form = create_form(App.ArticleType, %Article{})
render(conn, "new.html", form: form)
```
"""
@doc """
Creates a form struct.
## Arguments
* `type` - the module that implements `Formex.Type` behaviour, for example: `App.ArticleType`
* `struct` - the struct that will be used in `Ecto.Changeset.cast/3`, for example: `%App.Article{}`
* `params` - the parameters that will be used in `Ecto.Changeset.cast/3`
* `model` - optional model in case if `struct` is nil. Used by `Formex.Type.add_form/4`
"""
@spec create_form(module, Ecto.Schema.t, Map.t) :: Form.t
def create_form(type, struct, params \\ %{}, model \\ nil) do
form = %Form{
type: type,
struct: struct,
model: if(model, do: model, else: struct.__struct__),
params: params
}
|> type.build_form()
form
|> Map.put(:struct, preload_assocs(form))
|> create_changeset(type)
end
#
defp preload_assocs(form) do
form
|> Form.get_fields
|> Enum.filter(&(&1.type == :multiple_select))
|> Enum.reduce(form.struct, fn field, struct ->
@repo.preload(struct, field.name)
end)
end
#
defp create_changeset(form, type) do
struct = if form.struct, do: form.struct, else: struct(form.model)
changeset = struct
|> cast(form.params, get_normal_fields_names(form))
|> cast_multiple_selects(form)
|> cast_embedded_forms(form)
|> validate_required(get_required_fields_names(form))
|> validate_selects(form)
|> type.changeset_after_create_callback
form
|> Map.put(:changeset, changeset)
end
defp cast_multiple_selects(changeset, form) do
Form.get_fields(form)
|> Enum.filter(&(&1.type == :multiple_select))
|> Enum.reduce(changeset, fn field, changeset ->
module = form.model.__schema__(:association, field.name).queryable
ids = form.params[to_string(field.name)] || []
associated = module
|> where([c], c.id in ^ids)
|> @repo.all
|> Enum.map(&Ecto.Changeset.change/1)
changeset
|> put_assoc(field.name, associated)
end)
end
defp cast_embedded_forms(changeset, form) do
Form.get_subforms(form)
|> Enum.reduce(changeset, fn item, changeset ->
case item do
%Formex.FormNested{} ->
changeset
|> cast_assoc(item.name, required: item.required, with: fn _substruct, _params ->
subform = item.form
create_changeset(subform, subform.type).changeset
end)
%Formex.FormCollection{} ->
{:ok, pid} = Counter.start_link # does anyone has a better idea?
changeset = changeset
|> cast_assoc(item.name, required: item.required, with: fn _substruct, _params ->
subform = item.forms
|> Enum.at(Counter.increment(pid))
|> Map.get(:form)
changeset = create_changeset(subform, subform.type).changeset
|> cast(subform.params, [:formex_delete])
if get_change(changeset, :formex_delete) do
%{changeset | action: :delete}
else
changeset
end
end)
Counter.reset(pid) # Ecto can run once again above callback so we need to reset counter.
# the weirdest hackish thing I ever wrote. it should be changed.
# maybe add an unique ID for every struct and in this way identify it. or make pull request
# to Ecto.Changeset with another callback - (struct, params, *key*) - key is all I need
changeset
end
end)
end
#
defp get_normal_fields_names(form) do
Form.get_fields(form)
|> filter_normal_fields(form)
|> Enum.map(&(&1.name))
end
defp get_required_fields_names(form) do
Form.get_fields(form)
|> Enum.filter(&(&1.required))
|> filter_normal_fields(form)
|> Enum.map(&(&1.name))
end
# It will find only many_to_many and one_to_many associations (not many_to_one),
# because the names (field.name) of many_to_one assocs ends with "_id". This is ok
defp filter_normal_fields(items, form) do
items
|> Enum.filter(fn field ->
form.model.__schema__(:association, field.name) == nil
end)
end
@spec validate_selects(changeset :: Changeset.t, form :: Form.t) :: Changeset.t
defp validate_selects(changeset, form) do
form
|> Form.get_fields
|> Enum.filter(&(&1.type == :select))
|> Enum.filter(&(changeset.changes[&1.name] != nil))
|> Enum.reduce(changeset, fn (field, changeset) ->
value_exists = Enum.find(field.data[:choices], fn {_, id} ->
changeset.changes[field.name] == id
end)
if value_exists do
changeset
else
add_error(changeset, field.name, "is invalid")
end
end)
end
end