DEV Community

Cover image for Building a simple Calendly clone with Phoenix LiveView (pt. 3)
Ricardo García Vega
Ricardo García Vega

Posted on • Updated on • Originally published at bigardone.dev

Building a simple Calendly clone with Phoenix LiveView (pt. 3)

In the last part of the series, we generated a new Phoenix project and made the necessary changes to support Tailwind CSS. We also defined our domain models, consisting of event types and events, generating their migration files and Ecto schemas. Finally, we populated the database with three event types using the seeds file. In this part we will start building the public part of our application, in which a visitor will select one of them, a date, and a starting time, to schedule an event with us. More precisely, we will focus on the event type selection page, taking advantage of two new LiveView features:

  • Live sessions.
  • Function components.

Let's get cracking!

But before, let's recall how LiveView works

If you are already familiar with LiveView and how it works, you can skip this part and jump to the next section. Otherwise, hold your horses and read this section before continuing, since understanding how LiveView works internally will help you a lot while coding. Any LiveView begins as a regular HTTP request with a standard HTML response. When the initial HTML response renders in the browser, LiveView's JS client opens a Phoenix socket connection between the page and the application. This socket connection is nothing more than a process that stores a state and receives messages to update this state. Every time its internal state (a.k.a assigns) changes, LiveView re-rerenders the relevant parts of its HTML, pushing back the changes through the socket to the browser, where the JS client efficiently applies the changes to the DOM. The most remarkable thing about this is that LiveView guarantees a first HTML response render, whether JavaScript is enabled or not, which is very convenient for indexing, SEO, etc.

The public live session

Live session is one of the new features added by LiveView. It defines a group of live routes that can handle navigation between them through the socket without any additional HTTP request to the server. It can share the same root layout and list of hooks to attach to the mount lifecycle of the LiveView. Very handy when you need to assign the same data to the socket over and over within a group of live views. In our case, as the owners of the calendar, we want to display our name to the visitor. Let's add it to our application configuration:

# ./config/config.exs

import Config

config :calendlex,
  # ...
  owner: %{
    name: "Bigardone"
  }

# ...
Enter fullscreen mode Exit fullscreen mode

Now let's create the :public live session in the router file:

# ./lib/calendlex_web/router.ex

defmodule CalendlexWeb.Router do
  use CalendlexWeb, :router

  # ...

  live_session :public, on_mount: CalendlexWeb.Live.InitAssigns do
    scope "/", CalendlexWeb do
      pipe_through :browser

      live "/", PageLive
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

To assign the :owner configuration to the socket of all the live views within the :public live session, we will use the new module specified in the :on_mount option. Let's create it:

# ./lib/calendlex_web/live/init_assigns.ex

defmodule CalendlexWeb.Live.InitAssigns do
  import Phoenix.LiveView

  def on_mount(:default, _params, _session, socket) do
    owner = Application.get_env(:calendlex, :owner)
    socket = assign(socket, :owner, owner)

    {:cont, socket}
  end
end
Enter fullscreen mode Exit fullscreen mode

Live session hooks must implement the on_mount callback, which receives the identifier of the hook (to use pattern matching in case we want to define multiple versions), the public parameters, the session, and the socket. We get the owner's data from the application's configuration, assign it to the socket, and return {:cont, socket} to continue with LiveView's flow. With the owner's data available on every live view within the public session, we can move on and implement our first live view.

The event type selection page

This page will get rendered in the application's root path, and it will list all the available event types, letting the user select one, which will trigger a redirection to the next page.

Let's start by editing the PageLive module we created in the previous part:

# ./lib/calendlex_web/live/page_live.ex

defmodule CalendlexWeb.PageLive do
  use CalendlexWeb, :live_view

  # We will implement this module in a minute...
  alias CalendlexWeb.Components.EventType

  def mount(_params, _session, socket) do
    event_types = Calendlex.available_event_types()

    {:ok, assign(socket, event_types: event_types), temporary_assigns: [event_types: []]}
  end
end
Enter fullscreen mode Exit fullscreen mode

When a LiveView gets rendered, the mount/3 callback is invoked, and it accepts the private session and some public params. In this callback, we can fetch the necessary data we want to render. Therefore, we are getting all the available event types from the database and assigning them to the socket to render them in the template. We are also returning the temporary_assigns option, which sets the event_types assign to an empty list after rendering the template, preventing possible memory issues when having big lists of items. Calendlex.available_event_types/0 does not exist yet, so let's go ahead and implement it:

# ./lib/calendlex.ex

defmodule Calendlex do
  defdelegate available_event_types, to: Calendlex.EventType.Repo, as: :available
end
Enter fullscreen mode Exit fullscreen mode

We will use the Calendlex module as the public interface between the CalendlexWeb.* and Calendlex.* namespaces. This way, the presentation layer, or CalenlexWeb.*, does not have to know any implementation details or internals of the business logic, or Calendlex.*. The module exposes an available_event_types/0 function which delegates to the proper internal module in charge of doing any CRUD action related to event types, the Calendlex.EventType.Repo. Let's go ahead and create this module:

# ./lib/calendlex/event_type/repo.ex

defmodule Calendlex.EventType.Repo do
  alias Calendlex.{EventType, Repo}
  import Ecto.Query, only: [order_by: 3]

  def available do
    EventType
    |> order_by([e], e.name)
    |> Repo.all()
  end
end
Enter fullscreen mode Exit fullscreen mode

The available function is pretty straightforward. It gets all the event types from the database ordered by name. We have two different alternatives to render them in the PageLive live view. One is by implementing the render/1 callback function in the same LiveView module, and the other is creating a new template file, like we did in the previous part. I usually prefer the second option, so let's go ahead and modify the template file:

# ./lib/calendlex_web/live/page_live.html.heex

<section class="w-1/2 mx-auto">
  <div class="p-6 mb-2 bg-white border border-gray-200 shadow-md rounded-md">
    <header class="w-2/5 mx-auto mb-12 text-center">
      <h1 class="mb-5 text-xl font-semibold text-gray-500"><%= @owner.name %></h1>
      <p class="text-gray-500">Welcome to my scheduling page. Please follow the instructions to add an event to my calendar.</p>
    </header>
    <div class="mt-4 grid grid-cols-2 gap-x-6">
      <%= for event_type <- @event_types do %>
        <EventType.selector event_type={event_type} path={Routes.live_path(@socket, CalendlexWeb.EventTypeLive, event_type.slug)} />
      <% end %>
    </div>
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode

We refer to the owner's name value, <%= @owner.name %>, previously assigned in the CalendlexWeb.Live.InitAssigns.on_mount/4 hook. To render the available event types, we go through the list of element_types assigned to the socket, and we invoke one of the new features added to LiveView, function components. Thanks to the new HEEx HTML engine introduced by LiveView 0.16, we can now invoke these components using regular HTML tags, which is very convenient and reminds me of React.

The EventType selector function component

Function components, or stateless components, are regular functions that must receive an assigns parameter and return a ~H sigil with the HTML to render. They can't handle any messages, or hold any internal state whatsoever. Let's create the component's module:

# ./lib/calendlex_web/live/components/event_type.ex

defmodule CalendlexWeb.Components.EventType do
  use Phoenix.Component

  def selector(assigns) do
    ~H"""
    <%= live_redirect to: @path do %>
      <div class="flex items-center p-6 pb-20 text-gray-400 bg-white border-t border-gray-300 cursor-pointer hover:bg-gray-200 gap-x-4">
        <div {[class: "inline-block w-8 h-8 #{@event_type.color}-bg rounded-full border-2 border-white"]}></div>
        <h3 class="font-bold text-gray-900"><%= @event_type.name %></h3>
        <div class="ml-auto text-xl"><i class="fas fa-caret-right"></i></div>
      </div>
    <% end %>
    """
  end
end
Enter fullscreen mode Exit fullscreen mode

Since we are calling EventType.event_type, from the PageLive template, setting the event_type and path attributes, these values are automatically assigned and available in the ~H sigil.
If we check the terminal, we should see the following error, caused by the path value which corresponds to an unexisting live path value:

[error] #PID<0.564.0> running CalendlexWeb.Endpoint (connection #PID<0.555.0>, stream id 4) terminated
Server: localhost:4000 (http)
Request: GET /
** (exit) an exception was raised:
    ** (ArgumentError) no action CalendlexWeb.EventTypeLive for CalendlexWeb.Router.Helpers.live_path/3. The following actions/clauses are supported:

    live_path(conn_or_endpoint, CalendlexWeb.PageLive, params \\ [])
Enter fullscreen mode Exit fullscreen mode

To fix the error, let's add the corresponding live path to the router:

# ./lib/calendlex_web/router.ex

defmodule CalendlexWeb.Router do
  use CalendlexWeb, :router

  # ...

  live_session :public, on_mount: CalendlexWeb.Live.InitAssigns do
    scope "/", CalendlexWeb do
      # ...

      live "/:event_type_slug", EventTypeLive
    end
  end
Enter fullscreen mode Exit fullscreen mode

And finally, let's add the corresponding empty LiveView module and template:

# ./lib/calendlex_web/live/event_type_live.ex

defmodule CalendlexWeb.EventTypeLive do
  use CalendlexWeb, :live_view

  def mount(%{"event_type_slug" => _slug} = params, _session, socket) do
    {:ok, socket}
  end
end
Enter fullscreen mode Exit fullscreen mode
<!-- ./lib/calendlex_web/live/event_type_live.html.heex -->

<h1>EventTypeLive</h1>
Enter fullscreen mode Exit fullscreen mode

Jumping back to the browser, we should see the following:

To give the finishing touches to the layout and styling, copy the contents of the main CSS file and paste it into your local version, and replace the content of the root and live layouts with the following:

<!-- ./lib/calendlex_web/templates/layout/root.html.heex -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <%= csrf_meta_tag() %>
    <%= live_title_tag assigns[:page_title] || assigns[:owner][:name], suffix: " · Calendlex" %>
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
    <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
    <script src="https://kit.fontawesome.com/9539f8cd16.js" crossorigin="anonymous"></script>
  </head>
  <body class="antialiased text-gray-600 bg-gray-100">
    <div class="flex flex-col h-screen">
      <%= @inner_content %>
    </div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
<!-- ./lib/calendlex_web/templates/layout/live.html.heex -->

<main role="main" class="flex-1 pt-20">
  <p class="alert alert-info" role="alert"
    phx-click="lv:clear-flash"
    phx-value-key="info"><%= live_flash(@flash, :info) %></p>

  <p class="alert alert-danger" role="alert"
    phx-click="lv:clear-flash"
    phx-value-key="error"><%= live_flash(@flash, :error) %></p>

  <%= @inner_content %>
</main>
Enter fullscreen mode Exit fullscreen mode

After the browser refreshes the page, everything should look much nicer.

And that's it for today. In the following part, we will take care of the EventType live view, rendering the monthly calendar, in which the visitor will be able to select a date and a free time slot to schedule an event with us. We will take advantage of more LiveView's features, such as live components and patching the current navigation. In the meantime, you can check the end result in the live demo, or have a look at the source code.

Happy coding!

GitHub logo bigardone / calendlex

Simple Calendly clone with Phoenix LiveView

Oldest comments (18)

Collapse
 
slowburnaz profile image
Chris Turner

I'm running into an issue with the live_session @owner assignment...

When I start up the server and navigate to the page, I get an error like the following:
[error] #PID<0.521.0> running CalendlexWeb.Endpoint (connection #PID<0.520.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /
** (exit) an exception was raised:
** (ArgumentError) assign @owner not available in template.

Please make sure all proper assigns have been set. If you are
calling a component, make sure you are passing all required
assigns as arguments.

Available assigns: [:changed, :event_types, :flash, :live_action, :socket]

I've made sure all the code matches. Any ideas why? Just seems like the live_session stuff isn't working, or being ignored.

Collapse
 
antonrich profile image
Anton

I think that's because you need to remove this from the router:

  scope "/", CalendlexWeb do
    pipe_through :browser

    live "/", PageLive
  end
Enter fullscreen mode Exit fullscreen mode

But then I think you will ran into the same error I ran into.

Collapse
 
slowburnaz profile image
Chris Turner

That worked for me, thanks.

Fortunately (unfortunately?), I didn't run into your issue.

Thread Thread
 
bigardone profile image
Ricardo García Vega

Hi there! Can you run mix hex.outdated and confirm that you have the latest version of LiveView installed?

Thread Thread
 
slowburnaz profile image
Chris Turner • Edited

Actually, I had noticed that your live_view version was higher, so I did update that and it fixed the @owner assignment issue.

I was also running into another issue with the EventType component:
"function EventType.event_type/1 is undefined (module EventType is not available)" and it also couldn't event find the "EventType" module.

It seems that's supposed to be EventType.selector... so changing that, and then adding "alias Calendlex.Components.EventType" to the page_live.ex file helped.

Thread Thread
 
slowburnaz profile image
Chris Turner

I see that Anton was having the same issue as me, so disregard this.

Collapse
 
bigardone profile image
Ricardo García Vega

Yeah, my bad. I should have specified that live_session should wrap the current scope. I'll rewrite that part, thanks for reporting the issue :)

Collapse
 
antonrich profile image
Anton

I ran into a bit of snag yesterday. Today I started the tutorial again from the beginning and hit the same thing:
function CalendlexWeb.Live.InitAssigns.mount/3 is undefined or private

Collapse
 
bigardone profile image
Ricardo García Vega • Edited

👋🏼 Can you run mix hex.outdated and confirm that you have the latest version of LiveView installed?

Collapse
 
antonrich profile image
Anton

mix hex.outdated screensh
phoenix_live_view 0.16.4 0.17.5
Is the version of live_view an issue here? Seems so, I ran your source code it worked. I also checked it with the hex.outdated command and the live_view version is 0.17.5.
I'll try to update and report back.

Thread Thread
 
bigardone profile image
Ricardo García Vega

Upgrading phoenix_live_view should fix the issue 🤞🏼

Collapse
 
antonrich profile image
Anton

I have updated the version of live_view and it worked! Ricardo thanks for your help.

A couple other things that happened:
First, in /lib/calendlex_web/live/page_live.html.heex we have

<EventType.event_type event_type={event_type} path={Routes.live_path(@socket, CalendlexWeb.EventTypeLive, event_type.slug)} />
Enter fullscreen mode Exit fullscreen mode

Which led me to an error message:
function EventType.event_type/1 is undefined (module EventType is not available)

The solution was just to add CalendlexWeb.Components.EventType.event_type instead of just EventType.event_type

So, my question how did you manage to make it just EventType instead of the whole thing CalendlexWeb.Components.EventType?

Second, and instead of EventType.event_type you have in the source code EventType.selector.

Thread Thread
 
slowburnaz profile image
Chris Turner • Edited

I was having the same issue...

Aliasing EventType in the page_live.ex file does work.

Thread Thread
 
bigardone profile image
Ricardo García Vega

As Chris said, aliasing the component module in your live view should do the trick. Regarding the selector function, it was a last-minute refactor I did. I will pay more attention to these kind of errors in the following parts, sorry for the inconvenience 🙌

Collapse
 
alaadahmed profile image
AlaaDAhmed • Edited

Very nice tutorial thank you very much, I have small note for you :)
in selector function component instead of wrapping the whole classes inside {[class: "inline-block w-8 h-8 ..."]} you can just write it like normal HTML class, just like this class={"inline-block w-8 ..."} so just wrap the double quotes only with {".."}

Collapse
 
bigardone profile image
Ricardo García Vega

Thanks for the note, you are right. I'll change it ASAP, cheers!

Collapse
 
traceyonim profile image
Tracey Onim

Hello Ricardo, Kindly elaborate more on temporary_assigns options for me , how exactly do they work? I have tried to pass it in my application but when I render the page I can't see the list of items assigned since it has been overriden by the empty list passed in temporary_assigns

Collapse
 
juliolinarez profile image
Julio Linarez

Great tutorial!