Renderizar componentes condicionalmente con Phoenix LiveView
Tags: elixir, liveview, phoenix, webdev
Published: 2021-04-27 15:33:38

Cuando nos encontramos con una vista que tiene muchos elementos HTML, normalmente queremos separarlos en sus propios componentes para tener una mejor organización del código que facilite su comprensión y mantenimiento, así como desacoplar la lógica grande en piezas más pequeñas, y por qué no, reutilizar estos componentes en otra vista (muy al estilo del modelo mental de React.js).  Esto se puede conseguir fácilmente utilizando [LiveComponent](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html) de **Phoenix LiveView**.

El problema, es que muchas veces estos componentes comparten cierta lógica entre sí, y muchas veces dependen el uno del otro para saber si debe ser renderizado o no. A esto se le llama renderizado condicional, y con **Phoenix LiveView** podemos lograrlo manejando la lógica condicional en la **LiveView** padre, por medio de la función callback **handle_info**.

### Caso de Uso

Supongamos que tenemos una vista para registrar un vehículo en un taller de mecánica. Esta vista se compone de dos formularios, uno para el registro del dueño del vehículo y otro para el registro del vehículo como tal. Cada formulario tiene su propia vista de registro independiente, y por ello usamos un **LiveComponent** por cada una de estos, dentro del cual se maneja su propia lógica de negocios independientemente.

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

Estas vistas deben ser renderizadas independientemente y en orden:

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

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

Una vez registrado el usuario, se le debe pasar el *id* del usuario al componente de vehículo, y así poder asociar exitosamente el usuario con el vehículo cuando estamos registrando el vehículo.

### Cómo logramos esto? Vamos al código...

La lógica condicional la manejamos en la vista padre de los componentes usuario y vehículo, en este caso será la LiveView ***vehicle_live/index.ex*** y su respectivo archivo ***vehicle_live/index.html.leex*** donde se encuentran los elementos HTML a renderizar.

En el archivo ***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
```

En la función **mount**, le asignamos al socket la propiedad *view_to_show* por medio de la cual sabremos el componente a renderizar en ***vehicle_live/index.html.leex***. Le damos un valor de *:customer_view*, haciendo que la primera vista en ser renderizada sea la del componente de usuario.

La función callback **handle_info** será la encargada de cambiar la propiedad *view_to_show* del socket, y así, se realizará el renderizado condicional de cada componente según los argumentos que le pasemos a esta función.

En el archivo ***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 %>
```

Con esto, ya tenemos renderizada la vista de usuario. Lo único que nos faltaría sería llamar al callback **handle_info** desde el componente de usuario una vez que se haya ejecutado toda nuestra lógica de negocio, y así, luego permitir el renderizado de la vista de vehículo.

Supongamos que queremos renderizar el componente de vehículo inmediatamente después que se registre el usuario exitosamente. Para esto, en el archivo ***customer_component.ex***:

```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
  ...
```

Usando la función **send()** hacemos que en el LiveView padre se ejecute el callback **handle_info** que cumple con el contrato de parámetros *{:customer_registered, customer, view_to_show}*. Del cual podremos saber el indentificador del **handle_info** a ejecutar (*:customer_registered*), el usuario recién creado (*customer*) y la vista a renderizar (*:vehicle_view*).

### Esto es todo, tenemos renderizado condicional de componentes!

---
Para ver la implementación completa puedes visitar el repo:
{% github santiagocardo/car-workshop %}