Conditional rendering of components with Phoenix LiveView
Tags: elixir, liveview, phoenix, webdev
Published: 2021-04-27 15:34:18

When we find a view that has many HTML elements, we usually want to separate them into their components to have a better organization of the code that facilitates its understanding and maintenance, as well as decouple the large logic into smaller pieces, and why not, reuse these components in another view (very much in the style of React.js mental model). This can be easily achieved by using [LiveComponent](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html) from **Phoenix LiveView**.

The problem is that many times these components share some logic between them, and many times they depend on each other to know if it should be rendered or not. This is called conditional rendering, and with **Phoenix LiveView** we can achieve this by handling the conditional logic in the parent **LiveView**, through the **handle_info** callback function.

### Use Case

Suppose we have a view for registering a vehicle in a car workshop. This view is composed of two forms, one for the registration of the vehicle owner and one for the registration of the vehicle itself. Each form has its independent registration view, and so we use a **LiveComponent** for each of these, within which its business logic is handled independently.

![live componets](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/12a0yx9e9ktraic1tidi.png)

These views must be rendered independently and in order:

1. User registration (**customer_component.ex**)
![Register Customer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/apcsxpheptiwq7z43sce.png)

2. Vehicle registration (**vehicle_component.ex**)
![Register Vehicle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ebskxglwjtf0n6xl0t1t.png)

Once the user is registered, the user's *id* must be passed to the vehicle component, so that we can successfully associate the user with the vehicle when we are registering the vehicle.

### How do we achieve this? Let's go to the code...

The conditional logic is handled in the parent view of the user and vehicle components, in this case, it will be the LiveView ***vehicle_live/index.ex*** and its respective file ***vehicle_live/index.html.leex*** where the HTML elements to be rendered are located.

In the file ***vehicle_live/index.ex***:

```elixir
defmodule CarWorkshopWeb.VehicleLive.Index do
  use CarWorkshopWeb, :live_view

  alias CarWorkshop.{Vehicles.Vehicle, Accounts.Customer}

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, customer: %Customer{}, view_to_show: :customer_view)}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "Register Vehicle")
    |> assign(:vehicle, %Vehicle{})
    |> assign(:customer, %Customer{})
  end

  @impl true
  def handle_info({:customer_registered, customer, view_to_show}, socket),
    do: {:noreply, assign(socket, customer: customer, view_to_show: view_to_show)}
end
```

In the **mount** function, we assign to the socket the property *view_to_show*, using this, we will know the component to render in ***vehicle_live/index.html.leex***. We give it a value of *:customer_view*, making the first view to be rendered the one of the user component.

The callback function **handle_info** will be in charge of changing the *view_to_show* property of the socket, and thus, the conditional rendering of each component will be performed according to the arguments we pass to this function.

In the file ***vehicle_live/index.html.leex***:

```elixir
<%= if @view_to_show == :vehicle_view do %>
  <%= live_component @socket, CarWorkshopWeb.VehicleComponent,
    id: @vehicle.id || :new,
    title: "Register Vehicle",
    action: @live_action,
    vehicle: @vehicle,
    customer_id: @customer.id
  %>
<% else %>
  <%= live_component @socket, CarWorkshopWeb.CustomerComponent,
    id: @customer.id || :new,
    title: "Register Customer",
    action: @live_action,
    customer: @customer
  %>
<% end %>
```

With this, we already have the user view rendered. What we would need to do is call the **handle_info** callback from the user component once all our business logic has been executed, and then allow the rendering of the vehicle view.

Suppose we want to render the vehicle component immediately after the user has been successfully registered. For this, in the ***customer_component.ex*** file:

```elixir
  ...
  @impl true
  def handle_event("save", %{"customer" => customer_params}, socket) do
    case Accounts.create_customer(customer_params) do
      {:ok, customer} ->
        send(self(), {:customer_registered, customer, :vehicle_view})

        {:noreply, socket}

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

Using the **send()** function we make the parent LiveView execute the **handle_info** callback that matches the *{:customer_registered, customer, view_to_show}* parameter contract. From which we will be able to know the identifier of the **handle_info** to execute (*:customer_registered*), the newly created user (*customer*), and the view to render (*:vehicle_view*).

### That's it, we have a conditional rendering of components!

---
To see the full implementation you can visit the repo:
{% github santiagocardo/car-workshop %}