Edit single field of multiple instances at once

Hi!

I’m trying to design a form that allows updating many items at once (just a dozen, nothing more). The items are backed by an ecto schema and have multiple fields. This form only allows one field to be edited though. Although it’s not really relevant here, just imagine those items being shown in a table with each item filling one row, and only one column is editable in bulk.

As far as I understand, I could create a schemaless changeset with a field of type {:map, :string} to associate the primary key of each item with the new value for this particular field (here I’ve chosen :string, but any type would work, I guess). Am I on the right track here? Or should I try to use the existing schema of the items I’m editing somehow?

I have no clue how to use such a schemaless changeset though. I would like to use the form helpers (Phoenix.HTML.Form — Phoenix.HTML v3.2.0), but I don’t know which field I would have to pass to, for example text_input/3. I understand that I will have to validate the changeset after submitting, and transform the data in the changeset to an appropriate bulk update somehow, which is totally expected of course.

It would be great if there was a changeset-based solution, in combination with the html form helpers to this problem, so validation and error messsages would work as expected.

Any ideas are appreciated! Thanks for reading so far…

Interesting use case. Is your intention to prompt the user to update all fields and then click one submit button for everything?

If you wrap each field in its own form you could handle each change event individually and provide feedback with the standard changeset validation for each item. Then the submit button could sit outside the forms and with some light javascript trigger each form’s submit event. Those events will accumulate in the live view message queue and you can either, accumulate the change events on the server until all are accounted for then bulk update, or handle them one at a time as they arrive then feed back any persistence errors on each change back to the user.

Depends on whether you want all changes to fail if one does, or let some through while feeding back specific errors for further changes.

1 Like

One button to update all values for one column would be ideal.

I’ve never considered multiple forms. Thanks for that alternative thought.

In my mind there would be a solution where all values can be submitted together, with one form. I can even imagine a usecase where all column values should be validated together, where i would like to show the overarching error at the top or bottom of the form.

I was hoping that the :map or :array type would allow such a dynamic form.

I think in this case you’d have to hand-craft the various inputs so you can send all values together in a map, with each object’s id as a key like in this simplified example.

<%= for model <- [%{id: 1, field: "first"}, %{id: 2, field: "second"}] do %>
  <input name={"field[#{model.id}]"} value={model.field} type="text">
<% end %>

What that’s doing is creating inputs whose names are name=field[1] and name=field[2]

When you change the second element’s text, you’ll receive a phx-change="validate" event with the “_target” param pointing to the changed item, and the full map of fields.

IO.inspect "validate" params: %{
  "_csrf_token" => "...",
  "_target" => ["field", "2"],
  "field" => %{"1" => "first", "2" => "second changed"}
}

And when submitting the form, all the changed fields:

IO.inspect save params: %{
  "_csrf_token" => "...",
  "field" => %{"1" => "first", "2" => "second changed"}
}

There may be a more idiomatic “Ecto Changeset” way of doing it with for example inputs_for, but I sometimes find it hard bending my mind around the ways changesets interact with phoenix_html form functions and prefer the simpler HTML approach.

1 Like

You can even make the map more complex as needed: name={"people[#{model.id}][field]"} to make it easier to apply changes to each changeset.

IO.inspect validate params: %{
  "_csrf_token" => "...",
  "_target" => ["people", "2", "field"],
  "people" => %{
    "1" => %{"field" => "first"},
    "2" => %{"field" => "second changed"}
  }
>

IO.inspect save params: %{
  "_csrf_token" => "...",
  "people" => %{
    "1" => %{"field" => "first"},
    "2" => %{"field" => "second changed"}
  }
}