April 27, 2023 ~ 5 min read

Svelte As An Alternative LiveView DSL

In my last post I introduced LiveSvelte, a package that integrates Svelte with Phoenix LiveView, delivering end-to-end reactivity.

In that post we show examples of using Svelte components inside Phoenix LiveView.

But we can take it one step further. In this post I’ll introduce the concept of using LiveSvelte to make Svelte the Domain-Specific Language (DSL) for rendering LiveView.

Why?

LiveView is fantastic at adding server side state and reactivity on that state to your browser. On the other hand, local browser state in LiveView is an afterthought. This makes it harder to develop certain features that would otherwise be relatively easy with a frontend framework.

Svelte is great at rendering an interactive frontend. In addition, LiveView renders to the browser, so why don’t use a technology that is already excellent in the browser?

Combining the two technologies gives you a supercharged developer experience. LiveView, with local state.

In addition you get to plug into the vast NPM ecosystem with relative ease.

Examples

Let’s take a look at some examples

Simple

elixir
defmodule ExampleWeb.SvelteLive do
  use ExampleWeb, :live_view

  def render(assigns) do
    ~V"""
    This is Svelte code
    """
  end
end

We use the ~V sigil instead of the ~H sigil to render Svelte. Everything inside the ~V sigil is now Svelte code. This means you can do things like import NPM packages, make things reactive client side and use scoped styling. Let’s take a look at some more examples.

Client Side Reactivity

elixir
defmodule ExampleWeb.SvelteLive do
  use ExampleWeb, :live_view

  def render(assigns) do
    ~V"""
    <script>
      let number = 1
    </script>

    Incrementing the number: <button on:click={() => number += 1}>++{number}</button>
    """
  end
end

An example of client side reactivity.

Server + Client Reactivity

elixir
defmodule ExampleWeb.SvelteLive do
  use ExampleWeb, :live_view

  def render(assigns) do
    ~V"""
    <script>
      // We added export to make it reactive with the server
      // Number is set to 10
      export let number = 1

      // Let's combine local state with server state
      let number2 = 5

      $: combined = number + number2
    </script>

    {number} + {number2} = {combined}

    <button class="rounded" phx-click="increment">increment</button>
    <button class="rounded" on:click={() => number2 += 1}>increment</button>

    <style lang="stylus">
      button
        background-color black
        color white
        padding 0.5rem 1rem
    </style>
    """
  end

  def mount(_params, _session, socket) do
    # This initializes the number
    {:ok, assign(socket, :number, 10)}
  end

  def handle_event("increment", _values, socket) do
    # This will increment the number when the increment events gets sent
    {:noreply, assign(socket, :number, socket.assigns.number + 1)}
  end
end

Here we combine server and client side reactivity. And we add some scoped styling using the Stylus preprocessor.

NPM Package

elixir
defmodule ExampleWeb.SvelteLive do
  ...
  def render(assigns) do
    ~V"""
    <script>
        import {slide, fly} from "svelte/transition"
        import {Marquee} from "dynamic-marquee"
        import {onMount} from "svelte"
        ...
    </script>
    ...
    """
  end
  ...
end

For brevity sake I’ve not included all code, full code can be found here.

It’s easy to pull in NPM packages.

How Does this Work?

A couple of steps happen to make this all work.

  • Compilation: During compilation, everything in the ~V sigil gets picked up and written to a Svelte file. Esbuild picks up these Svelte files and generates 2 Javascript files, a client and a server file.
  • Server-side rendering: When someone visits a page, we call node from elixir to generate the initial page. Server-side rendering for Svelte is optional but enabled by default.
  • Serving the HTML: We also include the client js file inside the head of the HTML document that’s being generated.
  • Hydration: The browser executes the client Javascript file and adds all the necessary client side reactivity.

LiveView DSL vs LiveSvelte DSL

In the traditional LiveView model, LiveView passes raw HTML over the websocket, and then updates the HTML in the browser surgically (only the changed HTML gets changed).

With LiveSvelte, only data gets sent over the websocket, no raw HTML is sent. This allows Svelte to take this data and then perform the reactivity on it in the browser.

Why Does This Make Sense?

It’s great to have server-side state with LiveView, this allows you to build really cool dynamic applications with minimal Javascript and no traditional API, the API is message passing over a websocket and your backend code is what’s being executed. In my experience building an API can be a lot of work, LiveView gets rid of this step.

But LiveView renders to the browser, which is a client side environment, and for certain applications you do want client-side only interactivity. For example animations, local keyboard input (arrow keys for custom dropdown menu), or ultimately even games, which can be very dynamic in nature. You only really care about the data that’s being passed. Svelte can take this data, and apply the reactivity on the client.

What Do We Lose

You lose the ability to use Elixir inside your LiveView, which is still possible with the component based method. Svelte has for-loops, if-statements etc, which makes it map really well to the already declarative style you’re used to in LiveView.

Phoenix also has a bunch of components out of the box, like form elements, which you wouldn’t be able to use, you’d have to create your own inside Svelte. There’s the idea to recreate these components in LiveSvelte so you’d still have them available in a Svelte environment. Also note that Svelte and Javascript have a rich ecosystem of predefined components, so you do have access to the same idea, just not the default Phoenix ones.

Maybe more? Let me know why you wouldn’t use LiveSvelte this way.

Final Words

LiveSvelte gives you a unified experience that does both client and server reactivity on top of LiveView.

Note that you can still use Svelte components like outlined in the previous blog post, using the ~V sigil is an optional feature.

Would love to hear your thoughts on this concept!