Updating a value for a changeset in handle_event

Handle_event “validate” works fine with my changeset.

I’m having issues when I want to update a value outside of this event. Say a Google map market lat lng event I capture those two values and it works fine but I lose all of the other existing values in the form change set

So what’s the solution here do I need to have some kind of socket assign to keep track of the total form state outside of the actual change set and use that assign to render the input values?

1 Like

Check out:

Hope it helps

This doesn’t really help. What are you implying? That I need to keep track of the total form in the socket assigns?

I’m back home so here’s some example code:

<.form
  let={f}
  for={@listing_changeset}
  id="create-listing"
  phx-change="validate"
  phx-submit="create-listing"
>
  <div class="grid items-start grid-cols-3 mb-6">
    <.form_label form={f} field={:title} class="mb-0 py-[0.563rem] leading-6" />
    <div class="col-span-2">
      <.form_text_input form={f} field={:title} autocomplete="off" />
      <.form_feedback form={f} field={:title} />
    </div>
  </div>

  <div id="map" phx-hook="MainMap" phx-update="ignore" class="w-full h-96 rounded-lg mb-12"></div>

My hook:

Hooks.MainMap = {
  mounted() {
    const pushEventToComponent = (event, payload) => {
      this.pushEventTo(this.el, event, payload);
    };

    const santaCruz = { lat: -17.784, lng: -63.177 };
    // The map, centered at santaCruz
    const map = new google.maps.Map(document.getElementById("map"), {
      zoom: 12,
      center: santaCruz,
    });
    const marker = new google.maps.Marker({
      position: santaCruz,
      map: map,
      draggable: true
    });

    google.maps.event.addListener(marker, 'dragend', function (evt) {
      let lat = evt.latLng.lat().toFixed(6);
      let lng = evt.latLng.lng().toFixed(6);
      pushEventToComponent('set_lat_lng', { lat: lat, lng: lng });
      map.panTo(evt.latLng);
    });

    // centers the map on markers coords
    map.setCenter(marker.position);
  },
};

Finally my handle_event:

 def handle_event(
      "set_lat_lng",
      %{"lat" => lat, "lng" => lng} = attrs,
      socket
    ) do

  # I'm NUKING my entire existing form data here. 
  # What do I do in this case, socket.assigns.form_values 
  # perhaps I need to save the entire form values in this 
  # theoretical assigns? Maybe there's a better more recommended way?
  listing_changeset =
    %Listing{}
    |> Listings.change_listing(attrs)
    |> Map.put(:action, :validate)

  socket
  |> assign(:listing_changeset, listing_changeset)
  |> then(&{:noreply, &1})
end

Start with a changeset in your mount and store in your socket, then use that instead of %Listing{}

I do have a changeset in the mount, what I found thought is that only “valid” values are persisted in the changeset changes. All other invalid inputs are erased after I run the changeset.

This results in invalid form fields being wiped whenever I handle_event("set_lat_lng"). Jarring end user experience.

defmodule TestWeb.ListingLive.Create do
  use TestWeb, :live_view

  alias Test.Listings
  alias Test.Listings.Listing
  alias Test.Extension.KeyToAtom
  alias Test.Convert

  def mount(_params, _session, socket) do
    listing_types =
      Listings.list_listing_types() |> Enum.map(&{&1.name, &1.id}) |> Enum.sort_by(&elem(&1, 0))

    listing_changeset = Listings.change_listing(%Listing{}, %{})

    socket =
      socket
      |> assign(:listing_changeset, listing_changeset)
      |> assign(:listing_types, listing_types)

    {:ok, socket}
  end

  def handle_event(
        "set_lat_lng",
        %{"lat" => lat, "lng" => lng} = attrs,
        socket
      ) do
    listing_changeset =
      %Listing{}
      |> Listings.change_listing(attrs)
      |> Map.put(:action, :validate)

    socket
    |> assign(:listing_changeset, listing_changeset)
    |> then(&{:noreply, &1})
  end

  def handle_event(
        "validate",
        %{"listing" => attrs},
        socket
      ) do
    listing_changeset =
      %Listing{}
      |> Listings.change_listing(attrs)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, listing_changeset: listing_changeset)}
  end

  def handle_event(
        "create-listing",
        attrs,
        %{assigns: %{current_user: current_user}} = socket
      ) do
    IO.inspect("Doing a submit")
    IO.inspect(attrs)
    {:noreply, socket}
  end
end

I must be doing something incorrectly and not seeing the clear solution here. Appreciate the help!

I think @Hermanverschooten 's suggestion is to use something like Changeset.change to set the lat long on the changeset you have stored in assigns that has all your form data you don’t want to lose instead of creating a new changeset from an empty struct. Something like this:

  def handle_event(
        "set_lat_lng",
        %{"lat" => lat, "lng" => lng} = attrs,
        %{listing_changeset: existing_changeset} = socket # extract existing changeset
      ) do
    listing_changeset =
      existing_changeset # use as base
      |> Ecto.Changeset.change(lat: lat, lng: lng) # change just the attrs you care about
      |> Map.put(:action, :validate) # this is probably no longer meaningful and can be removed

    socket
    |> assign(:listing_changeset, listing_changeset)
    |> then(&{:noreply, &1})
  end
1 Like

Thanks that helped!

For posterity, here’s how I made things click in my head.

The handle_event("validate") creates a fresh Listing changeset using the form attr values as the client sees it. After all, we don’t want to wipe and pull out the rug from under the user.

The lat lng handler that’s invoked from the clientside Google Maps event manually puts the change into the socket.assigns.changeset.

listing_changeset = 
  socket.assigns.listing_changeset
  |> Ecto.Changeset.change(lat: lat, lng: lng)

By setting it in the socket.assigns.changeset, the values are placed in the form inputs, and when the user types in other data into other fields, that same lat lng is sent as well to handle_event("validate") so the lat lng is not lost.

The lesson here is to use Ecto.Changeset.change when inserting data from external events into form bound changesets and all is peachy.

Thanks gents!

2 Likes