How to show copied associations in a multi-selection list in LiveView from another table?

Greetings Everyone!!!

This is my first post ever in this forum. I have been imperatively programming for nearly 35 years, and I just have a couple of months into Web Development with Elixir/Phoenix, so I am having a very hard time trying to accomplish a very simple task. I hope you could guide me on how to do this.

I have…

…a Position that has_many Activities
…an Employee that has_one Position and has_many Activities

Just to be clear, lets say that I have 3 employees in the Storage Area. All of them have the same basic Storage Area Activities, but one of them helps the Invoice Area, so he will have an extra Activity.

From the Employee → Position → Activities path it looks like this:

E1 → P1 → SA1, SA2, SA3 # Storage Activity tasks.
E2 → P1 → SA1, SA2, SA3
E3 → P1 → SA1, SA2, SA3

When I update an Employee’s info I want to copy the records from the Position->Activities relation into the Employee->Activities relation, so the basic activities could be copied when assigning the position but could be changed after that, and the Employee->Activities relation would look like this:

E1 → SA1, SA2, SA3, IA1 # Basic Storage Activity tasks plus Invoicing Task
E2 → SA1, SA2, SA3
E3 → SA1, SA2, SA3

I successfully managed to finish the CRUD forms for the Activities, Positions and most of the Employee.

When the phx-change=“validate” event is triggered in the New Employee form, I can read the new Position value when that list box is changed and get the new activities list, but I don’t know how to update the activities list box in the employee form. I have tried a lot of ways, but all of them wrong.

So, I will be more than happy if you could show me the right route on how could I successfully accomplish this task. Any help will be more than welcome.

1 Like

Hi,

Your question might need improving to get a better answer.

Perhaps try including relevant code snippets - e.g your current form and handler function where you are doing the “saving” e.t.c Enough for someone to see what you want to do and where you are falling short.

From what I have read, and assuming your ecto relationships are properly defined has_many e.t.c - it sounds like you might be missing casting the activities association to the employee record you are updating.

See cast_assoc/3 in Ecto.Changeset — Ecto v3.12.4, but note this is at best a guess on my part, the question isn’t clear enough.

I am so sorry for wasting your time. I just realized that I am mixing technologies.

I used code from Contexts — Phoenix v1.7.14, specifically the function change_product/2, the part where references “category_ids”. I just realized that code is using Controllers, not LiveView. So, I will dive into it with my new point of view. Hopefully I will have it working soon.

Thank you very much for your kindness answering my post.

I am so sorry for wasting your time. I just realized that I am mixing technologies.

Oh don’t be sorry, we are all here to learn :wink:

And of course don’t hesitate if/when you next you get stuck.

1 Like

Greetings Everyone!!!

I have not been able to complete this task, so, I will show the code that is important for the task. Hopefully, you could help me out with the missing part.

When calling the form for new or edit employee, I get the Employee record or an Empty Employee record:

# .../employee_live/index.ex

   defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    |> assign(:page_title, "Edit Employee")
    |> assign(:employee, Context.get_employee!(id))
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Employee")
    |> assign(:employee, %Employee{})
  end

# .../myapp/context.ex
  def get_employee!(id) do
    Employee
    |> Repo.get!(id)
    |> Repo.preload([:positions, :activities])
  end

Then, about to render the form, the record is converted into a Form in the update function and I use a couple of functions to show the relationships Employee->Positions and Employee->Activities.

# .../lib/myapp/context.ex <- Called ahead
  def change_employee(%Employee{} = employee, attrs \\ %{}) do
    activities = list_activities_by_id(attrs["activity_ids"])

    employee
    |> Repo.preload([:positions, :activities])
    |> Employee.changeset(attrs)
    |> Ecto.Changeset.put_assoc(:activities, activities)
  end

  def list_activities_by_id(nil) do
    []
  end

  def list_activities_by_id(activity_ids) do
    Repo.all(from a in Activity, where: a.id in ^activity_ids)
  end

Then the app renders the form:

# .../employee_live/form_component.ex
  @impl true
  def update(%{employee: employee} = assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_new(:form, fn ->
       to_form(Context.change_employee(employee))
     end)}
  end
  @impl true
  def render(assigns) do
    ~H"""
    <div>
      ...
      <.simple_form
        for={@form}
        id="employee-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:emp_first_name]} type="text" label="First Name" phx-debounce="blur"/>
         ...
        <!-- :ERS: Here I am sending the @employee.emp_position from the socket -->
        <.input field={@form[:emp_position]} type="select" label="Position" options={MyApp.Context.position_opts(@employee.emp_position)} prompt="Choose an option:" phx-change="validate_position"/>
        <.input field={@form[:activity_ids]} type="select" label="Activities" multiple={true} options={MyApp.Context.activity_opts(@form.data)} phx-debounce="blur"/>
    ...
    </div>
    """
  end

I use this functions to create the list objects for the employee’s position and activities:

# .../lib/myapp/context.ex <--- Called above
  def position_opts(position_id) do
    for pst <- list_positions() do
      [
        key: pst.pos_descrip,
        value: pst.id,
        selected: pst.id == position_id
      ]
    end
  end

  def activity_opts(data) do
    activity_ids = Enum.map(data.activities, & &1.id )

    for act <- list_activities() do
      [
        key: act.act_descrip,
        value: act.id,
        selected: act.id in activity_ids
      ]
    end
  end

Up to this moment, everything works great: The new record has no activities selected, and the edited record already has its activities selected, but … whenever the Position field is changed I call the validate_position event to set/replace the new activities, where I get the list of the activities for the new position:

# .../lib/myapp/context.ex <--- Called ahead
  def get_position!(id) do    Position
    |> Repo.get!(id)
    |> Repo.preload(:activities)
  end

In here is where I have been running around for a couple of weeks now. I get the list of the new activities, but I don’t know how to have the app to “select” the new activities

  @impl true
  def handle_event("validate_position", %{"employee" => employee_params}, socket) do
    %{"emp_position" => new_position_id} = employee_params
    new_position_id = String.to_integer("0" <> new_position_id)
    new_position = Context.get_position!( new_position_id )

    changeset = 
      Context.change_employee(socket.assigns.employee, employee_params)
      |> Ecto.Changeset.put_assoc(:positions, new_position)
      |> Ecto.Changeset.cast_assoc(:activities, new_position.activities)

    # ├──┤ Totally lost in here ├──────────────────────────────────────────────┤

    {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
  end

I can set the the data into the changeset, but the app is either using the info from the socket (look for tag :ERS:) or from the Form. I should not be using the info in the Socket since it is the Original information, not the one being changed. Under different circumstances, I should be looking for the value in the changeset, the one being updated, but this form is not using the changeset.

I really hope you could help me to have the render function to see the new activities for the employee once the position is changed.

Any help will be more than welcome.

Greetings Everyone!!!

I did it!!!

It seems like I was going way too far trying to add the records for the associations.

At the end, this is what I did.

In the update event, I set a couple of flags to let the form know if it needs to update the activities list.

  def update(%{employee: employee} = assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_new(:form, fn ->
       to_form(Context.change_employee(employee))
     end)
     |> assign(act_change: true, act_ids: [])    # <<<==== Here are the flags
    }
  end

Then in the render event, the activity_ids parameter is set to the list of Activity IDs if needed.

  @impl true
  def render(assigns) do
    assigns =
      if assigns.act_change do      # <<<=== Flag to update
        if assigns.act_ids == [] do # <<<=== First time we render the page
          if Ecto.assoc_loaded?(assigns.employee.activities) do
            put_in(
              assigns.form.params["activity_ids"],
              Context.list_activities_ids(assigns.employee.activities)
              ####### The line above returns: ["1", "3", "7"] ########
            )
          else # <<<=== Asociation is not loaded, nothing to do.
            assigns
          end
        else # <<< === We have a list of IDs, means the Position has been changed
          put_in(
            assigns.form.params["activity_ids"],
            assigns.act_ids
          )
        end
      else # <<<=== Flag to update was not set
        assigns
      end
    ~H"""
    <div>
      ...
      <.simple_form
        for={@form}
        id="employee-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
    ...
        <.input field={@form[:activity_ids]} type="select" label="Activities"
          multiple={true} options={MyApp.Context.activity_opts()}
          phx-debounce="blur"
        />
    ...
      </.simple_form>
     """
   end

When showing the Activities List I left the selected attribute out, and let Phoenix do the rest.

  def activity_opts() do
    for act <- list_activities() do
      [
        key: act.act_descrip,
        value: act.id
        #selected: boolean condition not important any more
      ]
    end
  end

And when triggering the form’s validating event, only send the Activities IDs when the Position is changed:

  @impl true
  def handle_event("validate", params, socket) do
    %{ "_target" => ["employee", field_name] } = params
    %{"employee" => employee_params} = params

    {act_change, act_ids} =
      case field_name do
        "emp_position" -> # <<<=== The Position field has been changed
          %{ "emp_position" => new_pos_id } = employee_params
          new_pos_id = String.to_integer("0" <> new_pos_id)
          new_position = Context.get_position(new_pos_id)
          if new_position == nil do # <= The Prompt or an Invalid Position was selected
            {true, []}
          else # <<<=== We have a valid Position record, send its activities
            {true, Context.list_activities_ids(new_position.activities)}
          end
        _ -> # <<<=== Another field was updated
          {false, []}
      end
    changeset = Context.change_employee(socket.assigns.employee, employee_params)
    {:noreply, assign(socket, form: to_form(changeset, action: :validate),
        act_change: act_change, act_ids: act_ids) }
  end

After 3 week it is done.

For now, it looks beautiful because it IS WORKING!!! Whenever I select another Position, the Employee’s Activities list is updated.

With in a couple of years I will realize how awful this solutions is, but as of now, this solution looks gorgeous.

Best regards and happy hunting!!!

Slightly related; you might be interested in this checkgroup component Code snippets for Fly blog posts - https://fly.io/phoenix-files/making-a-checkboxgroup-input/ · GitHub from Making a CheckboxGroup Input · The Phoenix Files.