How would you design this dynamic form.

Hey all,
I am having some trouble understanding how change sets with nested data and forms work together.

How would I make the following example work?

I have 4 schemas:

  1. Template: has a content and name fields that are text.
  2. RequiredVariable: has “label” field and variable_name field, and a template_id. (Many to 1 with template)
  3. TemplateInstance: has a template_id field
  4. VariableValue: has a template_instance_id, a required_variable_id, and “value” fields.

What I want to happen is that when a user is making a new TemplateInstance, they pick a Template name from a drop down, then a form field for each RequiredVariable appears so they can create VariableValue’s attached to the TemplateInstance.

Below is what I have so far. Basicly, I try and dynamicly add the variable values to the changeset for the TemplateInstance. But I’m not sure how the incoming parameters from the form are meant to map back to the changeset.
The way I have it set up right now, whenever a Variable value gets set it wipes away the other ones :confused: .
I assume I am not mapping things properly in the changeset but I don’t know what I am missing.

Any help would be great. Thanks!

  def render(assigns) do
    ~H"""
    <button phx-click={CoreComponents.show_modal("add_instance")}> <.icon_plus></.icon_plus> </button>
    <.modal id="add_instance">
      <div class="content">
        <.simple_form :let={f} for={@new_instance} phx-change="validate">
          <.error :if={@new_instance.action}>
            Oops, something went wrong! Please check the errors below.
          </.error>
          <.input field={f[:template_id]} type="select" label="Template" options={@templates} />

          <.inputs_for :let={f_var} field={f[:variables]}>
            <div class="flex gap-x-5">
              <.input field={f_var[:value]}
                      type="text"
                      label={Map.get(@variables, f_var[:var_id].value).label} />
            </div>

          </.inputs_for>

          <:actions>
            <.button>Save</.button>
          </:actions>
        </.simple_form>
      </div>
    </.modal>
    """
  end

  def mount(_params, session, socket) do
    template_options =
      [
        {"Choose Template...", ""}
        | Fawkes.Templates.active!()
          |> Enum.map(fn q -> {q.name, q.id} end)
      ]

    {new_instance, vars} = new_instance(%{}, Map.get(session, "project_id"))

    socket =
      socket
      |> assign(:project_id, Map.get(session, "project_id"))
      |> assign(:new_instance, new_instance)
      |> assign(:vars, vars)
      |> assign(:templates, template_options)

    {:ok, socket, layout: false}
  end

  def handle_event("validate", params, socket) do
    {changeset, vars} = new_session(params, socket.assigns.project_id)
    socket = assign(socket, :new_instance, changeset)
    {:noreply, assign(socket, :vars, vars)}
  end

  defp new_instance(params, project_id) do
    new_params =
      Map.get(params, "session", %{})
      |> Map.put("project_id",project_id)

    TemplateInstance.changeset(%TemplateInstance{}, new_params)
    |> fill_vars(new_params)
  end

  def fill_vars(changeset, params) do
    instance_id = Ecto.Changeset.get_field(changeset, :instance_id)
    if is_nil(instance_id) or instance_id == "" do
      {changeset, %{}}
    else
      query = from q in TemplateInstance, preload: :variables
      instance = Repo.get(query, instance_id)

      if is_nil(instance) do
        {Ecto.Changeset.add_error(changeset, :instance_id, "Doesn't exist"), %{}}
      else
        context_changes =
          instance.required_vars
          |> Enum.map(fn var ->
            VariableValue.changeset(%VariableValue{}, %{"var_id" => var.id})
          end)
        changeset = Ecto.Changeset.put_assoc(changeset, :vars, vars)

        keys =
          instance.vars
          |> Map.new(fn var -> {var.id, var} end)

        {changeset, keys}
      end
    end
  end
1 Like

Hello and welcome!

This article should help you: One-to-Many LiveView Form | Benjamin Milde

If you’re still having trouble I can take a better look later.

As a rule of thumb (and I say this a lot) manipulating params is almost never a good idea. The whole point of the cast constructor is to map params (untrusted data) into a changeset. Then you have a known shape with all the fields typed properly to work with and life is much easier.

2 Likes