Keep the typed name when the foci pin moves

61d5d49 · Gabriel Morell · 2026-08-12 14:32

12 files +152 -23
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/form_component.ex
+18 −3
@@ -15,6 +15,22 @@ defmodule RoomSanctumWeb.FociLive.FormComponent do
15 15 end
16 16 end
17 17
18 + # Moving the pin must not discard what has been typed but not yet saved, so
19 + # the changeset is rebuilt from the form's current params rather than from
20 + # the stored record. No :action is set, so a half-filled form does not start
21 + # showing errors just because the map was touched.
22 + defp with_place(socket, place) do
23 + params = socket |> current_params() |> Map.put("place", place)
24 +
25 + Configuration.change_foci(socket.assigns.foci, params)
26 + end
27 +
28 + defp current_params(%{assigns: %{form: %Phoenix.HTML.Form{source: %Ecto.Changeset{params: p}}}})
29 + when is_map(p),
30 + do: p
31 +
32 + defp current_params(_socket), do: %{}
33 +
18 34 @impl true
19 35 def update(%{foci: foci} = assigns, socket) do
20 36 changeset = Configuration.change_foci(foci)
@@ -68,9 +84,8 @@ defmodule RoomSanctumWeb.FociLive.FormComponent do
68 84 srid: 4326
69 85 }
70 86
71 − cs = socket.assigns.foci |> Ecto.Changeset.change(place: lat_lng_pt)
72 − # |> Map.put(:action, :validate)
73 − # cs = socket.assigns.foci |> Configuration.change_foci(%{place: lat_lng_pt}) |> Map.put(:action, :validate)
87 + cs = with_place(socket, lat_lng_pt)
88 +
74 89 {
75 90 :noreply,
76 91 socket
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/form_component_coords.ex
+15 −2
@@ -18,6 +18,19 @@ defmodule RoomSanctumWeb.FociLive.FormComponentCoords do
18 18 end
19 19 end
20 20
21 + # see FormComponent: keep what has been typed when the coordinates change
22 + defp with_place(socket, place) do
23 + params = socket |> current_params() |> Map.put("place", place)
24 +
25 + Configuration.change_foci(socket.assigns.foci, params)
26 + end
27 +
28 + defp current_params(%{assigns: %{form: %Phoenix.HTML.Form{source: %Ecto.Changeset{params: p}}}})
29 + when is_map(p),
30 + do: p
31 +
32 + defp current_params(_socket), do: %{}
33 +
21 34 @impl true
22 35 def update(%{foci: foci} = assigns, socket) do
23 36 changeset = Configuration.change_foci(foci)
@@ -96,7 +109,7 @@ defmodule RoomSanctumWeb.FociLive.FormComponentCoords do
96 109 srid: 4326
97 110 }
98 111
99 − cs = socket.assigns.foci |> Ecto.Changeset.change(place: lat_lng_pt)
112 + cs = with_place(socket, lat_lng_pt)
100 113
101 114 {
102 115 :noreply,
@@ -139,7 +152,7 @@ defmodule RoomSanctumWeb.FociLive.FormComponentCoords do
139 152 srid: 4326
140 153 }
141 154
142 − cs = socket.assigns.foci |> Ecto.Changeset.change(place: lat_lng_pt)
155 + cs = with_place(socket, lat_lng_pt)
143 156
144 157 {
145 158 :noreply,
modified apps/room_sanctum/test/room_sanctum_web/live/foci_edit_test.exs
+119 −18
@@ -13,9 +13,32 @@ defmodule RoomSanctumWeb.FociEditTest do
13 13 import Phoenix.LiveViewTest
14 14
15 15 alias RoomSanctum.{Accounts, Configuration}
16 + alias RoomSanctumWeb.FociLive.{FormComponent, FormComponentCoords}
16 17
17 18 @sfo %Geo.Point{coordinates: {-122.3841977119446, 37.62257668960213}, srid: 4326}
18 19
20 + # The map hook pushes its events to #foci-form, which LiveViewTest cannot
21 + # drive (the form carries phx-target, not phx-hook), so the tests that need
22 + # a pin drop go at the component directly.
23 + defp socket_for(component, ctx) do
24 + {:ok, socket} =
25 + component.update(
26 + %{
27 + foci: ctx.foci,
28 + id: ctx.foci.id,
29 + title: "Modify Foci",
30 + action: :edit,
31 + patch: "/",
32 + current_user: ctx.user
33 + },
34 + %Phoenix.LiveView.Socket{assigns: %{__changed__: %{}, flash: %{}}}
35 + )
36 +
37 + socket
38 + end
39 +
40 + defp typed_name(socket), do: Ecto.Changeset.get_field(socket.assigns.form.source, :name)
41 +
19 42 setup %{conn: conn} do
20 43 {:ok, user} =
21 44 Accounts.register_user(%{
@@ -97,24 +120,10 @@ defmodule RoomSanctumWeb.FociEditTest do
97 120 end
98 121
99 122 describe "picking a new place" do
100 − # The map hook pushes map-update to #foci-form, which LiveViewTest cannot
101 − # drive (the form carries phx-target, not phx-hook), so this drives the
102 − # component directly. The point is that the fallback is assign_new: a
103 − # place clicked on the map must survive the next update/2.
104 − alias RoomSanctumWeb.FociLive.FormComponent
105 −
106 − defp socket_for(foci) do
107 − {:ok, socket} =
108 − FormComponent.update(
109 − %{foci: foci, id: foci.id, title: "Modify Foci", action: :edit, patch: "/"},
110 − %Phoenix.LiveView.Socket{assigns: %{__changed__: %{}}}
111 − )
112 −
113 − socket
114 − end
115 −
123 + # The fallback is assign_new: a place clicked on the map must survive the
124 + # next update/2.
116 125 test "the stored place is what the form falls back to", ctx do
117 − assert socket_for(ctx.foci).assigns.place == @sfo
126 + assert socket_for(FormComponent, ctx).assigns.place == @sfo
118 127 end
119 128
120 129 test "a place clicked on the map is not reset by the next re-render", ctx do
@@ -122,7 +131,7 @@ defmodule RoomSanctumWeb.FociEditTest do
122 131 FormComponent.handle_event(
123 132 "map-update",
124 133 %{"latlng" => %{"lat" => 42.3601, "lng" => -71.0589}},
125 − socket_for(ctx.foci)
134 + socket_for(FormComponent, ctx)
126 135 )
127 136
128 137 assert socket.assigns.place.coordinates == {-71.0589, 42.3601}
@@ -135,6 +144,98 @@ defmodule RoomSanctumWeb.FociEditTest do
135 144 end
136 145 end
137 146
147 + describe "renaming and moving the pin together" do
148 + # Each of these rebuilt the changeset from the stored record, so whatever
149 + # had been typed but not yet saved was thrown away.
150 + test "dragging the map keeps the name you typed", ctx do
151 + socket = socket_for(FormComponent, ctx)
152 +
153 + {:noreply, socket} =
154 + FormComponent.handle_event("validate", %{"foci" => %{"name" => "SFO2"}}, socket)
155 +
156 + assert typed_name(socket) == "SFO2"
157 +
158 + {:noreply, socket} =
159 + FormComponent.handle_event(
160 + "map-update",
161 + %{"latlng" => %{"lat" => 42.3601, "lng" => -71.0589}},
162 + socket
163 + )
164 +
165 + assert typed_name(socket) == "SFO2"
166 + assert socket.assigns.place.coordinates == {-71.0589, 42.3601}
167 + end
168 +
169 + test "the coordinate box keeps the name you typed", ctx do
170 + socket = socket_for(FormComponentCoords, ctx)
171 +
172 + {:noreply, socket} =
173 + FormComponentCoords.handle_event("validate", %{"foci" => %{"name" => "SFO2"}}, socket)
174 +
175 + {:noreply, socket} =
176 + FormComponentCoords.handle_event("coord-update", %{"coords" => "42.3601, -71.0589"}, socket)
177 +
178 + assert typed_name(socket) == "SFO2"
179 + assert socket.assigns.place.coordinates == {-71.0589, 42.3601}
180 + end
181 +
182 + test "nudging a single coordinate keeps the name you typed", ctx do
183 + socket = socket_for(FormComponentCoords, ctx)
184 +
185 + {:noreply, socket} =
186 + FormComponentCoords.handle_event("validate", %{"foci" => %{"name" => "SFO2"}}, socket)
187 +
188 + {:noreply, socket} =
189 + FormComponentCoords.handle_event("latitude-update", %{"value" => "42.3601"}, socket)
190 +
191 + assert typed_name(socket) == "SFO2"
192 + assert {_lon, 42.3601} = socket.assigns.place.coordinates
193 + end
194 +
195 + test "saving after both stores both", ctx do
196 + socket = socket_for(FormComponent, ctx)
197 +
198 + {:noreply, socket} =
199 + FormComponent.handle_event("validate", %{"foci" => %{"name" => "SFO2"}}, socket)
200 +
201 + {:noreply, socket} =
202 + FormComponent.handle_event(
203 + "map-update",
204 + %{"latlng" => %{"lat" => 42.3601, "lng" => -71.0589}},
205 + socket
206 + )
207 +
208 + {:noreply, _socket} =
209 + FormComponent.handle_event("save", %{"foci" => %{"name" => "SFO2"}}, socket)
210 +
211 + reloaded = Configuration.get_foci!(ctx.foci.id)
212 + assert reloaded.name == "SFO2"
213 + assert reloaded.place.coordinates == {-71.0589, 42.3601}
214 + end
215 +
216 + test "moving the pin twice still keeps the name", ctx do
217 + socket = socket_for(FormComponent, ctx)
218 +
219 + {:noreply, socket} =
220 + FormComponent.handle_event("validate", %{"foci" => %{"name" => "SFO2"}}, socket)
221 +
222 + socket =
223 + Enum.reduce([{42.36, -71.05}, {40.71, -74.0}], socket, fn {lat, lng}, acc ->
224 + {:noreply, acc} =
225 + FormComponent.handle_event(
226 + "map-update",
227 + %{"latlng" => %{"lat" => lat, "lng" => lng}},
228 + acc
229 + )
230 +
231 + acc
232 + end)
233 +
234 + assert typed_name(socket) == "SFO2"
235 + assert socket.assigns.place.coordinates == {-74.0, 40.71}
236 + end
237 + end
238 +
138 239 describe "the edit page itself" do
139 240 test "renders with the foci's name in the form", ctx do
140 241 {:ok, _live, html} = live(ctx.conn, Routes.foci_show_path(ctx.conn, :edit, ctx.foci))

Parents: 11a1dc3