Using Elixir/Phoenix And Struggle With Forms or UI? LiveSvelte To The Rescue

29 Oct 2024

If you use Elixir/Phoenix and struggle with forms and complex UI, you are not alone. In this article I will show you how to solve these problems with LiveSvelte.

What Is LiveSvelte

LiveSvelte is a library that allows you to use Svelte components inside your Phoenix code not loosing any of the Phoenix jazz (SSR, state change, etc).

Alternatives?

LiveSvelte is not alone in this space there are alternatives to run React and Vue code.

LiveSvelte In Action

Basic LiveView component that has counter property in its state and increase_counter event handler that increases the counter:

defmodule LiveviewpreviewWeb.CaseLive.Index do
  use LiveviewpreviewWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket |> assign(counter: 0)}
  end

  @impl true
  def handle_event("increase_counter", _params, socket),
    do:
      {:noreply,
       socket |> assign(counter: socket.assigns.counter + 1)}
end

Layout for this component that renders counter variable natively and another component — SvelteCounter:

<div class="w-full">
  <div class="flex flex-col w-full gap-2 items-center justify-center mb-4">
    <div class="flex flex-col w-1/2 gap-2 justify-center text-center">
      <p class="text-lg font-bold">Counter:</p>
      <p class="font-bold"><%= @counter %></p>
      <.button phx-click="increase_counter">
        Increase On The Server
      </.button>
    </div>
  </div>
  <div class="w-full h-[2px] bg-gray-200 my-4" />
  <LiveSvelte.svelte
    name="SvelteCounter"
    props={%{counter: @counter}}
  />
</div>

SvelteCounter component looks like this:

<script lang="ts">
 import { Live } from "live_svelte";

 export let live: Live;
 export let counter: number;
 let local_counter: number = 0;

 const increase_counter = () => {
    live.pushEvent("increase_counter");
 };
 $: increase_local_counter = () => {
    local_counter++;
 };
</script>

<div class="flex flex-row w-full gap-2 justify-center">
   <div
    class="flex flex-col w-1/3 gap-2 justify-center align-center text-center"
   >
      <p class="text-lg font-bold">Svelte Counter:</p>
      <p class="font-bold">{counter}</p>
      <button
         class="phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3 text-sm font-semibold leading-6 text-white active:text-white/80"
         on:click={increase_counter}>Increase On The Server</button
      >
   </div>
   <div
    class="flex flex-col w-1/3 gap-2 justify-center align-center text-center"
   >
      <p class="text-lg font-bold">Svelte Local Counter:</p>
      <p class="font-bold">{local_counter}</p>
      <button
         class="phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3 text-sm font-semibold leading-6 text-white active:text-white/80"
         on:click={increase_local_counter}>Increase Local Counter</button
      >
   </div>
</div>

Let’s look at the code above in SvelteCounter component more closely. In the script tag we have several external variables:

  • live — inserted in every component, holds LiveView callbacks
    export type Live = {
      pushEvent(event: string, payload?: object, onReply?: (reply: any, ref: number) => void): number
      pushEventTo(phxTarget: any, event: string, payload?: object, onReply?: (reply: any, ref: number) => void): number
    
      handleEvent(event: string, callback: (payload: any) => void): Function
      removeHandleEvent(callbackRef: Function): void
    
      upload(name: string, files: any): void
      uploadTo(phxTarget: any, name: string, files: any): void
    }
  • counter — LiveView state variable passed from LiveView layout of LiveviewpreviewWeb.CaseLive.Index

Additionally, we have local_counter variable that holds internal component state.

In the HTML section we display two counters:

  • Svelte Counter — counter variable passed from the LiveView component with corresponding button “Increase On The Server” that calls increase_counter function that uses live.pushEvent to send increase_counter event back to the LiveView component (LiveviewpreviewWeb.CaseLive.Index)
  • Svelte Counter Local — local_counter variable created within this Svelte component with corresponding button “Increase Local Counter” that calls increase_local_counter function that simply increases the local_counter variable’s value

Demo

In the demo below you can see how the state of the LiveView component is correctly rendered in LiveView’ layout and in Svelte component, with both buttons working exactly the same keeping the local state variable untouched.