Share
Embed
Making Invalid States Invalid in LiveView
Have you ever found a bug in your LiveView code because your app somehow had invalid and incoherent state?
“Your total is -$500.”
“Your return flight is .”
“The temperature is -600°F.”
When programming user interfaces, it’s important that we do not display inconsistent states to our users. But how can we prevent those bugs?
By making those invalid states impossible to represent!
Join me as we walk through a LiveView example that has bugs because of invalid states. As we fix them, you’ll learn to model domains in a way that makes rendering invalid states impossible. Your users and your future self will thank you.
German Velasco
October 15, 2021
Transcript
196 slides
-
OTHER NAMES ▸ Making invalid states unrepresentable ▸ Making illegal
states unrepresentable -
OTHER NAMES ▸ Making invalid states unrepresentable ▸ Making illegal
states unrepresentable ▸ Making impossible states impossible -
ASSIGNING VALUES def mount(_, _, socket) do { :ok, socket
|> assign(:direction, "one-way") |> assign(:departure, Date.utc_today()) |> assign(:return, nil) } end -
ASSIGNING VALUES def mount(_, _, socket) do { :ok, socket
|> assign(:direction, "one-way") |> assign(:departure, Date.utc_today()) |> assign(:return, nil) } end -
RENDERING THE ASSIGNS <form phx-submit="book"> <%= select :booking, :flight_type, ["one-way",
"two-way"], value: @direction %> <%= text_input :booking, :departure, value: @departure %> <%= text_input :booking, :return, value: @return %> <%= submit "Book" %> </form> -
RENDERING THE ASSIGNS <form phx-submit="book"> <%= select :booking, :flight_type, ["one-way",
"two-way"], value: @direction %> <%= text_input :booking, :departure, value: @departure %> <%= text_input :booking, :return, value: @return %> <%= submit "Book" %> </form> -
RENDERING THE ASSIGNS <form phx-submit="book"> <%= select :booking, :flight_type, ["one-way",
"two-way"], value: @direction %> <%= text_input :booking, :departure, value: @departure %> <%= text_input :booking, :return, value: @return %> <%= submit "Book" %> </form> -
RENDERING THE ASSIGNS <form phx-submit="book"> <%= select :booking, :flight_type, ["one-way",
"two-way"], value: @direction %> <%= text_input :booking, :departure, value: @departure %> <%= text_input :booking, :return, value: @return %> <%= submit "Book" %> </form> -
HANDLING THE EVENT def handle_event("book", %{"booking" => params}, socket) do
{:ok, message} = FlightBooker.book_trip(params) socket |> put_flash(:info, message) |> noreply() end -
HANDLING THE EVENT def handle_event("book", %{"booking" => params}, socket) do
{:ok, message} = FlightBooker.book_trip(params) socket |> put_flash(:info, message) |> noreply() end -
BOOKING THE TRIP def book_trip(params) do case params do %{"flight_type"
=> "one-way", "departure" => departure} -> {:ok, one_way_message(departure)} %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> {:ok, two_way_message(departure, return)} end end -
BOOKING THE TRIP def book_trip(params) do case params do %{"flight_type"
=> "one-way", "departure" => departure} -> {:ok, one_way_message(departure)} %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> {:ok, two_way_message(departure, return)} end end -
BOOKING THE TRIP def book_trip(params) do case params do %{"flight_type"
=> "one-way", "departure" => departure} -> {:ok, one_way_message(departure)} %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> {:ok, two_way_message(departure, return)} end end -
! THE BUGS ▸ ! One-way flight that got charged
for a return flight -
! THE BUGS ▸ ! One-way flight that got charged
for a return flight ▸ " Two-way flight without a return flight -
OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",
departure: Date.t() | nil, return: Date.t() | nil } -
OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",
departure: Date.t() | nil, return: Date.t() | nil } -
OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",
departure: Date.t() | nil, return: Date.t() | nil } -
OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",
departure: Date.t() | nil, return: Date.t() | nil } -
OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",
departure: Date.t() | nil, return: Date.t() | nil } -
POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}
%{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil} -
POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}
%{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil} -
POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}
%{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil} -
INVALID STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}
%{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil} -
VALID STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}
%{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil} -
ONLY 2/8 STATES ARE VALID! # one-way %{direction: "one-way", departure:
Date.t(), return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} -
A MORE constrained FLIGHT BOOKER DOMAIN MODEL ▸ One-way flight
with departure date -
A MORE constrained FLIGHT BOOKER DOMAIN MODEL ▸ One-way flight
with departure date ▸ OR -
A MORE constrained FLIGHT BOOKER DOMAIN MODEL ▸ One-way flight
with departure date ▸ OR ▸ Two-way flight with departure and return dates -
A MORE constrained FLIGHT BOOKER DOMAIN MODEL @type flight_booker ::
{:one_way, Date.t()} | {:two_way, Date.t(), Date.t()} -
A MORE constrained FLIGHT BOOKER DOMAIN MODEL @type flight_booker ::
{:one_way, Date.t()} | {:two_way, Date.t(), Date.t()} -
A MORE constrained FLIGHT BOOKER DOMAIN MODEL @type flight_booker ::
{:one_way, Date.t()} | {:two_way, Date.t(), Date.t()} -
TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do
{:one_way, date} end def two_way(%Date{} = departure, %Date{} = return) do {:two_way, departure, return} end end -
TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do
{:one_way, date} end def two_way(%Date{} = departure, %Date{} = return) do {:two_way, departure, return} end end -
TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do
{:one_way, date} end def two_way(%Date{} = departure, %Date{} = return) do {:two_way, departure, return} end end -
TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do
%OneWay{departure: date} end def two_way(%Date{} = departure, %Date{} = return) do %TwoWay{departure: departure, return: return} end end -
TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do
%OneWay{departure: date} end def two_way(%Date{} = departure, %Date{} = return) do %TwoWay{departure: departure, return: return} end end -
TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do
{:one_way, date} end def two_way(%Date{} = departure, %Date{} = return) do {:two_way, departure, return} end end -
IMPLICATIONS OF OUR NEW DOMAIN MODEL ▸ Render complex types
▸ Handle events with user input -
1. RENDER COMPLEX TYPE def mount(_, _, socket) do today
= Date.utc_today() booker = FlightBooker.one_way(today) {:ok, assign(socket, :booker, booker)} end -
1. RENDER COMPLEX TYPE def mount(_, _, socket) do today
= Date.utc_today() booker = FlightBooker.one_way(today) {:ok, assign(socket, :booker, booker)} end -
1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker
do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form> -
1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker
do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form> -
1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker
do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form> -
1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker
do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form> -
1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker
do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form> -
FORCED TO HANDLE RETURN ON "ONE-WAY" FLIGHT <form phx-submit="book" phx-change="update">
<%= case @booker do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form> -
FORCED TO HANDLE RETURN ON "ONE-WAY" FLIGHT <form phx-submit="book" phx-change="update">
<%= case @booker do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form> -
FORCED TO HANDLE RETURN ON "ONE-WAY" FLIGHT <form phx-submit="book" phx-change="update">
<%= case @booker do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form> -
TRANSFORM USER INPUT THROUGH PARSING ▸ less structured data |>
more structured data -
TRANSFORM USER INPUT THROUGH PARSING ▸ less structured data |>
more structured data ▸ raw data |> valid domain -
TRANSFORM USER INPUT THROUGH PARSING ▸ less structured data |>
more structured data ▸ raw data |> valid domain ▸ ⚠ |> -
HANDLE EVENT def handle_event("book", %{"booking" => params}, socket) do {:ok,
message} = params |> parse_params_into_booking() |> FlightBooker.book_trip() socket |> put_flash(:info, message) |> noreply() end -
HANDLE EVENT def handle_event("book", %{"booking" => params}, socket) do {:ok,
message} = params |> parse_params_into_booking() |> FlightBooker.book_trip() socket |> put_flash(:info, message) |> noreply() end -
HANDLE EVENT def handle_event("book", %{"booking" => params}, socket) do {:ok,
message} = params |> parse_params_into_booking() |> FlightBooker.book_trip() socket |> put_flash(:info, message) |> noreply() end -
defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"
=> departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"
=> departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"
=> departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"
=> departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"
=> departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"
=> departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"
=> departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"
=> departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
INVALID STATES REMOVED %{direction: "one-way", departure: nil, return: nil} %{direction:
"one-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: nil, return: nil} %{direction: "two-way", departure: nil, return: Date.t()} -
WHAT ABOUT ERROR HANDLING? ▸ Validate and present errors ▸
Potential for same problems -
POSSIBLE ERRORS @type error_fields :: :departure | :return @type errors
:: [error_fields] -
ADDING ERRORS ASSIGN socket |> assign(:direction, "one-way") |> assign(:departure, Date.utc_today())
|> assign(:return, nil) + |> assign(:errors, []) -
(PREVIOUS) POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return:
Date.t()} %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil} -
(PREVIOUS) POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return:
Date.t()} %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil} -
# one-way %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: []}
%{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil} -
8 X 4 = 32 POSSIBLE STATES! # one-way %{direction:
"one-way", departure: Date.t(), return: Date.t(), errors: []} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: Date.t(), errors: []} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:return]} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: Date.t(), return: nil, errors: []} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: nil, errors: []} %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} %{direction: "one-way", departure: nil, return: nil, errors: [:return]} %{direction: "one-way", departure: nil, return: nil, errors: [:departure, :return]} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "two-way", departure: nil, return: Date.t(), errors: []} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:return]} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure, :return]} %{direction: "two-way", departure: Date.t(), return: nil, errors: []} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:departure, :return]} %{direction: "two-way", departure: nil, return: nil, errors: []} %{direction: "two-way", departure: nil, return: nil, errors: [:departure]} %{direction: "two-way", departure: nil, return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]} -
"Please select a valid return date" %{ direction: "one-way", departure:
Date.t(), return: nil, errors: [:return] } -
"Please select a valid return date" %{ direction: "one-way", departure:
Date.t(), return: nil, errors: [:return] } ▸ ! -
ONLY 6 VALID STATES! ## one-way flight # one way
flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]} -
ONLY 6 VALID STATES! ## one-way flight # one way
flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]} -
ONLY 6 VALID STATES! ## one-way flight # one way
flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]} -
ONLY 6 VALID STATES! ## one-way flight # one way
flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]} -
ONLY 6 VALID STATES! ## one-way flight # one way
flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]} -
INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()
| {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error} -
INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()
| {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error} -
INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()
| {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error} -
INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()
| {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error} -
INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()
| {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error} -
POSSIBLE STATES IN OUR DOMAIN MODEL # one way {:one_way,
Date.t()} {:one_way, {:error, any()}} # two way {:two_way, departure: Date.t(), return: Date.t()} {:two_way, departure: {:error, any()}, return: Date.t()} {:two_way, departure: Date.t(), return: {:error, any()}} {:two_way, departure: {:error, any()}, return: {:error, any()}} -
POSSIBLE STATES IN OUR DOMAIN MODEL # one way {:one_way,
Date.t()} {:one_way, {:error, any()}} # two way {:two_way, departure: Date.t(), return: Date.t()} {:two_way, departure: {:error, any()}, return: Date.t()} {:two_way, departure: Date.t(), return: {:error, any()}} {:two_way, departure: {:error, any()}, return: {:error, any()}} -
POSSIBLE STATES IN OUR DOMAIN MODEL # one way {:one_way,
Date.t()} {:one_way, {:error, any()}} # two way {:two_way, departure: Date.t(), return: Date.t()} {:two_way, departure: {:error, any()}, return: Date.t()} {:two_way, departure: Date.t(), return: {:error, any()}} {:two_way, departure: {:error, any()}, return: {:error, any()}} -
RENDERING COMPLEX TYPES <form phx-submit="book" phx-change="update"> <%= case @booker do
%> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <.departure_input departure={departure} /> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <.departure_input departure={departure} /> <.return_input return={return} /> <% end %> <%= submit "Book" %> </form> -
RENDERING COMPLEX TYPES <form phx-submit="book" phx-change="update"> <%= case @booker do
%> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <.departure_input departure={departure} /> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <.departure_input departure={departure} /> <.return_input return={return} /> <% end %> <%= submit "Book" %> </form> -
RENDERING COMPLEX TYPES <form phx-submit="book" phx-change="update"> <%= case @booker do
%> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <.departure_input departure={departure} /> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <.departure_input departure={departure} /> <.return_input return={return} /> <% end %> <%= submit "Book" %> </form> -
RENDERING COMPLEX TYPES <form phx-submit="book" phx-change="update"> <%= case @booker do
%> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <.departure_input departure={departure} /> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <.departure_input departure={departure} /> <.return_input return={return} /> <% end %> <%= submit "Book" %> </form> -
RENDERING COMPLEX TYPES (DEPARTURE INPUT) def departure_input(assigns) do ~H""" <%=
case @departure do %> <% {:error, value} -> %> <%= text_input :booking, :departure, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :departure, value: value %> <% end %> """ end -
RENDERING COMPLEX TYPES (DEPARTURE INPUT) def departure_input(assigns) do ~H""" <%=
case @departure do %> <% {:error, value} -> %> <%= text_input :booking, :departure, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :departure, value: value %> <% end %> """ end -
RENDERING COMPLEX TYPES (DEPARTURE INPUT) def departure_input(assigns) do ~H""" <%=
case @departure do %> <% {:error, value} -> %> <%= text_input :booking, :departure, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :departure, value: value %> <% end %> """ end -
RENDERING COMPLEX TYPES (DEPARTURE INPUT) def departure_input(assigns) do ~H""" <%=
case @departure do %> <% {:error, value} -> %> <%= text_input :booking, :departure, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :departure, value: value %> <% end %> """ end -
RENDERING COMPLEX TYPES (RETURN INPUT) def return_input(assigns) do ~H""" <%=
case @return do %> <% {:error, value} -> %> <%= text_input :booking, :return, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :return, value: value %> <% end %> """ end -
RENDERING COMPLEX TYPES (RETURN INPUT) def return_input(assigns) do ~H""" <%=
case @return do %> <% {:error, value} -> %> <%= text_input :booking, :return, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :return, value: value %> <% end %> """ end -
RENDERING COMPLEX TYPES (RETURN INPUT) def return_input(assigns) do ~H""" <%=
case @return do %> <% {:error, value} -> %> <%= text_input :booking, :return, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :return, value: value %> <% end %> """ end -
RENDERING COMPLEX TYPES (RETURN INPUT) def return_input(assigns) do ~H""" <%=
case @return do %> <% {:error, value} -> %> <%= text_input :booking, :return, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :return, value: value %> <% end %> """ end -
TRANSFORM USER INPUT (BEFORE) defp parse_params_into_booking(params) do case params do
%{"flight_type" => "one-way", "departure" => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
TRANSFORM USER INPUT (BEFORE) defp parse_params_into_booking(params) do case params do
%{"flight_type" => "one-way", "departure" => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end -
TRANSFORM USER INPUT (NOW) defp parse_params_into_booking(params) do case params do
%{"flight_type" => "one-way", "departure" => departure} -> FlightBooker.one_way(departure) %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> FlightBooker.two_way(departure, return) %{"flight_type" => "two-way", "departure" => departure} -> return = Date.utc_today() |> Date.to_string() FlightBooker.two_way(departure, return) end end -
TRANSFORM USER INPUT (NOW) defp parse_params_into_booking(params) do case params do
%{"flight_type" => "one-way", "departure" => departure} -> FlightBooker.one_way(departure) %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> FlightBooker.two_way(departure, return) %{"flight_type" => "two-way", "departure" => departure} -> return = Date.utc_today() |> Date.to_string() FlightBooker.two_way(departure, return) end end -
TRANSFORM USER INPUT (NOW) defp parse_params_into_booking(params) do case params do
%{"flight_type" => "one-way", "departure" => departure} -> FlightBooker.one_way(departure) %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> FlightBooker.two_way(departure, return) %{"flight_type" => "two-way", "departure" => departure} -> return = Date.utc_today() |> Date.to_string() FlightBooker.two_way(departure, return) end end -
PARSING INPUT INTO OUR DOMAIN def one_way(departure_value) do departure_value |>
parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end -
PARSING INPUT INTO OUR DOMAIN def one_way(departure_value) do departure_value |>
parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end -
PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:
date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value} -
PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:
date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value} -
PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:
date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value} -
PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:
date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value} -
PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:
date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value} -
PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:
date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value} -
PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:
date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value} -
PARSING INPUT INTO OUR DOMAIN def one_way(departure_value) do departure_value |>
parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end -
PARSING INPUT INTO OUR DOMAIN def one_way(departure_value) do departure_value |>
parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end -
PARSING INPUT INTO OUR DOMAIN defp build_one_way(date) do {:one_way, date}
end defp build_two_way(departure, return) do {:two_way, departure, return} end -
26 INVALID STATES REMOVED # one-way %{direction: "one-way", departure: Date.t(),
return: Date.t(), errors: []} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: Date.t(), errors: []} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:return]} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: nil, errors: []} %{direction: "one-way", departure: nil, return: nil, errors: [:return]} %{direction: "one-way", departure: nil, return: nil, errors: [:departure, :return]} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "two-way", departure: nil, return: Date.t(), errors: []} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:return]} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure, :return]} %{direction: "two-way", departure: Date.t(), return: nil, errors: []} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:departure, :return]} %{direction: "two-way", departure: nil, return: nil, errors: []} %{direction: "two-way", departure: nil, return: nil, errors: [:departure]} %{direction: "two-way", departure: nil, return: nil, errors: [:return]} -
VALIDATE WHAT YOU CANNOT CONSTRAIN WITH MODELING def book_trip({:two_way, departure,
return}) do if Date.compare(departure, return) == :gt do {:error, "Departure cannot be after return"} else {:ok, "You have booked a two-way flight departing #{departure} and returning #{return}"} end end -
VALIDATE WHAT YOU CANNOT CONSTRAIN WITH MODELING def book_trip({:two_way, departure,
return}) do if Date.compare(departure, return) == :gt do {:error, "Departure cannot be after return"} else {:ok, "You have booked a two-way flight departing #{departure} and returning #{return}"} end end -
VALIDATE WHAT YOU CANNOT CONSTRAIN WITH MODELING def book_trip({:two_way, departure,
return}) do if Date.compare(departure, return) == :gt do {:error, "Departure cannot be after return"} else {:ok, "You have booked a two-way flight departing #{departure} and returning #{return}"} end end -
VALIDATE WHAT YOU CANNOT CONSTRAIN WITH MODELING def book_trip({:two_way, departure,
return}) do if Date.compare(departure, return) == :gt do {:error, "Departure cannot be after return"} else {:ok, "You have booked a two-way flight departing #{departure} and returning #{return}"} end end -
WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms ▸
Great casting helpers -
WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms ▸
Great casting helpers ▸ Great validation helpers -
WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms ▸
Great casting helpers ▸ Great validation helpers ▸ Use schemaless changesets as an anti-corruption layer -
COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way,
date_or_error} | {:two_way, date_or_error, date_or_error} -
COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way,
date_or_error} | {:two_way, date_or_error, date_or_error} -
COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way,
Ecto.Changeset.t()} | {:two_way, Ecto.Changeset.t()} -
USE ECTO'S CASTING & ERROR GENERATION def one_way(departure_value) do departure_value
|> parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end -
USE ECTO'S CASTING & ERROR GENERATION def one_way(departure_value) do departure_value
|> parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end -
USE ECTO'S CASTING & ERROR GENERATION def one_way(departure_value) do %OneWay{}
|> OneWay.changeset(%{departure: departure_value}) |> build_one_way() end def two_way(departure_value, return_value) do %TwoWay{} |> TwoWay.changeset(%{departure: departure_value, return: return_value}) |> build_two_way() end -
USE ECTO'S CASTING & ERROR GENERATION def one_way(departure_value) do %OneWay{}
|> OneWay.changeset(%{departure: departure_value}) |> build_one_way() end def two_way(departure_value, return_value) do %TwoWay{} |> TwoWay.changeset(%{departure: departure_value, return: return_value}) |> build_two_way() end -
RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,
changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %> -
RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,
changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %> -
RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,
changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %> -
RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,
changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %> -
RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,
changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %> -
RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,
changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %> -
RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,
changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %> -
RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,
changeset} -> %> <button type="button" phx-click="change-flight-type" value="two-way">Two-way</button> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <button type="button" phx-click="change-flight-type" value="one-way">One-way</button> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %> -
HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)
do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end -
HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)
do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end -
HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)
do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end -
HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)
do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end -
HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)
do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end -
HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"
=> departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end -
HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"
=> departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end -
HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"
=> departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end -
HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"
=> departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end -
HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"
=> departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end -
! EXPLORATION NOTES ▸ Model your domain to remove invalid
states ▸ ! Render complex types Declaratively & Exhaustively -
! EXPLORATION NOTES ▸ Model your domain to remove invalid
states ▸ ! Render complex types Declaratively & Exhaustively ▸ Parse raw ⚠ unsafe values into your domain values -
! EXPLORATION NOTES ▸ Model your domain to remove invalid
states ▸ ! Render complex types Declaratively & Exhaustively ▸ Parse raw ⚠ unsafe values into your domain values ▸ ✅ Validate what you cannot constrain -
! EXPLORATION NOTES ▸ Model your domain to remove invalid
states ▸ ! Render complex types Declaratively & Exhaustively ▸ Parse raw ⚠ unsafe values into your domain values ▸ ✅ Validate what you cannot constrain ▸ Combine powers with Ecto $ when dealing with maps