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 ) :
[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.