defmodule Formex.Ecto.CustomField.SelectAssoc do @behaviour Formex.CustomField import Ecto.Query alias Formex.Field @repo Application.get_env(:formex, :repo) @moduledoc """ This module generates a `:select` field with options downloaded from Repo. Example of use for Article with one Category: ``` schema "articles" do belongs_to :category, App.Category end ``` ``` form |> add(:category_id, Formex.Ecto.CustomField.SelectAssoc, label: "Category") ``` Formex will find out that `:category_id` refers to App.Category schema and download all rows from Repo ordered by name. If you are using `:without_choices` option (from `Formex.Field.create_field/3`), you don't need to implement `:choice_label_provider`, this module will do it for you. ## Options * `choice_label` - controls the content of ``'s. May be `:field_name`, `:assoc_name` or `[:assoc_name, :field_name]` Example of use: ``` schema "users" do field :first_name, :string field :last_name, :string belongs_to :department, App.Department end ``` ``` schema "departments" do field :name, :string field :description, :string end ``` Group by last name of user: ``` form |> add(:user_id, SelectAssoc, group_by: :last_name) ``` Group by department, by `:name` (default) field: ``` form |> add(:user_id, SelectAssoc, group_by: :department) ``` Group by department, but by another field ``` form |> add(:user_id, SelectAssoc, group_by: [:department, :description]) ``` """ @doc false def create_field(form, name, opts) do if form.struct_module.__schema__(:association, name) == nil do create_field_single(form, name, opts) else create_field_multiple(form, name, opts) end end defp create_field_single(form, name_id, opts) do name = Regex.replace(~r/_id$/, Atom.to_string(name_id), "") |> String.to_atom module = form.struct_module.__schema__(:association, name).related opts = opts |> parse_opts(module) |> put_choices(module) Field.create_field(:select, name_id, opts) end defp create_field_multiple(form, name, opts) do module = form.struct_module.__schema__(:association, name).related opts = opts |> parse_opts(module) |> put_choices(module) selected = if form.struct.id do form.struct |> @repo.preload(name) |> Map.get(name) |> Enum.map(&(&1.id)) else [] end phoenix_opts = Keyword.merge(opts[:phoenix_opts], [ selected: selected ]) opts = Keyword.merge(opts, phoenix_opts: phoenix_opts) Field.create_field(:multiple_select, name, opts) end defp put_choices(opts, module) do if !opts[:without_choices] do choices = module |> apply_query(opts[:query]) |> apply_group_by_assoc(opts[:group_by]) |> @repo.all |> group_rows(opts[:group_by]) |> generate_choices(opts[:choice_label]) Keyword.put(opts, :choices, choices) else Keyword.put(opts, :choice_label_provider, fn id -> row = from(e in module, where: e.id == ^id) |> apply_query(opts[:query]) |> apply_group_by_assoc(opts[:group_by]) |> @repo.one if row do get_choice_label_val(row, opts[:choice_label]) else nil end end) end end defp parse_opts(opts, module) do opts |> Keyword.update(:group_by, nil, fn(property_path) -> cond do is_list(property_path) -> property_path is_atom(property_path) -> cond do module.__schema__(:association, property_path) -> [property_path, :name] true -> [property_path] end true -> nil end end) end defp apply_query(query, custom_query) when is_function(custom_query) do custom_query.(query) end defp apply_query(query, _) do query end defp apply_group_by_assoc(query, [assoc|t]) do if Enum.count(t) > 0 do from(query, preload: [^assoc]) else query end end defp apply_group_by_assoc(query, _) do query end defp group_rows(rows, property_path) when is_list(property_path) do rows |> Enum.group_by(&(Formex.Utils.Map.get_property(&1, property_path))) end defp group_rows(rows, _) do rows end defp generate_choices(rows, choice_label) when is_list(rows) do rows |> Enum.map(fn row -> label = get_choice_label_val(row, choice_label) {label, row.id} end) |> Enum.sort(fn {name1, _}, {name2, _} -> name1 < name2 end) end defp generate_choices(grouped_rows, choice_label) when is_map(grouped_rows) do grouped_rows |> Enum.map(fn {group_label, rows} -> {group_label, generate_choices(rows, choice_label)} end) |> Map.new(&(&1)) end defp get_choice_label_val(row, choice_label) do cond do is_function(choice_label) -> choice_label.(row) !is_nil(choice_label) -> Map.get(row, choice_label) true -> if Map.has_key? row, :name do row.name else throw "Key :name not found. You should use the :choice_label option of SelectAssoc" end end end end