Fixing Form Handling with STI

I tried a quick experiment based on http://codeloveandboards.com/blog/2017/10/03/migrating-activerecord-sti-to-ecto/ where depending on the type selected, a particular changeset is used and it’s possible to control the validation of a map field for each scenario. And for the most part it works the way I wanted it to. Except:

  • On insert, empty keys are created for the other embedded schemas that weren’t selected.
  • On edit, Phoenix forms aren’t rendered with the values present in the DB.

Aside from the debate over whether STI is a good approach, am I missing something in my form definitions that’s causing these problems?

Thanks.

def changeset(%Poll{} = [poll], attrs) do
  endpoint
  |> validate_required(:poll_type, :poll_attrs)
  |> validate_data
end

defp validate_data(changeset) do
  changeset
  |> build_poll_changeset
  |> case do
       %{valid?: true}   -> changeset
       %{errors: errors} -> agg_poll_errors(changeset, errors)
     end
end

defp build_poll_changeset(changeset) do
  poll_type  = get_field(changeset, :poll_type)
  poll_attrs = get_field(changeset, :poll_attrs)

  cond do
    poll_type == Poll.poll_type_event()  -> Poll.Event.changeset(poll_attrs)
    poll_type == Poll.poll_type_raffle() -> Poll.Raffle.changeset(poll_attrs)
    true -> changeset
  end
end

defp agg_poll_errors(changeset, errors) do
  Enum.reduce(errors, changeset, fn {key, {message, meta}},
    acc -> add_error(acc, key, message, meta)
  end)
end

And the forms look like:

<div id="poll-wrapper">
  <%= label f, :poll_type_event, "Event", class: "poll-tab" %>
  <%= radio_button f, :poll_type, "event", checked: true %>
  <%= error_tag f, :poll_type_event %>

  <%= label f, :poll_type_raffle, "Raffle", class: "poll-tab" %>
  <%= radio_button f, :poll_type, "raffle" %>
  <%= error_tag f, :poll_type_raffle %>

  <div id="poll_type_event_options" class="tab">
    <%= label f, :poll_attrs_event_ename, "Name" %>
    <%= text_input f, :poll_attrs_event_ename, name: "poll[poll_attrs][ename]" %>
    <%= error_tag f, :poll_attrs_event_ename %>
    <%= label f, :poll_attrs_event_edesc, "Description" %>
    <%= text_input f, :poll_attrs_event_edesc, name: "poll[poll_attrs][edesc]" %>
    <%= error_tag f, :poll_attrs_event_edesc %>
  </div>

  <div id="poll_type_raffle_options" class="tab">
    <%= label f, :poll_attrs_raffle_rname, "Name" %>
    <%= text_input f, :poll_attrs_raffle_rname, name: "poll[poll_attrs][rname]" %>
    <%= error_tag f, :poll_attrs_raffle_rname %>
    <%= label f, :poll_attrs_raffle_rdesc, "Description" %>
    <%= text_input f, :poll_attrs_raffle_rdesc, name: "poll[poll_attrs][rdesc]" %>
    <%= error_tag f, :poll_attrs_raffle_rdesc %>
  </div>
</div>