Nested inputs and embedded schemas with LiveView

Let’s say I have a Survey that I give to people. It consists of a list of questions, and those that they want to answer they check, and upon checking, a select input appears for them to input their answer of “yes”, “no”, or “maybe”. The questions will be sourced from elsewhere, for now we can assume they are simply defined in code. I only want to store the {name, answer} pair for those they explicitly check.

So for example:
[ ] Do you like LiveView?

Once checked, and a selection made (imagine it is a dropdown :slight_smile: ) :
[X] Do you like LiveView? [:yes, :no, :maybe]

My initial thought is that the best way to accomplish this is by prepopulating all of the embedded questions into the changeset, and then make sure I don’t insert the ones I don’t want to insert via using “action: :ignore”. To do that I use the following schemas:

defmodule SurveyThing.Surveys.Survey do
  schema "surveys" do
    field :name, :string

    embeds_many :questions, Question do
      field :name, :string
      field :answer, Ecto.Enum, values: [:yes, :no, :maybe]
      field :selected, :boolean, virtual: true
    end

  end

  def changeset(survey, attrs) do
    survey |> cast(attrs, [:name]) |> cast_embed(:questions, question_changeset/2)
  end

  def question_changeset(question, attrs) do
    question |> cast(attrs, [:name, :answer, :selected]) |> ignore_if_not_selected
  end

  defp ignore_if_not_selected(%Ecto.Changeset{changes: %{selected: false}} = c), do: %{c | action: :ignore}
  defp ignore_if_not_selected(c), do: c
end

And in my LiveView, you can imagine me on mount/3 doing something like this to populate the changeset:

%Surveey{}
|> SurveyThings.Surveys.change_survey()
|> Ecto.Changeset.put_embed(:questions, [%{name: "Do you like LiveView?", selected: false}]

And in the template, something nested in the form like:

  <%= for qf <- inputs_for(f, :questions) do %>
    <%= checkbox qf, :selected %>
    <!-- could be better, but you get the drift -->
    <%= text_input qf, :name, disabled: true %>
    <%= select qf, :answer, [:yes, :no, :maybe] %>
  <% end %>

With the usual validated/submit hooks that are generated by default. However, if I were, for example, to modify the Survey.name field, that would trigger the validate hook, that would then validate what is currently there. The question is not currently selected, so that nested changeset gets action: :ignore applied. THEN it is removed from my changeset entirely, and the question will no longer render!

I was hoping that the :action would only be considered in the Repo operation, but it looks like it is considered in the various form helpers considered in cast_embed/2. Specifically, after cast_embed/2 is called which sets action: ignored on the nested question, the question is no longer in the changeset.

Any idea? I’ve been messing with this for an hour or two now and can’t get anything to work. About to give up and just ALWAYS persist all questions in the database, even the unanswered ones, as I can’t get the :ignore action to work as I want.

One alternative I have thought of is generating all of these nested inputs in a static manner instead of via the changeset, but there doesn’t seem to be any existing HTML form functions to assist with that and so I hesitate to write all of this functionality from scratch as it seems like I’m missing something here. Annoyingly, inputs_for does not support default: for its LiveView variant, anyway.

What I ended up doing was to remove the ignore_if_not_selected code above and instead just manually modify the changeset when finally doing the insert via filtering and re-putting the embed via put_embed in my main “insert survey” function.

The only thing I don’t like about this is that if the insertion fails that modified/filtered changeset can flow back to the UI and I’m back at square one - but that should be pretty rare, I hope.