Skip to content

Commit

Permalink
refactor(TripPlannerForm): use LiveComponent
Browse files Browse the repository at this point in the history
  • Loading branch information
thecristen committed Sep 18, 2024
1 parent 8af34c0 commit 6c4eaf1
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 253 deletions.
4 changes: 1 addition & 3 deletions assets/ts/phoenix-hooks/algolia-autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const AlgoliaAutocomplete: Partial<ViewHook> = {
const pushToLiveView = (data: Partial<Item>): void => {
if (hook.el.querySelector("[data-config='trip-planner']")) {
// this will fail outside of a LiveView, that's fine
hook.pushEventTo(hook.el, "map_change", {
hook.pushEvent("map_change", {
id: hook.el.id,
...data
});
Expand All @@ -46,8 +46,6 @@ const AlgoliaAutocomplete: Partial<ViewHook> = {
})
);

// side effect: dispatch event to LV to get initial map markers
pushToLiveView(inputValues);
return inputValues.name || "";
};

Expand Down
58 changes: 42 additions & 16 deletions lib/dotcom/trip_plan/input_form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,52 @@ defmodule Dotcom.TripPlan.InputForm do
use TypedEctoSchema
import Ecto.Changeset

@valid_modes [:commuter_rail, :subway, :bus, :ferry]
@time_types [:now, :leave_at, :arrive_by]

@error_messages %{
from: "Please specify an origin location.",
to: "Please add a destination.",
from_to_same: "Please select a destination at a different location from the origin."
from_to_same: "Please select a destination at a different location from the origin.",
modes: "Please select at least one mode of transit.",
datetime: "Please specify a date and time in the future or select 'Now'."
}

@primary_key false
typed_embedded_schema do
embeds_one(:from, __MODULE__.Location)
embeds_one(:to, __MODULE__.Location)
field(:datetime_type, Ecto.Enum, values: @time_types)
field(:datetime, :naive_datetime)
field(:modes, {:array, Ecto.Enum}, values: @valid_modes)
field(:wheelchair, :boolean, default: true)
end

def time_types, do: @time_types
def valid_modes, do: @valid_modes

def changeset(params \\ %{}) do
changeset(%__MODULE__{}, params)
end

def changeset(form, params) do
form
|> cast(params, [])
|> cast(params, [:datetime_type, :datetime, :modes, :wheelchair])
|> cast_embed(:from, required: true)
|> cast_embed(:to, required: true)
end

def validate_params(params) do
changes =
params
|> changeset()
|> update_change(:from, &update_location_change/1)
|> update_change(:to, &update_location_change/1)
|> validate_required(:from, message: error_message(:from))
|> validate_required(:to, message: error_message(:to))
|> validate_same_locations()

if changes.errors == [] do
changes
else
%Ecto.Changeset{changes | action: :update}
end
params
|> changeset()
|> update_change(:from, &update_location_change/1)
|> update_change(:to, &update_location_change/1)
|> validate_required(:from, message: error_message(:from))
|> validate_required(:to, message: error_message(:to))
|> validate_required([:datetime_type, :modes, :wheelchair])
|> validate_same_locations()
|> validate_length(:modes, min: 1, message: error_message(:modes))
|> validate_chosen_datetime()
end

# make the parent field blank if the location isn't valid
Expand All @@ -66,6 +74,24 @@ defmodule Dotcom.TripPlan.InputForm do
end
end

defp validate_chosen_datetime(changeset) do
case get_field(changeset, :datetime_type) do
:now ->
force_change(changeset, :datetime, NaiveDateTime.local_now())

_ ->
changeset
|> validate_change(:datetime, &validate_datetime/2)
end
end

defp validate_datetime(field, date) do
case Timex.compare(date, Util.now(), :hours) do
-1 -> [{field, error_message(:datetime)}]
_ -> []
end
end

def error_message(key), do: @error_messages[key]

defmodule Location do
Expand Down
194 changes: 194 additions & 0 deletions lib/dotcom_web/components/live_components/trip_planner_form.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
defmodule DotcomWeb.Components.LiveComponents.TripPlannerForm do
@moduledoc """
A form to plan trips.
"""
use DotcomWeb, :live_component

import DotcomWeb.ViewHelpers, only: [mode_name: 1, svg: 1]
import Phoenix.HTML.Form, only: [input_name: 2, input_value: 2, input_id: 2]

alias Dotcom.TripPlan.InputForm

@form_defaults %{
"datetime_type" => :now,
"datetime" => NaiveDateTime.local_now(),
"modes" => [:commuter_rail, :subway, :bus, :ferry],
"wheelchair" => true
}

@impl true
def mount(socket) do
form =
%InputForm{}
|> InputForm.changeset(@form_defaults)
|> to_form()

{:ok,
assign(socket, %{
form: form,
location_keys: InputForm.Location.fields(),
show_datepicker: false
})}
end

@impl true
def render(assigns) do
~H"""
<section>
<.form
:let={f}
for={@form}
method="get"
phx-change="validate"
phx-submit="save_form"
phx-target={@myself}
>
<div :for={field <- [:from, :to]} class="mb-1">
<.algolia_autocomplete
config_type="trip-planner"
placeholder="Enter a location"
id={"#{@form_name}--#{field}"}
>
<.inputs_for :let={location_f} field={f[field]} skip_hidden={true}>
<input
:for={subfield <- @location_keys}
type="hidden"
class="location-input"
id={location_f[subfield].id}
value={location_f[subfield].value}
name={location_f[subfield].name}
/>
</.inputs_for>
<.form_error :for={{msg, _} <- f[field].errors} :if={used_input?(f[field])}>
<%= msg %>
</.form_error>
</.algolia_autocomplete>
</div>
<.fieldset legend="When">
<.input_group
id={input_id(@form, :datetime_type)}
field={f[:datetime_type]}
type="radio"
phx-click="toggle_datepicker"
phx-target={@myself}
phx-update="ignore"
>
<:input_item
:for={type <- Ecto.Enum.values(InputForm, :datetime_type)}
id={input_name(@form, :datetime_type) <> "_#{type}"}
value={type}
checked={input_value(@form, :datetime_type) == type}
/>
</.input_group>
<.form_error
:for={{msg, _} <- f[:datetime_type].errors}
:if={used_input?(f[:datetime_type])}
>
<%= msg %>
</.form_error>
<.form_label :if={@show_datepicker} for="timepick">
<input
id="timepick"
type="datetime-local"
step="any"
name={input_name(@form, :datetime)}
value={input_value(@form, :datetime)}
/>
<span class="sr-only">Date and time to leave at or arrive by</span>
</.form_label>
<.form_error
:for={{msg, _} <- f[:datetime_type].errors}
:if={used_input?(f[:datetime_type])}
>
<%= msg %>
</.form_error>
<.form_error :for={{msg, _} <- f[:datetime].errors} :if={used_input?(f[:datetime])}>
<%= msg %>
</.form_error>
</.fieldset>
<.fieldset legend="Modes">
<.accordion open>
<:heading>
<%= input_value(@form, :modes) |> selected_modes() %>
</:heading>
<:content>
<.mode_selector field={f[:modes]} modes={modes()} />
</:content>
<:extra :if={used_input?(f[:modes])}>
<.form_error :for={{msg, _} <- f[:modes].errors}>
<%= msg %>
</.form_error>
</:extra>
</.accordion>
</.fieldset>
<div class="inline-flex items-center">
<.form_input type="checkbox" field={f[:wheelchair]} label="Prefer accessible routes" />
<%= svg("icon-accessible-small.svg") %>
</div>
<.button type="submit" phx-disable-with="Planning your trip...">
Get trip suggestions
</.button>
</.form>
</section>
"""
end

@impl true
def handle_event("toggle_datepicker", %{"value" => datetime_value}, socket) do
{:noreply, assign(socket, :show_datepicker, datetime_value !== "now")}
end

def handle_event("validate", %{"input_form" => params}, socket) do
form =
params
|> InputForm.validate_params()
|> Phoenix.Component.to_form()

{:noreply, assign(socket, %{form: form})}
end

def handle_event("save_form", %{"input_form" => params}, socket) do
params
|> InputForm.validate_params()
|> Ecto.Changeset.apply_action(:update)
|> case do
{:ok, data} ->
_ = socket.assigns.on_submit.(data)

data
|> Ecto.Changeset.change()
|> Phoenix.Component.to_form()

{:error, changeset} ->
changeset
|> Phoenix.Component.to_form()
end
|> then(fn form ->
{:noreply, assign(socket, %{form: form})}
end)
end

defp modes do
InputForm
|> Ecto.Enum.values(:modes)
|> Enum.map(&{mode_name(&1), &1})
end

defp selected_modes([:commuter_rail, :subway, :bus, :ferry]) do
"All modes"
end

defp selected_modes([]), do: "No transit modes selected"

defp selected_modes([mode]), do: mode_name(mode) <> " Only"
defp selected_modes([mode1, mode2]), do: mode_name(mode1) <> " and " <> mode_name(mode2)

defp selected_modes(modes) do
modes
|> Enum.map(&mode_name/1)
|> Enum.intersperse(", ")
|> List.insert_at(-2, "and ")
|> Enum.join("")
end
end
Loading

0 comments on commit 6c4eaf1

Please sign in to comment.