Associating Categories to a Business [Context documentation]

I’m reading the Context documentation trying to delve deeper into this subject.

Here it describes how to associate Categories to a Product (in my case, to a Business). I already have the basic struct using the LiveView and the doc is slightly different, as it is describing the process as the Controller structure.

Everything running good until this topic, where it asks to write the category_opts function inside ProductHTML view in lib/hello_web/controllers/product_html.ex file. But I don’t have this file (as I’m using the LiveView).

Here is the function provided by the documentation and I’m writing it on my form_component.ex:

def category_opts(changeset) do
    existing_ids =
      changeset
      |> Ecto.Changeset.get_change(:categories, [])
      |> Enum.map(& &1.data.id)

    for cat <- Balaio.Catalog.list_categories(),
        do: [key: cat.title, value: cat.id, selected: cat.id in existing_ids]
  end

And the input field to present the data on the form (notice the changeset param):

<.input
          field={@form[:category_ids]}
          type="select"
          multiple={true}
          options={category_opts(@changeset)}
        />

It returns this error message:

key :changeset not found in: %{id: :new, socket: #Phoenix.LiveView.Socket

I’m trying to understand how to switch from the Controller described on doc to use this structure on the LiveView code I already have.

My form_component.ex are described here.

The @ in @changeset is trying to read assigns.changeset bu you never assigned your changeset, just the form here. But that’s ok—you don’t need to since the changeset is still accessible inside your @form assign via the source key.

Try this:

  options={category_opts(@form.source)}
1 Like

Thanks for your response.

The @changeset is the instruction provided by the documentation. What I’m trying is to translate these instructions to the LiveView way.

As I already have this whole form_componet, I’m trying to change the category_opts function (provided by the doc) to use my form assign.

It is a way to use parts of both solutions (my form_component and the instructions provided by doc).

Eventually, I’ll open a PR suggesting to improve the Context doc.

If you want it to use your form assign with category_opts (which I agree would be better) just destructure source out of it. You can even rename it to changeset in the process so it’s clearer:

def category_opts(%{source: changeset} = _form) do

I was trying something like this. Not sure it’s the correct way.

def category_opts(socket) do
    form_changeset = Map.get(socket.assign_form, :form, %Ecto.Changeset{})

    existing_ids =
      form_changeset
      |> Ecto.Changeset.get_change(:category_ids, [])
      |> Enum.map(& &1.data.id)

    for cat <- Balaio.Catalog.list_categories(),
        do: %{key: cat.title, value: cat.id, selected: cat.id in existing_ids}
  end

That’s what I was saying, though: to pass @form to category_opts do what I showed you. The whole thing will look like this:

def category_opts(%{source: changeset} = _form) do
  existing_ids =
    changeset
    |> Ecto.Changeset.get_change(:categories, [])
    |> Enum.map(& &1.data.id)

  for cat <- Balaio.Catalog.list_categories(),
    do: [key: cat.title, value: cat.id, selected: cat.id in existing_ids]
end

then you can call:

    options={category_opts(@form)}

If you need to guard for @form being nil, you can add an additional function head for category_opts above the current one:

def category_opts(nil), do: category_opts(%{source: %Ecto.Changeset{}})

Though there is probably nicer way to do that :thinking:

1 Like

Thanks for it. Helped a lot :blush:

1 Like