neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Keep the typed name when the foci pin moves
61d5d49 · Gabriel Morell · 2026-08-12 14:32
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