Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Making Invalid States Invalid in LiveView

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
Tweet

More Decks by German Velasco

Other Decks in Technology

Transcript

  1. OTHER NAMES ▸ Making invalid states unrepresentable ▸ Making illegal

    states unrepresentable ▸ Making impossible states impossible
  2. ASSIGNING VALUES def mount(_, _, socket) do { :ok, socket

    |> assign(:direction, "one-way") |> assign(:departure, Date.utc_today()) |> assign(:return, nil) } end
  3. ASSIGNING VALUES def mount(_, _, socket) do { :ok, socket

    |> assign(:direction, "one-way") |> assign(:departure, Date.utc_today()) |> assign(:return, nil) } end
  4. 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>
  5. 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>
  6. 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>
  7. 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>
  8. HANDLING THE EVENT def handle_event("book", %{"booking" => params}, socket) do

    {:ok, message} = FlightBooker.book_trip(params) socket |> put_flash(:info, message) |> noreply() end
  9. HANDLING THE EVENT def handle_event("book", %{"booking" => params}, socket) do

    {:ok, message} = FlightBooker.book_trip(params) socket |> put_flash(:info, message) |> noreply() end
  10. 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
  11. 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
  12. 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
  13. ! THE BUGS ▸ ! One-way flight that got charged

    for a return flight ▸ " Two-way flight without a return flight
  14. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  15. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  16. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  17. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  18. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  19. 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}
  20. 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}
  21. 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}
  22. 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}
  23. 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}
  24. 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()}
  25. A MORE constrained FLIGHT BOOKER DOMAIN MODEL ▸ One-way flight

    with departure date ▸ OR ▸ Two-way flight with departure and return dates
  26. A MORE constrained FLIGHT BOOKER DOMAIN MODEL @type flight_booker ::

    {:one_way, Date.t()} | {:two_way, Date.t(), Date.t()}
  27. A MORE constrained FLIGHT BOOKER DOMAIN MODEL @type flight_booker ::

    {:one_way, Date.t()} | {:two_way, Date.t(), Date.t()}
  28. A MORE constrained FLIGHT BOOKER DOMAIN MODEL @type flight_booker ::

    {:one_way, Date.t()} | {:two_way, Date.t(), Date.t()}
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 1. RENDER COMPLEX TYPE def mount(_, _, socket) do today

    = Date.utc_today() booker = FlightBooker.one_way(today) {:ok, assign(socket, :booker, booker)} end
  36. 1. RENDER COMPLEX TYPE def mount(_, _, socket) do today

    = Date.utc_today() booker = FlightBooker.one_way(today) {:ok, assign(socket, :booker, booker)} end
  37. 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>
  38. 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>
  39. 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>
  40. 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>
  41. 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>
  42. 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>
  43. 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>
  44. 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>
  45. TRANSFORM USER INPUT THROUGH PARSING ▸ less structured data |>

    more structured data ▸ raw data |> valid domain
  46. TRANSFORM USER INPUT THROUGH PARSING ▸ less structured data |>

    more structured data ▸ raw data |> valid domain ▸ ⚠ |>
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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()}
  59. (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}
  60. (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}
  61. # 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}
  62. 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]}
  63. "Please select a valid return date" %{ direction: "one-way", departure:

    Date.t(), return: nil, errors: [:return] } ▸ !
  64. 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]}
  65. 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]}
  66. 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]}
  67. 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]}
  68. 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]}
  69. 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}
  70. 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}
  71. 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}
  72. 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}
  73. 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}
  74. 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()}}
  75. 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()}}
  76. 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()}}
  77. 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>
  78. 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>
  79. 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>
  80. 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>
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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
  95. 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
  96. 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}
  97. 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}
  98. 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}
  99. 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}
  100. 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}
  101. 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}
  102. 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}
  103. 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
  104. 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
  105. 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
  106. 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]}
  107. 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
  108. 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
  109. 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
  110. 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
  111. !

  112. WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms ▸

    Great casting helpers ▸ Great validation helpers
  113. WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms ▸

    Great casting helpers ▸ Great validation helpers ▸ Use schemaless changesets as an anti-corruption layer
  114. COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way,

    date_or_error} | {:two_way, date_or_error, date_or_error}
  115. COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way,

    date_or_error} | {:two_way, date_or_error, date_or_error}
  116. COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way,

    Ecto.Changeset.t()} | {:two_way, Ecto.Changeset.t()}
  117. 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
  118. 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
  119. 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
  120. 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
  121. 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 %>
  122. 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 %>
  123. 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 %>
  124. 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 %>
  125. 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 %>
  126. 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 %>
  127. 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 %>
  128. 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 %>
  129. 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
  130. 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
  131. 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
  132. 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
  133. 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
  134. 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
  135. 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
  136. 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
  137. 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
  138. 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
  139. ! EXPLORATION NOTES ▸ Model your domain to remove invalid

    states ▸ ! Render complex types Declaratively & Exhaustively
  140. ! EXPLORATION NOTES ▸ Model your domain to remove invalid

    states ▸ ! Render complex types Declaratively & Exhaustively ▸ Parse raw ⚠ unsafe values into your domain values
  141. ! 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
  142. ! 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