Current section
Files
Jump to
Current section
Files
lib/ex_admin/form.ex
defmodule ExAdmin.Form do
@moduledoc """
Override the default new and edit form pages for an ExAdmin resource.
By default, ExAdmin renders the form page without any additional
configuration. It renders each column in the model, except the id,
inserted_at, and updated_at columns in an attributes table.
To customize the new and edit pages, use the `form` macro.
For example, the following will show on the id an name fields, as
well place a selection column and batch actions row on the page:
defmodule MyProject.ExAdmin.Contact do
use ExAdmin.Register
register_resource MyProject.Contact do
form contact do
inputs do
input contact, :first_name
input contact, :last_name
input contact, :email
input contact, :category, collection: MyProject.Category.all
end
inputs "Groups" do
inputs :groups, as: :check_boxes, collection: MyProject.Group.all
end
end
end
end
## The form command
Call form with a name for your resource and a do block.
## The inputs command
Calling inputs with a do block displays a field set with the specified inputs
from the block.
Adding a label to inputs, labels the field set.
## The input command
The first argument is input is alway the resource name give to the form
command. The second argument is the the field name expressed as an atom.
Optionally, a third argument can be a keyword list.
### Override the default label
input resource, :name, label: "Customer Name"
### Specify a collection
input resource, :category, collection: Repo.all(Category)
### Specify the field names for a collection
input resource, :user, fields: [:first_name, :email]
### Specifying type of control
input user, :password, type: :password
### Array field support
# default tag style
input user, :nicknames
# multi select restricted to the provided collection
input user, :groups, select2: [collection: ~w(Sales Marketing Dev)]
# tags style with the extra collection options
input user, :groups, select2: [collection: ~w(Sales Marketing Dev)]
### Customizing DateTime fields
input user, :start_at, options: [sec: []]
Most of the options from the `datetime_select` control from
`phoenix_html` should work.
### Map field support
Since maps don't have a defined schema, you can define the schema as an option
to the input macro. For example:
form user do
inputs "User Details" do
input user, :name
end
inputs "Statistics" do
input user, :stats, schema: [age: :integer, height: :string, birthday: :string]
end
end
### Array of maps field support
Like maps, you must provided the schema for an array of maps. For example:
form user do
inputs "User Details" do
input user, :name
end
inputs "Addresses" do
input user, :addresses, schema: [street: :string, city: :string]
end
end
## Rendering a has_many :through (many-to-many) relationship
The example at the beginning of the chapter illustrates how to add
a list of groups, displaying them as check boxes.
inputs "Groups" do
inputs :groups, as: :check_boxes, collection: MyProject.Group.all
end
## Nested attributes
ExAdmin supports in-line creation of a has_many, through: (many-to-many)
relationship. The example below allows the user to add/delete phone numbers on the
contact form using the has_many command.
form contact do
inputs do
input contact, :first_name
input contact, :last_name
input contact, :email
input contact, :category, collection: UcxNotifier.Category.all
end
inputs "Phone Numbers" do
has_many contact, :phone_numbers, fn(p) ->
input p, :label, collection: PhoneNumber.labels
input p, :number
end
end
end
Note: has_many does not yet work with simple one-to-many relationships.
# Adding conditional fields
The following complicated example illustrates a number of concepts
possible in a form definition. The example allows management of an
authentication token for a user while on the edit page.
First, the `if params[:id] do` condition ensures that the code block
only executes for an edit form, and not a new form.
Next, the actions command adds in-line content to an inputs block.
form user do
inputs "User Details" do
input user, :name
# ...
end
if params[:id] do
inputs "Authentication Token" do
actions do
user = Repo.get User, params[:id]
if user.authentication_token do
content content_tag(:li, user.authentication_token, style: "padding: 5px 10px")
content content_tag(:li, token_link("Reset Token", :reset, params[:id]), class: "cancel")
content content_tag(:li, token_link("Delete Token", :delete, params[:id]), class: "cancel")
else
content content_tag(:li, token_link("Create Token", :create, params[:id]),
class: "cancel", style: "padding-left: 20px")
end
end
end
end
end
## The javascript command
Use the javascript command to add javascript to the form page.
For example, the following adds a change handler to get a list of
assets using an ajax call:
javascript do
\"""
$(document).ready(function() {
$('#asset_assetable_type_id').change(function() {
$.get('/assets/'+$(this).val()+'/assetables?format=js');
});
});
\"""
end
"""
use ExAdmin.Adminlog
import ExAdmin.Utils
import ExAdmin.Helpers
import ExAdmin.DslUtils
import ExAdmin.Form.Fields
import ExAdmin.ViewHelpers, only: [escape_javascript: 1]
require IEx
import ExAdmin.Theme.Helpers
alias ExAdmin.Schema
import Kernel, except: [div: 2]
use Xain
import Xain, except: [input: 2, input: 1]
defmacro __using__(_) do
quote do
import unquote(__MODULE__)
import Xain, except: [input: 1]
end
end
################
# DSL Macros
@doc """
Customize the form page.
Use the form command to customize the new and edit page forms. Pass
a name for the resource to be created or modified.
"""
defmacro form(resource, [do: contents]) do
quote location: :keep, bind_quoted: [resource: escape(resource), contents: escape(contents)] do
import ExAdmin.Index, only: [index: 1]
def form_view(var!(conn), unquote(resource) = var!(resource), var!(params) = params) do
import ExAdmin.Register, except: [actions: 1]
var!(input_blocks, ExAdmin.Form) = []
var!(script_block, ExAdmin.Form) = nil
unquote(contents)
items = var!(input_blocks, ExAdmin.Form) |> Enum.reverse
script_block = var!(script_block, ExAdmin.Form)
Module.concat(var!(conn).assigns.theme, Form).build_form(
var!(conn), var!(resource), items, var!(params), script_block, ExAdmin.Form.global_script)
end
def get_blocks(var!(conn), unquote(resource) = var!(resource), var!(params) = _params) do
# TODO: do we need the passed params? they are not used.
_ = {var!(conn), var!(resource), var!(params)}
import ExAdmin.Register, except: [actions: 1]
var!(input_blocks, ExAdmin.Form) = []
var!(script_block, ExAdmin.Form) = nil
unquote(contents)
var!(input_blocks, ExAdmin.Form) |> Enum.reverse
end
def ajax_view(conn, params, resource, resources, block) do
defn = conn.assigns.defn
field_name = String.to_atom params[:field_name]
model_name = model_name(resource)
ext_name = ext_name(model_name, field_name)
view = markup safe: true do
ExAdmin.Form.Fields.ajax_input_collection(resource, resources, model_name, field_name, params[:id1], params[:nested2], block, conn.params)
end
~s/$('##{ext_name}_input').html("#{escape_javascript(view)}");/
end
end
end
@doc """
Add an fieldset to the form
"""
defmacro inputs(opts) do
quote(do: inputs("", unquote(opts)))
end
@doc """
Add a has_many field to a form.
"""
defmacro inputs(name, opts, do: block) do
quote location: :keep do
import Xain, except: [input: 1]
# inputs_save = var!(inputs, ExAdmin.Form)
var!(inputs, ExAdmin.Form) = []
unquote(block)
items = var!(inputs, ExAdmin.Form) |> Enum.reverse
# var!(inputs, ExAdmin.Form) = inputs_save
input_block = %{type: :inputs, name: unquote(name), inputs: items, opts: unquote(opts)}
var!(input_blocks, ExAdmin.Form) = [input_block | var!(input_blocks, ExAdmin.Form)]
end
end
@doc """
Add a named fieldset to a form.
Works the same as inputs/1, but labels the fieldset with name
"""
defmacro inputs(name, do: block) do
quote location: :keep do
import Xain, except: [input: 1]
# inputs_save = var!(inputs, ExAdmin.Form)
var!(inputs, ExAdmin.Form) = []
unquote(block)
items = var!(inputs, ExAdmin.Form) |> Enum.reverse
# var!(inputs, ExAdmin.Form) = inputs_save
input_block = %{type: :inputs, name: unquote(name), inputs: items, opts: []}
var!(input_blocks, ExAdmin.Form) = [input_block | var!(input_blocks, ExAdmin.Form)]
end
end
@doc """
Add a has_many field to a form.
"""
defmacro inputs(name, opts) do
quote location: :keep do
import Xain, except: [input: 1]
opts = Enum.into unquote(opts), %{}
item = %{type: :inputs, name: unquote(name), opts: opts}
var!(inputs, ExAdmin.Form) = [item | var!(inputs, ExAdmin.Form)]
end
end
@doc """
Display an input field on the form.
Display all types of form inputs.
## Options
* `:type` - Sets the type of the control (`:password`, `:hidden`, etc)
* `:label` - Sets a custom label
* `:collection` - Sets the collection to render for a `belongs_to` relationship
* `:fields` - Sets a list of fields to be used in a select control.
For example `input post :user, fields: [:first_name, :last_name]`
would render a control like:
`<select>`
`<option id="1">JosƩ Valim</option>`
`<option id="2">Chris McCord</option>
`</select>`
* `:prompt` - Sets a HTML placeholder
* `:change` - Sets change handler to a control. When set to a string,
the string is assumed to be javascript and added with the control.
When a keyword list, the list is used to define what should happen
when the input changes. See the section below on valid keywords.
* `:ajax` - Used for ajax controls. See ajax below
* `:display` - When set to false, the control and its label are
hidden with `style="display: none"`. Use this to hide inputs
that will later be displayed with javascript or an ajax request
* `:as` - Sets the type of collection. Valid options are:
* `:check_boxes` - Use check boxes
* `:radio` - Use radio buttons
## Ajax controls
Use the ajax: true, change: [...] do allow dynamic updating of
nested collection inputs.
For example, assume a page for purchasing
a product, where the product has a number of options and each option
has different possible color selections.
When option 1 is selected, they have the choice of red or black. If
option 2 is selected, they have the choice of red or green. So, based
on the option selected, the color select needs to be dynamically reloaded.
TBD: Complete this
"""
defmacro input(resource, name, opts \\ []) do
quote do
opts = Enum.into unquote(opts), %{}
item = %{type: :input, resource: unquote(resource), name: unquote(name), opts: opts}
var!(inputs, ExAdmin.Form) = [item | var!(inputs, ExAdmin.Form)]
end
end
@doc """
Display a nested resource on the form.
Adds management of a has_many resource to the page, allowing in-line
addition, editing, and deletion of the nested resource.
"""
defmacro has_many(resource, name, opts \\ [], fun \\ nil) do
quote do
opts = ExAdmin.DslUtils.fun_to_opts unquote(opts), unquote(fun)
item = %{type: :has_many, resource: unquote(resource), name: unquote(name), opts: opts}
var!(inputs, ExAdmin.Form) = [item | var!(inputs, ExAdmin.Form)]
end
end
@doc """
Add an action block to a form
TBD: Add more description here
"""
defmacro actions(do: block) do
quote do
var!(inputs, ExAdmin.Form) = []
var!(items, ExAdmin.Form) = []
unquote(block)
items = var!(items, ExAdmin.Form) |> Enum.reverse
item = %{type: :actions, name: "", items: items}
var!(inputs, ExAdmin.Form) = [item | var!(inputs, ExAdmin.Form)]
end
end
@doc """
Add a HTML content block to a form
For example:
content do
\"""
<div>Something here</div>
<div>More stuff here</div>
\"""
end
"""
defmacro content(do: block) do
quote do
contents = unquote(block)
item = %{type: :content, name: "", content: unquote(block), opts: []}
var!(inputs, ExAdmin.Form) = [item | var!(inputs, ExAdmin.Form)]
end
end
@doc """
Add HTML content to a form.
Can be called multiple times to append HTML content for a page
For example:
content content_tag(:li, user.authentication_token, style: "padding: 5px 10px")
content content_tag(:li, token_link("Reset Token", :reset, params[:id]), class: "cancel")
content content_tag(:li, token_link("Delete Token", :delete, params[:id]), class: "cancel")
"""
defmacro content(items, opts \\ quote(do: [])) do
quote do
item = %{type: :content, content: unquote(items), opts: unquote(opts)}
var!(items, ExAdmin.Form) = [item | var!(items, ExAdmin.Form)]
end
end
@doc """
Add javascript to the form
Adds a block of javascript code to the form. Typically used to add
change or click handlers to elements on the page
"""
defmacro javascript(do: block) do
quote do
var!(script_block, ExAdmin.Form) = unquote(block)
end
end
#################
# Functions
defp build_item(resource, defn, name) do
Adminlog.debug "build_item 1 ...."
case translate_field defn, name do
field when field == name ->
%{type: :input, resource: resource, name: name, opts: %{}}
{:map, _field} ->
%{type: :input, resource: resource, name: name, opts: %{map: true}}
{:maps, _field} ->
%{type: :input, resource: resource, name: name, opts: %{maps: true}}
field ->
case resource.__struct__.__schema__(:association, field) do
%Ecto.Association.BelongsTo{cardinality: :one, queryable: assoc} ->
repo = Application.get_env(:ex_admin_runtime, :repo)
collection = case repo.aggregate(assoc, :count, :id) do
n when n < 1000 -> repo.all assoc
_ -> Enum.filter([Map.fetch!(resource, field)], &(&1))
end
%{type: :input, resource: resource, name: field, opts: %{collection: collection}}
_ ->
nil
end
end
end
def setup_resource(resource, params, model_name) do
model_name = String.to_atom(model_name)
case params[model_name] do
nil -> resource
model_params ->
struct(resource, Map.to_list(model_params))
end
end
def put_script_block(script_block) do
if script_block do
Xain.script type: "text/javascript" do
text "\n" <> script_block <> "\n"
end
end
end
def build_scripts(list) do
head = "$(function() {\n"
script = for i <- list, is_tuple(i), into: head, do: build_script(i)
script <> "});"
end
def build_script({:change, %{id: id, script: script}}) do
"""
$(document).on('change','##{id}', function() {
#{script}
});
"""
end
def build_script(_other), do: ""
def get_action(resource, mode) do
case mode do
:new ->
admin_resource_path(resource, :create)
:edit ->
admin_resource_path(resource, :update)
end
end
defp get_put_fields(:edit) do
Xain.input(name: "_method", value: "put", type: "hidden")
end
defp get_put_fields(_), do: nil
def build_hidden_block(_conn, mode) do
csrf = Plug.CSRFProtection.get_csrf_token
div style: "margin:0;padding:0;display:inline" do
Xain.input(name: "utf8", type: :hidden, value: "ā")
Xain.input(type: :hidden, name: "_csrf_token", value: csrf)
get_put_fields(mode)
end
end
def build_main_block(conn, resource, model_name, schema) do
errors = Phoenix.Controller.get_flash(conn, :inline_error)
for item <- schema do
item = put_in item, [:required], conn.assigns[:ea_required] || []
build_item(conn, item, resource, model_name, errors)
end
|> flatten
end
defp flatten(list) when is_list(list), do: List.flatten(list)
defp flatten(other), do: [other]
@hidden_style [style: "display: none"]
defp check_display(opts) do
if Map.get(opts, :display, true), do: [], else: @hidden_style
end
defp check_params(display_style, resource, params, model_name, field_name, _ajax) do
cond do
params["id"] -> []
params[model_name][params_name(resource, field_name, params)] -> []
true -> display_style
end
end
defp field_type(resource, field_name) do
field_type_matching = Application.get_env(:ex_admin_runtime, :field_type_matching) || %{}
original_ft = resource.__struct__.__schema__(:type, field_name)
Map.get(field_type_matching, original_ft, original_ft)
end
defp params_name(resource, field_name, _params) do
case resource.__struct__.__schema__(:association, field_name) do
%{cardinality: :one, owner_key: owner_key} ->
Atom.to_string(owner_key)
%{cardinality: :many, owner_key: owner_key, through: [_, name]} ->
Atom.to_string(name) <> "_" <> Inflex.pluralize(Atom.to_string(owner_key))
_ ->
Atom.to_string(field_name)
end
end
@doc false
def wrap_item(resource, field_name, model_name, label, error, opts, params, required, contents) do
as = Map.get opts, :as
ajax = Map.get opts, :ajax
ext_name = ext_name(model_name, field_name)
display_style = check_display(opts)
|> check_params(resource, params, model_name, field_name, ajax)
{label, hidden} = case label do
{:hidden, l} -> {l, @hidden_style}
l when l in [:none, false] -> {"", @hidden_style}
l -> {l, display_style}
end
{error, hidden} =
if error in [nil, [], false] do
{"", hidden}
else
{"error ", []}
end
{theme_module(Form).theme_wrap_item(field_type(resource, field_name), ext_name, label, hidden,
ajax, error, contents, as, required), ext_name}
end
def wrap_item_type(type, label, ext_name, contents, error, required) do
ExAdmin.theme.wrap_item_type(type, label, ext_name, contents, error, required)
end
defp build_select_binary_tuple_list(collection, item, field_name, resource, model_name, ext_name) do
html_opts = item[:opts][:html_opts] || []
html_opts = Keyword.merge([name: "#{model_name}[#{field_name}]"], html_opts)
select("##{ext_name}_id.form-control", html_opts) do
handle_prompt(field_name, item)
for item <- collection do
{value, name} = case item do
{value, name} -> {value, name}
other -> {other, other}
end
selected = if Map.get(resource, field_name) == value,
do: [selected: :selected], else: []
option(name, [value: value] ++ selected)
end
end
end
@doc false
def build_item(_conn, %{type: :script, contents: contents}, _resource, _model_name, _errors) do
Adminlog.debug "build_item 2:"
script type: "javascript" do
text "\n" <> contents <> "\n"
end
end
def build_item(conn, %{type: :input, field_type: :map, name: field_name, resource: _resource
} = item, resource, model_name, _error) do
Adminlog.debug "build_item 11: #{inspect field_name}"
schema = get_schema(item, field_name)
data = Map.get(resource, field_name, model_name) || %{}
for {field, type} <- schema do
build_input(conn, type, field, field_name, data, model_name)
end
|> Enum.join("\n")
end
def build_item(conn, %{type: :input, field_type: {:array, :map}, name: field_name, resource: _resource} = item, resource, model_name, error) do
Adminlog.debug "build_item 12: #{inspect field_name}"
schema = get_schema(item, field_name)
human_label = "#{humanize(field_name) |> Inflex.singularize}"
new_record_name_var = new_record_name field_name
div ".has_many.#{field_name}" do
{contents, html} = theme_module(conn, Form).build_inputs_has_many field_name, human_label, fn ->
resource_contents = (Map.get(resource, field_name) || [])
|> Enum.with_index
|> Enum.map(fn({res, inx}) ->
errors = map_array_errors(error, field_name, inx)
html = theme_module(conn, Form).theme_map_field_set(conn, res, schema, inx, field_name, model_name, errors)
html
end)
fieldset_contents = theme_module(conn, Form).theme_map_field_set(conn, :nil, schema, new_record_name(field_name), field_name, model_name, nil)
{fieldset_contents, resource_contents}
end
{_, onclick} = Phoenix.HTML.html_escape theme_module(conn, Form).has_many_insert_item(
contents, new_record_name_var)
markup do
html
theme_module(conn, Form).theme_button("Add New #{human_label}", href: "#", onclick: onclick, type: ".btn-primary")
end
end
end
@doc """
Private: Build a belongs_to control.
Generate a select box for a belongs_to control.
"""
def build_item(conn, %{type: :input, name: field_name, resource: _resource,
opts: %{collection: collection}} = item, resource, model_name, error) do
Adminlog.debug "build_item 3: #{inspect field_name}"
# IO.puts "build_item 3: #{inspect field_name}"
collection = if is_function(collection), do: collection.(conn, resource), else: collection
module = resource.__struct__
errors_field_name = if field_name in module.__schema__(:associations) do
Map.get module.__schema__(:association, field_name), :owner_key
else
field_name
end
required = if errors_field_name in (conn.assigns[:ea_required] || []),
do: true, else: false
errors = get_errors(error, errors_field_name)
label = Map.get item[:opts], :label, field_name
onchange = Map.get item[:opts], :change
binary_tuple = binary_tuple?(collection)
{html, _id} = wrap_item(resource, field_name, model_name, label, errors, item[:opts],
conn.params, required, fn(ext_name) ->
item = update_in item[:opts], &(Map.delete(&1, :change) |> Map.delete(:ajax))
markup do
if binary_tuple do
build_select_binary_tuple_list(collection, item, field_name, resource, model_name, ext_name)
else
input_collection(resource, collection, model_name, field_name, nil, nil, item, conn.params, error)
end
build_errors(errors, item[:opts][:hint])
end
end)
id = ext_name(model_name, field_name)
value = case onchange do
script when is_binary(script) ->
{:change, %{id: id <> "_id", script: onchange}}
list when is_list(list) ->
update = Keyword.get(list, :update)
params = Keyword.get(list, :params)
if update do
route_path = admin_resource_path(resource.__struct__, :index)
target = pluralize(field_name)
nested = pluralize(update)
{extra, param_str} =
case params do
atom when is_atom(atom) -> extra_javascript(model_name, atom, atom)
[{param, attr}] -> extra_javascript(model_name, param, attr)
_ -> {"", ""}
end
control_id = "#{model_name}_#{update}_input"
get_cmd = if resource.id do
"$.get('#{route_path}/#{resource.id}/#{target}/'+$(this).val()+'/#{nested}"
else
"$.get('#{route_path}/#{target}/'+$(this).val()+'/#{nested}"
end
script = "$('##{control_id}').show();\n" <>
extra <>
"console.log('show #{control_id}');\n" <>
get_cmd <>
"/?field_name=#{update}#{param_str}&format=js');\n"
{:change, %{id: id <> "_id", script: script}}
end
_ -> nil
end
if onchange do
{html, value}
else
html
end
end
def build_item(conn, %{type: :actions, items: items}, resource, model_name, errors) do
Adminlog.debug "build_item 4: #{inspect model_name}"
fieldset ".actions" do
for i <- items do
build_item(conn, i, resource, model_name, errors)
end
end
end
def build_item(_conn, %{type: :content, content: content}, _resource, _model_name, _errors) when is_binary(content) do
Adminlog.debug "build_item 5."
text content
end
def build_item(_conn, %{type: :content, content: content}, _resource, _model_name, _errors) do
Adminlog.debug "build_item 6."
text elem(content, 1)
end
def build_item(conn, %{type: :input, resource: _resource, name: field_name, opts: opts},
resource, model_name, errors) do
Adminlog.debug "build_item 7: #{inspect field_name}"
errors = get_errors(errors, field_name)
label = get_label(field_name, opts)
required = if field_name in (conn.assigns[:ea_required] || []), do: true, else: false
{html, _id} = wrap_item(resource, field_name, model_name, label, errors, opts, conn.params, required, fn(ext_name) ->
field_type = opts[:type] || field_type(resource, field_name)
[
build_control(field_type, resource, opts, model_name, field_name, ext_name),
build_errors(errors, opts[:hint])
]
end)
html
end
def build_item(conn, %{type: :has_many, resource: _resource, name: field_name,
opts: %{fun: fun}}, resource, model_name, errors) do
Adminlog.debug "build_item 8: #{inspect field_name}"
field_field_name = "#{field_name}_attributes"
human_label = "#{humanize(field_name) |> Inflex.singularize}"
new_record_name_var = new_record_name field_name
div ".has_many.#{field_name}" do
{contents, html} = theme_module(conn, Form).build_inputs_has_many field_name,
human_label, fn ->
resource_content = get_resource_field2(resource, field_name)
|> Enum.with_index
|> Enum.map(fn({res, inx}) ->
fields = fun.(res) |> Enum.reverse
ext_name = "#{model_name}_#{field_field_name}_#{inx}"
{new_inx, html} = build_has_many_fieldset(conn, res, fields, inx,
ext_name, field_name, field_field_name, model_name, errors)
res_id = ExAdmin.Schema.get_id(res)
markup do
html
Xain.input [id: "#{ext_name}_id",
name: "#{model_name}[#{field_field_name}][#{new_inx}][id]",
value: "#{res_id}", type: :hidden]
end
end)
ext_name = "#{model_name}_#{field_field_name}_#{new_record_name_var}"
{assoc_model, _} = assoc_model_tuple = ExAdmin.Repo.get_assoc_model(resource, field_name)
fields = fun.(assoc_model_tuple) |> Enum.reverse
{_, fieldset_contents} = build_has_many_fieldset(conn, assoc_model.__struct__,
fields, new_record_name_var, ext_name, field_name, field_field_name,
model_name, errors)
{fieldset_contents, resource_content}
end
html
{_, onclick} = Phoenix.HTML.html_escape theme_module(conn, Form).has_many_insert_item(
contents, new_record_name_var)
theme_module(conn, Form).theme_button "Add New #{human_label}",
href: "#", onclick: onclick, type: ".btn-primary"
end
end
@doc """
Handle building an inputs :name, as: ...
"""
def build_item(conn, %{type: :inputs, name: name, opts: %{collection: collection} = opts},
resource, model_name, errors) when is_atom(name) do
Adminlog.debug "build_item 9: #{inspect name}"
collection = if is_function(collection), do: collection.(conn, resource), else: collection
errors = get_errors(errors, name)
name_ids = "#{Atom.to_string(name) |> Inflex.singularize}_ids"
assoc_ids = Enum.map(get_resource_field2(resource, name), &(Schema.get_id(&1)))
name_str = "#{model_name}[#{name_ids}][]"
required = get_required name, opts
theme_module(conn, Form).build_inputs_collection model_name, name, name_ids, required, fn ->
markup do
Xain.input name: name_str, type: "hidden", value: ""
if opts[:as] == :check_boxes do
theme_module(conn, Form).wrap_collection_check_boxes fn ->
for opt <- collection do
opt_id = Schema.get_id(opt)
name_str = "#{model_name}[#{name_ids}][#{opt_id}]"
selected = opt_id in assoc_ids
display_name = display_name opt
theme_module(conn, Form).collection_check_box display_name, name_str,
opt_id, selected
end
end
else
select id: "#{model_name}_#{name_ids}", class: "form-control", multiple: "multiple",
name: name_str do
for opt <- collection do
opt_id = Schema.get_id(opt)
selected = if opt_id in assoc_ids, do: [selected: "selected"], else: []
display_name = display_name opt
option "#{display_name}", [value: "#{opt_id}"] ++ selected
end
end
end
build_errors(errors, opts[:hint])
end
end
end
@doc """
Handle building the items for an input block.
This is where each of the fields will be build
"""
def build_item(conn, %{type: :inputs, name: _field_name} = item, resource, model_name, errors) do
opts = Map.get(item, :opts, [])
Adminlog.debug "build_item 10: #{inspect _field_name}"
theme_module(conn, Form).form_box item, opts, fn ->
theme_module(conn, Form).theme_build_inputs item, opts, fn ->
for inpt <- item[:inputs] do
type = resource.__struct__.__schema__(:type, inpt[:name])
item = put_in inpt, [:field_type], type
build_item(conn, item, resource, model_name, errors)
end
end
end
end
defp get_schema(item, field_name) do
schema = item[:opts][:schema]
unless schema, do: raise("Can't render map without schema #{inspect field_name}")
schema
end
def build_input(conn, type, field, field_name, data, model_name, errors \\ nil, index \\ nil) do
field = to_string field
error = if errors in [nil, [], false], do: "", else: ".has-error"
{inx, id} = if is_nil(index) do
{"", "#{model_name}_#{field_name}_#{field}"}
else
{"[#{index}]", "#{model_name}_#{field_name}_#{index}_#{field}"}
end
name = "#{model_name}[#{field_name}]#{inx}[#{field}]"
label = humanize field
theme_module(conn, Form).build_map(id, label, index, error, fn class ->
markup do
[]
|> Keyword.put(:type, input_type(type))
|> Keyword.put(:class, class)
|> Keyword.put(:id, id)
|> Keyword.put(:name, name)
|> Keyword.put(:value, data[field])
|> Xain.input
build_errors(errors, nil)
end
end)
end
defp input_type(:string), do: "text"
defp input_type(:integer), do: "number"
defp input_type(_), do: "text"
@doc false
def build_control(:boolean, resource, opts, model_name, field_name, ext_name) do
opts = unless Map.get(resource, field_name) in [false, nil, "false"] do
Map.put_new(opts, :checked, "checked")
else
opts
end
opts = opts
|> Map.put_new(:type, :checkbox)
|> Map.put_new(:value, "true")
|> Map.put_new(:name, "#{model_name}[#{field_name}]")
|> Map.put_new(:id, ext_name)
|> Map.to_list
markup do
Xain.input(type: :hidden, value: "false", name: "#{model_name}[#{field_name}]")
Xain.input(opts)
end
end
def build_control(Ecto.DateTime, resource, opts, model_name, field_name, _ext_name) do
%{name: model_name, model: resource, id: model_name, class: "form-control"}
|> datetime_select(field_name, Map.get(opts, :options, []))
end
def build_control(Ecto.Date, resource, opts, model_name, field_name, _ext_name) do
%{name: model_name, model: resource, id: model_name, class: "form-control"}
|> date_select(field_name, Map.get(opts, :options, []))
end
def build_control(Ecto.Time, resource, opts, model_name, field_name, _ext_name) do
%{name: model_name, model: resource, id: model_name, class: "form-control"}
|> time_select(field_name, Map.get(opts, :options, []))
end
def build_control(:text, resource, opts, model_name, field_name, ext_name) do
value = Map.get(resource, field_name, "") |> escape_value
options = opts
|> Map.put(:class, "form-control")
|> Map.put_new(:name, "#{model_name}[#{field_name}]")
|> Map.put_new(:id, ext_name)
|> Map.delete(:display)
|> Map.to_list
Xain.textarea(value, options)
end
def build_control({:array, type}, resource, opts, model_name, field_name, ext_name) when type in ~w(string integer)a do
name = "#{model_name}-#{field_name}"
# currently we only support select 2
opts = Map.put_new opts, :select2, [tags: true]
ctrl_opts =
opts
|> Map.put(:class, "form-control #{name}")
|> Map.put(:multiple, true)
|> Map.put_new(:name, "#{model_name}[#{field_name}][]")
|> Map.put_new(:id, ext_name)
|> Map.delete(:display)
|> Map.delete(:select2)
|> Map.to_list
options = case Map.get(resource, field_name, []) do
nil -> []
list when is_list(list) -> list
string when is_binary(string) ->
String.split(string, " ")
end
build_array_control_select2(opts[:select2], name)
|> build_array_control_control(ctrl_opts, options)
|> build_array_control_block
end
def build_control(type, resource, opts, model_name, field_name, ext_name) do
{field_type, value} = cond do
type == :file || (type |> Kernel.to_string |> String.ends_with?(".Type")) ->
val = Map.get(resource, field_name, %{}) || %{}
{:file, Map.get(val, :filename, "")}
true ->
{:text, Map.get(resource, field_name, "")}
end
value = ExAdmin.Render.to_string(value)
Map.put_new(opts, :type, field_type)
|> Map.put(:class, "form-control")
|> Map.put_new(:maxlength, "255")
|> Map.put_new(:name, "#{model_name}[#{field_name}]")
|> Map.put_new(:id, ext_name)
|> Map.put_new(:value, value |> escape_value)
|> Map.delete(:display)
|> Map.to_list
|> Xain.input
end
defp build_array_control_control({collection, script}, ctrl_opts, options) do
select = Xain.select ctrl_opts do
Enum.map(options, fn opt ->
Xain.option opt, value: opt, selected: "selected", style: "color: #333"
end) ++
Enum.map(collection, fn opt ->
Xain.option opt, value: opt
end)
end
{select, script}
end
defp build_array_control_block({select, nil}), do: select
defp build_array_control_block({select, script}), do: [select, script]
defp build_array_control_select2(nil, _), do: {nil, []}
defp build_array_control_select2(select2, name) do
Keyword.pop(select2, :collection, [])
|> build_array_control_select2_script_opts
|> build_array_control_select2_script(name)
end
def build_array_control_select2_script_opts({collection, true}) do
{collection, "{}"}
end
def build_array_control_select2_script_opts({collection, list}) when is_list(list) do
args = Enum.reduce(list, [], fn {k,v}, acc ->
["#{k}: #{v}"|acc]
end)
|> Enum.reverse
|> Enum.join(", ")
{collection, "{#{args}}"}
end
def build_array_control_select2_script({collection, options}, name) do
script = Xain.script do
"""
$(document).ready(function() {
$(".#{name}").select2(#{options});
})
"""
end
{collection, script}
end
def datetime_select(form, field_name, opts \\ []) do
value = value_from(form, field_name)
builder =
Keyword.get(opts, :builder) || fn b ->
markup do
date_builder(b, opts)
span ".date-time-separator"
time_builder(b, opts)
end
end
builder.(datetime_builder(form, field_name, date_value(value), time_value(value), opts))
end
def date_select(form, field_name, opts \\ []) do
value = Keyword.get(opts, :value, value_from(form, field_name) || Keyword.get(opts, :default))
builder = Keyword.get(opts, :builder) || &date_builder(&1, opts)
builder.(datetime_builder(form, field_name, date_value(value), nil, opts))
end
defp date_builder(b, _opts) do
markup do
b.(:year, [])
span(".date-separator")
b.(:month, [])
span(".date-separator")
b.(:day, [])
end
end
defp date_value(%{"year" => year, "month" => month, "day" => day}),
do: %{year: year, month: month, day: day}
defp date_value(%{year: year, month: month, day: day}),
do: %{year: year, month: month, day: day}
defp date_value({{year, month, day}, _}),
do: %{year: year, month: month, day: day}
defp date_value({year, month, day}),
do: %{year: year, month: month, day: day}
defp date_value(nil),
do: %{year: nil, month: nil, day: nil}
defp date_value(other),
do: raise(ArgumentError, "unrecognized date #{inspect other}")
def time_select(form, field, opts \\ []) do
value = Keyword.get(opts, :value, value_from(form, field) || Keyword.get(opts, :default))
builder = Keyword.get(opts, :builder) || &time_builder(&1, opts)
builder.(datetime_builder(form, field, nil, time_value(value), opts))
end
defp time_builder(b, opts) do
markup do
b.(:hour, [])
span ".time-separator"
b.(:min, [])
if Keyword.get(opts, :sec) do
span ".time-separator"
b.(:sec, [])
end
end
end
defp time_value(%{"hour" => hour, "min" => min} = map),
do: %{hour: hour, min: min, sec: Map.get(map, "sec", 0)}
defp time_value(%{hour: hour, min: min} = map),
do: %{hour: hour, min: min, sec: Map.get(map, :sec, 0)}
defp time_value({_, {hour, min, sec, _msec}}),
do: %{hour: hour, min: min, sec: sec}
defp time_value({hour, min, sec, _mseg}),
do: %{hour: hour, min: min, sec: sec}
defp time_value({_, {hour, min, sec}}),
do: %{hour: hour, min: min, sec: sec}
defp time_value({hour, min, sec}),
do: %{hour: hour, min: min, sec: sec}
defp time_value(nil),
do: %{hour: nil, min: nil, sec: nil}
defp time_value(other),
do: raise(ArgumentError, "unrecognized time #{inspect other}")
@months [
{"January", "1"},
{"February", "2"},
{"March", "3"},
{"April", "4"},
{"May", "5"},
{"June", "6"},
{"July", "7"},
{"August", "8"},
{"September", "9"},
{"October", "10"},
{"November", "11"},
{"December", "12"},
]
map = &Enum.map(&1, fn i ->
i = Integer.to_string(i)
{String.rjust(i, 2, ?0), i}
end)
@days map.(1..31)
@hours map.(0..23)
@minsec map.(0..59)
defp datetime_builder(form, field, date, time, parent) do
id = Keyword.get(parent, :id, id_from(form, field))
name = Keyword.get(parent, :name, name_from(form, field))
fn
:year, opts when date != nil ->
{year, _, _} = :erlang.date()
{value, opts} = datetime_options(:year, year-5..year+5, id, name, parent, date, opts)
build_select(:datetime, :year, value, opts)
:month, opts when date != nil ->
{value, opts} = datetime_options(:month, @months, id, name, parent, date, opts)
build_select(:datetime, :month, value, opts)
:day, opts when date != nil ->
{value, opts} = datetime_options(:day, @days, id, name, parent, date, opts)
build_select(:datetime, :day, value, opts)
:hour, opts when time != nil ->
{value, opts} = datetime_options(:hour, @hours, id, name, parent, time, opts)
build_select(:datetime, :hour, value, opts)
:min, opts when time != nil ->
{value, opts} = datetime_options(:min, @minsec, id, name, parent, time, opts)
build_select(:datetime, :min, value, opts)
:sec, opts when time != nil ->
{value, opts} = datetime_options(:sec, @minsec, id, name, parent, time, opts)
build_select(:datetime, :sec, value, opts)
end
end
defp build_select(_name, _type, value, opts) do
value = if Range.range? value do
Enum.map value, fn(x) ->
val = Integer.to_string x
{val,val}
end
else
value
end
select "", [{:class, "form-control date-time"} |opts] do
current_value = "#{opts[:value]}"
Enum.map value, fn({k,v}) ->
selected = if v == current_value, do: [selected: "selected"], else: []
option k, [{:value, v}| selected]
end
end
end
defp datetime_options(type, values, id, name, parent, datetime, opts) do
opts = Keyword.merge Keyword.get(parent, type, []), opts
suff = Atom.to_string(type)
{value, opts} = Keyword.pop(opts, :options, values)
{value,
opts
|> Keyword.put_new(:id, id <> "_" <> suff)
|> Keyword.put_new(:name, name <> "[" <> suff <> "]")
|> Keyword.put_new(:value, Map.get(datetime, type))}
end
defp value_from(%{model: resource}, field_name) do
Map.get(resource, field_name, "")
end
defp id_from(%{id: id}, field),
do: "#{id}_#{field}"
defp id_from(name, field) when is_atom(name),
do: "#{name}_#{field}"
defp name_from(%{name: name}, field),
do: "#{name}[#{field}]"
defp name_from(name, field) when is_atom(name),
do: "#{name}[#{field}]"
@doc false
def build_has_many_fieldset(conn, res, fields, orig_inx, ext_name, field_name, field_field_name, model_name, errors) do
theme_module(conn, Form).theme_build_has_many_fieldset(conn, res, fields,
orig_inx, ext_name, field_name, field_field_name, model_name, errors)
end
@doc false
def get_label(field_name, opts) do
cond do
Map.get(opts, :type) in ["hidden", :hidden] ->
:none
Map.get(opts, :display) ->
{:hidden, Map.get(opts, :label, field_name) }
Map.get(opts, :ajax) ->
{:ajax, Map.get(opts, :label, field_name)}
true ->
Map.get opts, :label, field_name
end
end
defp new_record_name(field_name) do
name = field_name
|> Atom.to_string
|> Inflex.singularize
|> String.replace(" ", "_")
|> String.upcase
"NEW_#{name}_RECORD"
end
def escape_value(nil), do: nil
def escape_value(value) when is_map(value), do: value
def escape_value(value) do
Phoenix.HTML.html_escape(value) |> elem(1)
end
@doc false
def build_field_errors(conn, field_name) do
conn.private
|> Map.get(:phoenix_flash, %{})
|> Map.get("inline_error", [])
|> get_errors(field_name)
|> Enum.reduce("", fn(error, acc) ->
acc <> """
<p class="inline-errors">#{error_messages error}</p>
"""
end)
end
@doc false
def default_form_view(conn, resource, params) do
case conn.assigns.defn do
nil ->
throw :invalid_route
%{__struct__: _} = defn ->
columns = defn.resource_model.__schema__(:fields)
|> Enum.filter(&(not &1 in [:id, :inserted_at, :updated_at]))
|> Enum.map(&(build_item resource, defn, &1))
|> Enum.filter(&(not is_nil(&1)))
items = [%{type: :inputs, name: "", inputs: columns, opts: []}]
Module.concat(var!(conn).assigns.theme, Form).build_form(
conn, resource, items, params, false, ExAdmin.Form.global_script)
end
end
defp binary_tuple?([]), do: false
defp binary_tuple?(collection) do
Enum.all?(collection, &(is_binary(&1) or (is_tuple(&1) and (tuple_size(&1) == 2))))
end
def required_abbr(true) do
abbr ".required *", title: "required"
end
def required_abbr(_), do: ""
def get_required(field_name, %{required: required}) do
if field_name in required, do: true, else: false
end
def get_required(_, _), do: false
@doc false
def extra_javascript(model_name, param, attr) do
{"var extra = $('##{model_name}_#{attr}').val();\n", "&#{param}='+extra+'"}
end
@doc false
defp map_array_errors(nil, _, _), do: nil
defp map_array_errors(errors, field_name, inx) do
Enum.filter_map(errors || [],
fn {k,{_err, opts}} -> k == field_name and opts[:index] == inx end,
fn {_k,{err, opts}} -> {opts[:field], err} end)
end
@doc false
def get_errors(nil, _field_name), do: nil
def get_errors(errors, field_name) do
Enum.reduce errors, [], fn({k, v}, acc) ->
if k == field_name, do: [v | acc], else: acc
end
end
@doc false
def build_errors(nil, nil), do: nil
def build_errors(nil, hint) do
theme_module(Form).build_hint(hint)
end
def build_errors(errors, _) do
for error <- errors do
theme_module(Form).build_form_error(error)
end
end
@doc false
def error_messages(:unique), do: "has already been taken"
def error_messages(:invalid), do: "has to be valid"
def error_messages({:too_short, min}), do: "must be longer than #{min - 1}"
def error_messages({:must_match, field}), do: "must match #{humanize field}"
def error_messages(:format), do: "has incorrect format"
def error_messages("empty"), do: "can't be blank"
def error_messages({msg, opts}) when is_binary(msg) do
count = if is_integer(opts[:count]), do: opts[:count], else: 0
String.replace(msg, "%{count}", Integer.to_string(count))
end
def error_messages(other) when is_binary(other), do: other
def error_messages(other), do: "error: #{inspect other}"
def global_script, do: """
$(function() {
$(document).on('click', '.remove_has_many_maps', function() {
console.log('remove has many maps');
$(this).closest(".has_many_fields").remove();
return false;
});
});
"""
end