Creating a Date Range Picker with Phoenix LiveView
Tags: elixir, liveview, opensource, phoenix
Published: 2024-02-05 17:26:10

> **Tl;dr**: You can use the full [DateRangePicker component here](https://github.com/krodante/liveview_date_range_picker). Please please PLEASE submit PRs and help me improve it!

Phoenix has built-in support for Tailwind, and the Tailwind forms plugin has a built-in datepicker when you set `<input type="date">`. With just a few lines of code you can have a beautiful, fully functional datepicker for your forms. What more could anyone want?!?

"But Kathy," you say, "My project requires a range of dates. I need to select more than one!"

You're in luck! You could just… add another input field!!! Then you can use two dates for a range!!!

---
---
---

That's it. That's the post.

---

---

---

---

---

---

---

---

---

---

---

---

---

---

---

---


![dog laughing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4jj58j9zshvwrminydvc.png)

#### JUST KIDDING!!!

I tried that approach, and while it functioned properly and I was able to save multiple dates in a database for a range, the process doubled the number of clicks and cluttered forms.

A great goblin in World of Warcraft once said, "time is money, friend,"[^1] and I don't want anyone to feel frustrated with the ever-increasing micro-aggression of having to click through TWO datepicker components when it could be put in a single field. I also love Elixir and LiveView and wanted to work on a fun little project that would make people happy. Even better: after the obligatory initial Google research I couldn't find much information on making a date _range_ picker in LiveView. Now I can share my learnings with everyone!! Wins all around!!!

## Getting Started
This post assumes that you have [Elixir](https://elixir-lang.org/install.html), [Phoenix](https://hexdocs.pm/phoenix/installation.html), and [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/0.19.0/installation.html) [^2] installed on your system.

> **Note:**
> I will not be adding aliases during this post unless they are already available in generators. While those are great for cleaning up code, they can be confusing in tutorials and people might see errors if they copy-paste a line of code without the alias. I'll add a Clean Up section at the end to show different ways to improve what we're working on.

Like any good tutorial post, let's create a project to show more concrete examples of what we want to do. We're going to create an Event Management System!

That sounds super fancy, but really we're just going to have a form with two input fields: `name` and `date_range`.

We'll set up the infrastructure of our project with this phoenix generator:

```bash
mix archive.install hex phx_new
mix phx.new date_range_picker
cd date_range_picker/
mix phx.gen.live Events Event events name:string start_date:datetime end_date:datetime
```

So fancy!

Our generator will make lots of files for us (many of which we won't need) and give us some handy instructions on how to set up our routes. Replace `get "/", PageController, :home` in the `router.ex` file with our live routes:

```elixir
# lib/date_range_picker_web/router.ex

  scope "/", DateRangePickerWeb do
    pipe_through :browser

    live "/", EventLive.Index, :index
    live "/new", EventLive.Index, :new
    live "/:id/edit", EventLive.Index, :edit
  end
```

Then we run `mix ecto.setup` to set up the events database table. Now we can start our server with `iex -S mix phx.server` and see our super fancy Event Management System!!
 
![initial form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uii92d0v7504yk08ct8u.png)

Wooooooowwwwww!!!! :heart_eyes:

So, who doesn't love single page apps?! We're going to be super cool and have all of our functionality on this main page!!

In `lib/date_range_picker_web/live/event_live/index.ex`, replace the default `defp apply_action(socket, :index, _params) do` with:

```elixir
# lib/date_range_picker_web/live/event_live/index.ex

defp apply_action(socket, :index, _params) do
  socket
  |> assign(:page_title, "Listing Events")
  |> assign(:event, %Event{})
end
```

Then in the `index.html.heex` file, use the pre-built `FormComponent` instead of the "New Event" button:

```elixir
# lib/date_range_picker_web/live/event_live/index.html.heex

<.header>
  Listing Events
  <.live_component
    id="event_form"
    action={:new}
    event={@event}
    title="Event"
    module={DateRangePickerWeb.EventLive.FormComponent}
  />
</.header>
```


![replace new button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zkpqza8u1hufgvgbxu27.png)

Now open up the `FormComponent`. We don't want to push our changes to a different route, so remove `|> push_patch(to: socket.assigns.patch)` in `save_event(socket, :new, event_params)`

```elixir
# lib/date_range_picker_web/live/event_live/form_component.ex

defp save_event(socket, :edit, event_params) do
  case Events.update_event(socket.assigns.event, event_params) do
    {:ok, event} ->
      notify_parent({:saved, event})

      {:noreply,
        socket
        |> put_flash(:info, "Event updated successfully")}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign_form(socket, changeset)}
  end
end
```


![form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ap0i77fkiv4hp42nhd4u.png)
 

Neat! We have a working form! :sparkles:

## The Calendar

"But Kathy," you say. "I thought this post was about how to create a Date Range Picker with LiveView. This form is boring."

It sure is! And now that we have our form with our two datepicker inputs, we can really feel the struggle of having to go through all those clicks just to create an event. I don't know about you, but I hated every second of testing that form :weary:

We're also going to do something REALLY crazy and create the `DateRangePicker` as a universal component. And then add that component to the `FormComponent`. That's right. Nested components in a form. Buckle up :smirk:

The `FormComponent` contains logic that is specific to the `Event` module. That's fine for smaller applications, but in general you want to add generic components wherever possible. We'll likely want to use a generic `.date_range_picker` anywhere in the app, so it should work with any module.

We'll start by adding a new file called `date_range_picker.ex` in the `lib/date_range_picker_web/components` directory.


```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

defmodule DateRangePickerWeb.Components.DateRangePicker do
  use DateRangePickerWeb, :live_component

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      Cool things coming!
    </div>
    """
  end
  
end
```

We'll keep Start Date as the default datepicker for reference and we'll have our End Date field use our fancy new component. Replace the `.simple_form` block with the following:

```elixir
# lib/date_range_picker_web/live/event_live/form_component.ex

<.simple_form
  for={@form}
  id="event_form"
  phx-target={@myself}
  phx-change="validate"
  phx-submit="save"
>
  <.input field={@form[:name]} type="text" label="Name" />
  <.input field={@form[:start_date]} type="datetime-local" label="Start date" />
  <.live_component
    module={DateRangePickerWeb.Components.DateRangePicker}
    label="Date Range"
    id={@id}
    form={@form}
    start_date_field={@form[:start_date]}
    end_date_field={@form[:end_date]}
  />

  <:actions>
    <.button phx-disable-with="Saving...">Save Event</.button>
  </:actions>
</.simple_form>
```

![form with datepicker input](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wxb169lo45h8dmihwg0x.png)

This next part is a bit ["draw the owl"](https://pbs.twimg.com/media/Eyhu70pWEAgawSg?format=jpg&name=small) because we'll be creating the calendar section of the form. I closely followed the [FullstackPhoenix Calendar Guide](https://fullstackphoenix.com/tutorials/create-a-celander-in-phoenix-liveview) for this implementation, and I HIGHLY recommend checking out that post (and their other posts!) for more detailed information

Feel free to copy-paste this full code snippet into the `date_range_picker.ex` file:

```elixir
defmodule DateRangePickerWeb.Components.DateRangePicker do
  use DateRangePickerWeb, :live_component
    
  @week_start_at :sunday
  
  @impl true
  def render(assigns) do
    ~H"""
    <div class="date-range-picker">
      <div class="fake-input-tag relative">
        <.input name={"#{@id}_display_value"} type="text" value="" />
      </div>
  
      <div id={"#{@id}_calendar"} class="absolute z-50 w-72 shadow">
        <div id="calendar_background" class="w-full bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-3">
          <div id="calendar_header" class="flex justify-between text-gray-900">
            <div id="button_left">
              <button type="button" class="p-1.5 text-gray-400 hover:text-gray-500">
                <.icon name="hero-arrow-left" />
              </button>
            </div>
   
            <div id="current_month_year" class="self-center">
              <%= Calendar.strftime(@current_date, "%B %Y") %>
            </div>
   
            <div id="button_right">
              <button type="button" class="p-1.5 text-gray-400 hover:text-gray-500">
                <.icon name="hero-arrow-right" />
              </button>
            </div>
          </div>
   
          <div id="click_today" class="text-gray-500 text-sm text-center cursor-pointer">
            Today
          </div>
            
          <div id="calendar_weekdays" class="text-center mt-6 grid grid-cols-7 text-xs leading-6 text-gray-500">
            <div :for={week_day <- List.first(@week_rows)}>
              <%= Calendar.strftime(week_day, "%a") %>
            </div>
          </div>
  
          <div id="calendar_days" class="isolate mt-2 grid grid-cols-7 gap-px text-sm">
            <button :for={day <- Enum.flat_map(@week_rows, fn week -> week end)} type="button">
              <time class="mx-auto flex h-6 w-6 items-center justify-center rounded-full" datetime={Calendar.strftime(day, "%Y-%m-%d")}>
                <%= Calendar.strftime(day, "%d") %>
              </time>
            </button>
          </div>
        </div>
      </div>
    </div>
    """
  end

  @impl true
  def mount(socket) do
    current_date = Date.utc_today()
    
    {
      :ok,
      socket
      |> assign(:current_date, current_date)
      |> assign(:week_rows, week_rows(current_date))
    } 
  end

  defp week_rows(current_date) do
    first =
      current_date
      |> Date.beginning_of_month()
      |> Date.beginning_of_week(@week_start_at)

    last =
      current_date
      |> Date.end_of_month()
      |> Date.end_of_week(@week_start_at)

    Date.range(first, last)
    |> Enum.map(&(&1))
    |> Enum.chunk_every(7)
  end
end
```

And now we have:


![form with calendar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nofuq0ehdh1lxra2vgje.png)

Ooooookay that's a lot of stuff. Again, if you'd like more details about everything going on here please check out the [FullstackPhoenix Calendar Guide](https://fullstackphoenix.com/tutorials/create-a-celander-in-phoenix-liveview), otherwise we'll continue to adding click events and making the calendar more interactive.

Cool! So! We have a calendar! And it looks pretty awesome. So let's have it do some stuff!

We'll start with changing the months, and we'll _handle_ that with [`handle_event/3`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#c:handle_event/3) (:laughing: jokes! :laughing:)

We've got this button in `<div id="button_left">` that could reeeeally use a [`phx-click`](https://hexdocs.pm/phoenix_live_view/bindings.html#click-events) event. Let's change that div and add `phx-target={@myself} phx-click="prev-month"`:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

<div id="button_left">
  <button type="button" phx-target={@myself} phx-click="prev-month" class="p-1.5 text-gray-400 hover:text-gray-500">
    <.icon name="hero-arrow-left" />
  </button>
</div>
```

Now when the user clicks on the arrow-left icon, LiveView will send a "prev-month" event that we'll need to catch. Let's add a `handle_event/3` under `mount/1`:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def handle_event("prev-month", _, socket) do
  new_date =
    socket.assigns.current_date
    |> Date.beginning_of_month()
    |> Date.add(-1)
  
  {
    :noreply,
    socket
    |> assign(:current_date, new_date)
    |> assign(:week_rows, week_rows(new_date))
  }
end
```

Go back to your form, click the left arrow, and OHMYGOSHWOW it switches to the previous month!! Let's do the same for "next-month" and click all over the place!! :tada: :tada: :tada:

Add `phx-target={@myself} phx-click="next-month"` to the "button_right" div:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

<div id="button_right">
  <button type="button" phx-target={@myself} phx-click="next-month" class="p-1.5 text-gray-400 hover:text-gray-500">
    <.icon name="hero-arrow-right" />
  </button>
</div>
```

Then handle the "next-month" event:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def handle_event("next-month", _, socket) do
  new_date =
    socket.assigns.current_date
    |> Date.end_of_month()
    |> Date.add(1)
  
  {
    :noreply,
    socket
    |> assign(:current_date, new_date)
    |> assign(:week_rows, week_rows(new_date))
  }
end
```

While we're at it, let's have that "Today" link reset the calendar back to the current month. As before, add `phx-target={@myself} phx-click="today"` to "click_today":

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

<div id="click_today" phx-target={@myself} phx-click="today" class="text-gray-500 text-sm text-center cursor-pointer">
  Today
</div>
```

Aaaaaand handle the "today" event:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def handle_event("today", _, socket) do
  current_date = Date.utc_today()
  
  {
    :noreply,
    socket
    |> assign(:current_date, current_date)
    |> assign(:week_rows, week_rows(current_date))
  }
end
```

And now you can switch months! So exciting!! LiveView is the best!! :sparkling_heart:

Let's do a bit more work on the UX before we move on to actually saving the form fields. We'll start with having the calendar hidden at first and then shown on click.

To start off with the calendar hidden we'll add a `:calendar?` assign as a boolean in `mount/1`:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def mount(socket) do
  current_date = Date.utc_today()
    
  {
    :ok,
    socket
    |> assign(:calendar?, false)
    |> assign(:current_date, current_date)
    |> assign(:week_rows, week_rows(current_date))
  } 
end
```

And then we'll check for that `@calendar?` assign in the main calendar div:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

<div :if={@calendar?} id={"#{@id}_calendar"} class="absolute z-50 w-72 shadow">
```

Now when you refresh the page, you won't see the calendar. Next we'll add a `phx-click` event to that "fake-input-tag" and open the calendar with `phx-click="open-calendar" phx-target={@myself}`:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

<div class="fake-input-tag relative" phx-click="open-calendar" phx-target={@myself}>
```

And we'll add the event handler for it near our other `handle_event/3` functions:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def handle_event("open-calendar", _, socket) do
  {:noreply, socket |> assign(:calendar?, true)}
end
```

Yay! It opens!! But... now we can't close it. Thankfully that's super easy with the `phx-click-away` event. We can add that to our main calendar div (where we added `:if={@calendar?}` so that the calendar will close whenever we click anywhere outside of it.

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

<div :if={@calendar?} id={"#{@id}_calendar"} class="absolute z-50 w-72 shadow" phx-click-away="close-calendar" phx-target={@myself}>
```

Add the event handler:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def handle_event("close-calendar", _, socket) do
  {:noreply, socket |> assign(:calendar?, false)}
end
```

E voila! Your calendar now opens and closes! :muscle:

Next I'd like to add some styling for the current selected date and hovering over potential dates. This part involves a lot of [Tailwind CSS hover magic](https://tailwindcss.com/docs/hover-focus-and-other-states), so I won't go too much into detail on the implementation. We'll be looking at the "calendar_days" div and adding some dynamic classes:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

<button
  :for={day <- Enum.flat_map(@week_rows, &(&1))}
  type="button"
  class={[
    "calendar-day overflow-hidden py-1.5 h-10 w-auto focus:z-10 w-full",
    today?(day) && "font-bold border border-black",
    "hover:bg-blue-300 hover:border hover:border-black",
    other_month?(day, @current_date) && "text-gray-400"
  ]}
>
```

And then we'll add those `today?/1` and `other_month?/2` helper functions near the bottom of the `date_range_picker.ex` file:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

defp today?(day), do: day == Date.utc_today()

def other_month?(day, current_date) do
  Date.beginning_of_month(day) != Date.beginning_of_month(current_date)
end
```

Look!!! Styles and hovering!!!! :heart_eyes:

Days that are outside of the current month are a lighter gray, the current date has a border, and any date you hover over has a blue background! Yay team! :clap:

![prettier calendar ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dylwj5afxsak3frp7o4n.png)

## Persisting Data

Now to the exciting part: actually saving the dates!! We need to hook up the `@form` assign from the `FormComponent` to the `DateRangePicker` component in order to change that form data.

Back in `FormComponent`, add the `form`, `start_date_field`, and `end_date_field` arguments the `.live_component` function:

```elixir
# lib/date_range_picker_web/live/event_live/form_component.ex

<.live_component
  module={DateRangePickerWeb.Components.DateRangePicker}
  label="Date Range"
  id="date-range-picker"
  form={@form}
  start_date_field={@form[:start_date]}
  end_date_field={@form[:end_date]}
/>
```

Then back in `DateRangePicker`, initialize those new attributes as assigns in `mount/1` and then populate them in a new [`update/2`](https://hexdocs.pm/phoenix_live_view/0.20.4/Phoenix.LiveComponent.html#c:update/2) callback. `range_start` and `range_end` will be the dates we use in the calendar. We'll update them in `update/2` with the values from the `start_date_field` and `end_date_field` in the form.

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def mount(socket) do
  current_date = Date.utc_today()

  {
    :ok,
    socket
    |> assign(:calendar?, false)
    |> assign(:current_date, current_date)
    |> assign(:range_start, nil)
    |> assign(:range_end, nil)
  }
end

@impl true
def update(assigns, socket) do
  range_start = assigns.start_date_field.value
  range_end = assigns.end_date_field.value
  current_date = socket.assigns.current_date

  {
    :ok,
    socket
    |> assign(assigns)
    |> assign(:current_date, current_date)
    |> assign(:range_start, range_start)
    |> assign(:range_end, range_end)
  }
end
```

Next we'll add a click event to the date button that will select a date. Add the attributes `phx-target={@myself} phx-click="pick-date" phx-value-date={Calendar.strftime(day, "%Y-%m-%d") <> "T00:00:00Z"}` to the date button:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

<button
  :for={day <- Enum.flat_map(@week_rows, &(&1))}
  type="button"
  phx-target={@myself}
  phx-click="pick-date"
  phx-value-date={Calendar.strftime(day, "%Y-%m-%d") <> "T00:00:00Z"}
  class={[
    "calendar-day overflow-hidden py-1.5 h-10 w-auto focus:z-10 w-full",
    today?(day) && "font-bold border border-black",
    "hover:bg-blue-300 hover:border hover:border-black",
    other_month?(day, @current_date) && "text-gray-400"
  ]}
>
```

And this, my friends, is where we get fancy with state machines! :raised_hands:

Date ranges are tricky because we can't handle just ANY click; we need to know if it's the first click (start date), second click (end date), or third click (reset start date). Thankfully we don't have too many states to track, so it's not too complicated. Many many thanks to my wonderful colleague [Dmitry Doronin](https://www.linkedin.com/in/ddoronin/) for this suggestion!! My previous implementation was full of very complex `case` statements, and he made it so much simpler!! :star:

Let's go back up to the top of the `DateRangePicker` and define our states. `:set_start` is followed by `:set_end`, `:set_end` is followed by `:reset`, and `:reset` goes back to `:set_start`:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@fsm %{
  set_start: :set_end,
  set_end: :reset,
  reset: :set_start
}
@initial_state :set_start
```

Then we need to go back to `update/1` and set the `:state` assign:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def update(assigns, socket) do
  range_start = assigns.start_date_field.value
  range_end = assigns.end_date_field.value
  current_date = socket.assigns.current_date

  {
    :ok,
    socket
    |> assign(assigns)
    |> assign(:current_date, current_date)
    |> assign(:range_start, range_start)
    |> assign(:range_end, range_end)
    |> assign(:state, @initial_state)
  }
end
```

Before we forget, let's also make sure to reset the state when we close the calendar by adding the `:state` assign in the `close-calendar` event handler:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def handle_event("close-calendar", _, socket) do
  {
    :noreply, 
    socket 
    |> assign(:calendar?, false)
    |> assign(:state, @initial_state)
  }
end
```

Now we can work on our new `pick-date` event!

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def handle_event("pick-date", %{"date" => date}, socket) do
  {:ok, date_time, _} = DateTime.from_iso8601(date)

  ranges = calculate_date_ranges(socket.assigns.state, date_time)
  state = @fsm[socket.assigns.state]

  {
    :noreply,
    socket
    |> assign(ranges)
    |> assign(:state, state)
  }
end

defp calculate_date_ranges(:set_start, date_time) do
  %{
    range_start: date_time,
    range_end: nil
  }
end

defp calculate_date_ranges(:set_end, date_time), do: %{range_end: date_time}

defp calculate_date_ranges(:reset, _date_time) do
  %{
    range_start: nil,
    range_end: nil
  }
end
```

Oooooookay. So. What's going on here?? :dizzy_face:

In `handle_event/3` we get the date as a String value from the `phx-click` event. We convert that string to a DateTime type because we need to save the date as a DateTime for our postgresql database. Then we use some `calculate_date_ranges/2` helper functions to set the `:range_start` and `:range_end` assigns. These helper functions allow us to manage the state of clicks:

* If the state is `:set_start`, we only set the `range_start` assign.
* If the state is `:set_end`, we already have the `range_start` assigned from the previous click, so we only set `range_end`.
* If the state is `:reset`, we set both assigns back to `nil`.

After we set the ranges, we use our `@fsm` (finite state machine) to re-assign the `:state` and indicate what the next step will be.

---

"But Kathy," you say, "this is really cool and all, but what about, you know, actually saving the date???"

Great question! We're going to add that functionality to our `"close-calendar"` event! If you thought adding a state machine was tricky, be prepared to level up and send updates across LiveViews and LiveComponents! 

I'll start with the new `"close-calendar"` event and then go through each line:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

@impl true
def handle_event("close-calendar", _, socket) do
  attrs = %{
    id: socket.assigns.id,
    start_date: socket.assigns.range_start,
    end_date: socket.assigns.range_end,
    form: socket.assigns.form
  }

  send(self(), {:updated_event, attrs})

  {
    :noreply,
    socket
    |> assign(:calendar?, false)
    |> assign(:end_date_field, set_field_value(socket.assigns, :end_date_field, attrs.end_date))
    |> assign(:start_date_field, set_field_value(socket.assigns, :start_date_field, attrs.start_date))
    |> assign(:state, @initial_state)
  }
end
```

We'll need to update the original `:form` assign in the `FormComponent` in order to record the new dates. To do that, we need to notify the `EventLive.Index` LiveView of the changes. First we make a Map of the attributes we want to send, and then we call `send(self(), {:updated_event, attrs})` to send them to `EventLive.Index`. This [`send`](https://hexdocs.pm/elixir/Kernel.html#send/2) function sends a message to the process that is running `EventLive.Index` with an "address" or "topic" of `:updated_event` along with the attributes we want to update.

Now we need to handle that message in `EventLive.Index`:

```elixir
# lib/date_range_picker_web/live/event_live/index.ex

@impl true
def handle_info({:updated_event, attrs}, socket) do
  event = socket.assigns.event
  form = attrs.form

  new_form =
    Phoenix.HTML.FormData.to_form(
      %{
        "start_date" => attrs.start_date,
        "end_date" => attrs.end_date,
        "name" => form.params["name"]
      },
      id: form.id
    )

  updated_socket =
    socket
    |> assign(:event, event)
    |> assign(:id, attrs.id)
    |> assign(:new_form, new_form)

  send_update(
    DateRangePickerWeb.EventLive.FormComponent,
    updated_socket.assigns
    |> Map.delete(:flash)
    |> Map.delete(:streams)
  )

  {:noreply, updated_socket}
end
```

This [`handle_info/2`](https://hexdocs.pm/phoenix_live_view/0.20.4/Phoenix.LiveView.html#c:handle_info/2) callback will catch the `:updated_event` message along with the attributes we defined in `DateRangePicker`. We update the socket with the event, the form ID, and the new `%Form` struct. Then we call [`send_update/3`](https://hexdocs.pm/phoenix_live_view/0.20.4/Phoenix.LiveView.html#send_update/3) to the `FormComponent` to re-render the form with the new fields. **Note**: `:flash` and `:streams` are automatically assigned when calling `send/2`, but we don't want to use those in `FormComponent` so we remove them here.

Back in our `FormComponent`, we need to change our `update/2` callback to use the new form data if it exists in the assigns. We can add some `new_form_changeset/2`helper functions to make that look a bit cleaner:

```elixir
# lib/date_range_picker_web/live/event_live/form_component.ex

@impl true
def update(%{event: event} = assigns, socket) do
  changeset = new_form_changeset(event, assigns)

  {
    :ok,
    socket
    |> assign(assigns)
    |> assign_form(changeset)
  }
end

defp new_form_changeset(event, assigns) when is_map_key(assigns, :new_form) do
  Events.change_event(event, assigns.new_form.params)
end

defp new_form_changeset(event, _assigns) do
  Events.change_event(event)
end
```

For the very last part we need to update the HTML in the `DateRangePicker` component and `FormComponent` to set the form fields appropriately.

Remove `<.input field={@form[:start_date]} type="datetime-local" label="Start date" />` from `FormComponent` so that the `.simple_form` block only has the "Name" `.input` field and the `.live_component` field:

```elixir
# lib/date_range_picker_web/live/event_live/form_component.ex

<.simple_form
  for={@form}
  id="event_form"
  phx-target={@myself}
  phx-change="validate"
  phx-submit="save"
>
  <.input field={@form[:name]} type="text" label="Name" />
  <.live_component
    module={DateRangePickerWeb.Components.DateRangePicker}
    label="Date Range"
    id={@id}
    form={@form}
    start_date_field={@form[:start_date]}
    end_date_field={@form[:end_date]}
  />

  <:actions>
    <.button phx-disable-with="Saving...">Save Event</.button>
  </:actions>
</.simple_form>
```

Add the "start_date" and "end_date" `.input` fields to the `DateRangePicker` component instead as hidden inputs, and change the value of the "fake-input-tag" to show the selected range:

```elixir
# lib/date_range_picker_web/components/date_range_picker.ex

<.input field={@start_date_field} type="hidden" />
<.input field={@end_date_field} type="hidden" />
<div class="fake-input-tag relative" phx-click="open-calendar" phx-target={@myself}>
  <.input
    name={"#{@id}_display_value"}
    type="text"
    label={@label}
    value={"#{@range_start} - #{@range_end}"}
  />
</div>
```

With all of that hooked up correctly, you should be able to use your form and see both dates!

![Working form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fwjop9n8znp7mqrjbma4.png)

## Cleaning up

There is A LOT more we can do with this, but you now have a very basic, functioning date range picker for your forms!! Congratulations!!

I have a more full-feature version of this component available for public use on my [github](https://github.com/krodante/liveview_date_range_picker). Full features include:

* indicate that the input is range or a single date to use it as a range picker or a single date picker
* styling for hovering over potential date ranges
* add `.date_range_picker` and `.date_picker` functions in `CoreComponents` that can be used in forms instead of calling `.live_component`
* support for a "minimum" date, ensuring that the start date cannot be before the minimum date.



[^1]:
  "[Time is money, friend](https://youtu.be/mLcbzp-rGWI?t=2)"
[^2]:
  LiveView is included in Phoenix versions 1.5+, so you won't need to explicitly install it if you're using a newer version of Phoenix.