Where should I put extra form data/meta data for a LV form?

Wondering if anyone has a good suggestion on how to store ‘extra meta data’ for a form?

I think I can best describe my conundrum with an example.

To start with, I have a Config schema with an embeds_many for config_fields.

defmodule MyApp.Config do
  use MyAppWeb, :schema
  
  defmodule ConfigField do
    use Ecto.Schema

    embedded_schema do
      field(:label, :string)
      field(:value, :string)
    end

    def changeset(config_field, attrs \\ %{}) do
      config_field
      |> cast(attrs, [
        :label,
        :value
      ])
    end
  end
  
  schema "configs" do
    field(:label, :string)

    embeds_many(:config_fields, ConfigField, on_replace: :delete)

    timestamps()
  end
  
  def changeset(config, attrs \\ %{}) do
    config
    |> cast(attrs, [
      :label,
    ])
    |> cast_embed(:config_fields)
  end
end

In my LiveView form, I have an inputs_for for the config_fields. My value is determined from a select drop down, but my options are specific to each config_field. They also aren’t fixed.

<.inputs_for :let={cf} field={@form[:config_fields]}>
  <.input field={cf[:value]} type="select" options={@UNSURE_WHERE_TO_GET_FROM} />
</.inputs_for>

The options are from another schema, where a user can change these options as they please.

defmodule MyApp.TemplateConfigFields do
  use MyAppWeb, :schema
  
  schema "template_config_fields" do
    field(:label, :string)
    field(:options, {:array, :string})

    timestamps()
  end
  
  def changeset(template_config_field, attrs \\ %{}) do
    template_config_field
    |> cast(attrs, [
      :label,
      :options
    ])
    |> unique_constraint(:label)
  end
  
  def get_all() do
    query = from(tco in __MODULE__)
    MyRepo.all(query)
  end
end

In the handle_params of my LiveView, the form is created like the following

changeset = Config.changeset(%Config{})
socket = assign(socket, form: to_form(changeset))

And then I add the config fields based off the template config fields.

config_fields = TemplateConfigFields.get_all()
update(socket, :form, fn %{source: changeset} ->
  changeset = Ecto.Changeset.put_embed(changeset, :config_fields, config_fields)
  to_form(changeset)
end)

I have 3 paths that I’ve explored for displaying the relevant options.

  1. Using cf.source.data.options
    Because the Config is an empty struct, after the first ‘validate’ the form with the put_embed of ConfigFields structs are lost. This means the ConfigField becomes an empty struct with the label and value being reflect as a ‘changes’ in the form.
### BEFORE
# cf
%Phoenix.HTML.Form{
  source: #Ecto.Changeset<action: nil, changes: %{}, errors: [],
   data: #MyApp.Config.ConfigField<>, valid?: true>,
  data: %MyApp.Config.ConfigField{
    id: nil,
    label: "select menu",
    value: nil,
    options: ["asd", "asdsa", "sadsad"]
  },
  action: nil,
  params: %{"_persistent_id" => "3"},
}

# cf.source.data.options
["asd", "asdsa", "sadsad"]

After validate is called on the form

# cf
%Phoenix.HTML.Form{
  source: #Ecto.Changeset<
    action: :insert,
    changes: %{label: "select menu", value: "asd"},
    errors: [],
    data: #MyApp.Config.ConfigField<>,
    valid?: true
  >,
  data: %MyApp.Config.ConfigField{
    id: nil,
    label: nil,
    value: nil,
    options: nil
  },
  action: :validate,
  params: %{
    "_persistent_id" => "3",
    "label" => "select menu",
    "value" => "asd"
  },
}
# cf.source.data.options
nil
  1. Persisting the options as a hidden value
    Using input_value(cf, :options) won’t work because the ‘options’ are converted into a string without any separators.
embedded_schema do
  field(:label, :string)
  field(:value, :string)
  field(:options, {:array, :string}, virtual: true)
end
  1. Using a seperate variable in the assigns
    I’ve considered using a map with the key being ‘name’ of the config_field and teh value being the options, options={@config_field_options[cf[:name].value}], but that feels a bit messy?

For me I feel option 3 is the most logical solution.
Only 1 value from the TemplateConfigFields will be stored, so why would you drag all those values along in your form?

Load the values from the db as a map with the field name as key and the list of values as the value, assign it to the socket and use a function to retrieve them.

defp get_options(config_field_options, %{value: field_name}) do
  Map.get(config_field_options, field_name)
end

and call it as get_options(@config_field_options, cf[:name])

1 Like

I’ve suspected so and that is my current ‘solution’.

But that’s a good shout using a defp to get the options as its more explicit to what shenanigan I’m up to!

Cheers!

1 Like