Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Phoenix Form Field For Array Model Data Type

I'm using the amazing Phoenix Web Framework and trying to figure out how to create the form field(s) for a model with an array field.

Here is an example field from the model: field :grateful, {:array, :string}

I've tried generating the fields like this:

<%= inputs_for f, :grateful, fn fp -> %>
   <%= text_input fp, :grateful %>
<% end %>

But I get this error: could not generate inputs for :grateful from Motivation.DailyPost. Check the field exists and it is one of embeds_one, embeds_many, has_one, has_many, belongs_to or many_to_many

If I generate the field like this: <%= text_input fp, :grateful %> it generates a form field with a name of: daily_post[grateful] which actually won't work. I would need daily_post[grateful][].

The code below works, but loading the data after saving does not work. All array values are merged into one input field.

  <div class="form-group" id="grateful-group">
    <%= label f, :grateful, class: "control-label" %>
      <%= text_input f, :grateful, name: "daily_post[grateful][]" %>
    <%= error_tag f, :grateful %>
    <input type="button" class="btn btn-success" id="add-grateful" value="add" />
    <script>
      window.onload = () => {
        $('#add-grateful').click((e) => {
          $('<input type="text" name="daily_post[grateful][]" />').appendTo("#grateful-group");
        })
      }
    </script>
  </div>

How can I properly work with the array datatype in phoenix?

Thanks!

like image 372
Chip Dean Avatar asked Dec 31 '16 01:12

Chip Dean


1 Answers

Ok I think I got it now. This is what I did:

defmodule Motivation.InputHelpers do
  use Phoenix.HTML

  def array_input(form, field, attr \\ []) do
    values = Phoenix.HTML.Form.input_value(form, field) || [""]
    id = Phoenix.HTML.Form.input_id(form,field)
    content_tag :ul, id: container_id(id), data: [index: Enum.count(values) ] do
      values
      |> Enum.with_index()
      |> Enum.map(fn {k, v} ->
        form_elements(form, field, k, v)
      end)
    end
  end

  def array_add_button(form, field) do
    id = Phoenix.HTML.Form.input_id(form,field)
    # {:safe, content}
    content = form_elements(form,field,"","__name__")
      |> safe_to_string
      # |> html_escape
    data = [
      prototype: content,
      container: container_id(id)
    ];
    link("Add", to: "#",data: data, class: "add-form-field")
  end

  defp form_elements(form, field, k ,v) do
    type = Phoenix.HTML.Form.input_type(form, field)
    id = Phoenix.HTML.Form.input_id(form,field)
    new_id = id <> "_#{v}"
    input_opts = [
      name: new_field_name(form,field),
      value: k,
      id: new_id
    ]
    content_tag :li do
      [
        apply(Phoenix.HTML.Form, type, [form, field, input_opts]),
        link("Remove", to: "#", data: [id: new_id], class: "remove-form-field")
      ]
    end
  end

  defp container_id(id), do: id <> "_container"

  defp new_field_name(form, field) do
    Phoenix.HTML.Form.input_name(form, field) <> "[]"
  end

end

I import that file into my web.ex, then I can print array fields like this:

<%= array_input f, :grateful %>

I can also print an add button for the field like this:

<%= array_add_button f, :grateful %>

To facilitate adding & removing fields I wrote this javascript:

window.onload = () => {
  const removeElement = ({target}) => {
    let el = document.getElementById(target.dataset.id);
    let li = el.parentNode;
    li.parentNode.removeChild(li);
  }
  Array.from(document.querySelectorAll(".remove-form-field"))
    .forEach(el => {
      el.onclick = (e) => {
        removeElement(e);
      }
    });
  Array.from(document.querySelectorAll(".add-form-field"))
    .forEach(el => {
      el.onclick = ({target}) => {
        let container = document.getElementById(target.dataset.container);
        let index = container.dataset.index;
        let newRow = target.dataset.prototype;
        container.insertAdjacentHTML('beforeend', newRow.replace(/__name__/g, index));
        container.dataset.index = parseInt(container.dataset.index) + 1;
        container.querySelectorAll('a.remove-form-field').forEach(el => {
          el.onclick = (e) => {
            removeElement(e);
          }
        })
      }
    });
}
like image 85
Chip Dean Avatar answered Sep 17 '22 18:09

Chip Dean