Svx

A PoC for single-file components for PhoenixLiveView

Table of Contents

Installation

  1. Add svx to your list of dependencies in mix.exs:
def deps do
  [
    {:svx, "~> 0.3.2"}
  ]
end

Note: Requires fswatch (apt-get fswatch or brew install fswatch)

  1. In lib/<you_app>/application.ex add Svx to apps that you start:
{Svx.Compiler, [path: "lib/<your_app>_web/live", namespace: ExampleWeb.Live]}
  1. Add @import "./generated.css"; to assets/css/app.css
  2. Create your views as .lsvx in lib/your_app_web/live

They will be available under ExampleWeb.Live.

How to

See also priv/example for a full example

Component structure

An svx-component is a file with a .lsvx extension that contains three parts (the order in which they appear in the file is not important):

Module names

Module names are generated by a simple substitution:

So, your_app/lib/your_app_web/live/ui/some_module.lsvx becomes YourAppWeb.Live.Ui.SomeModule

When you set up your router.ex, you can *Web:

  scope "/", SvxWeb do
    pipe_through :browser

    get "/", PageController, :index
    live "/thermostat", Live.Thermostat
  end

Svx compiler will output component names to stdout, so you you can see what names are actually generated

Generated CSS

All code in <style></style> is extracted and placed at assets/css/generated.css. The easiest way to make sure that it's reloaded when you change it is to add @import "./generated.css"; to assets/css/app.css

Errors

If you have errors in your markup, Svx will still attempt to compile your component, but will replace component content with the error from Heex tokenizer or other errors that may arise when compiling the component.

Example

Component code

<script type="elixir">
  use ExampleWeb, :live_view

  def mount(_params, _p, socket) do
    temperature = 11
    {:ok, assign(socket, :temperature, temperature)}
  end
</script>

<%= for x <- [1,2,3], do: "#{x}" %>

<div title={@temperature}>
  <p class={"temp-#{@temperature > 10}"}>Hello, temperature is: <%= @temperature %></p>
</div>

<style>
  .temp-false {
    color: blue;
    font-size: 24pt;
    text-decoration: underline;
  }
  .temp-true {
    color: red;
    font-size: 24pt;
    text-decoration: underline;
  }
</style>

The code above is equivalent to

defmodule YourAppWeb.Live.Thermostat do
  use ExampleWeb, :live_view

  def mount(_params, _p, socket) do
    temperature = 11
    {:ok, assign(socket, :temperature, temperature)}
  end

  def render(assigns) do
    ~H"""
    <%= for x <- [1,2,3], do: "#{x}" %>

    <div title={@temperature}>
      <p class={"temp-#{@temperature > 10}"}>Hello, temperature is: <%= @temperature %></p>
    </div>
    """
  end
end

And the css will be located at assets/css/generated.css

Caveats

It's a proof of concept. So things will definitely break :)

The code uses LiveView's Phoenix.LiveView.HTMLTokenizer.tokenize/5 directly:

Additionally, all I do is create a string with module code, and run Code.compile_string/2 on it. So this can break :)

Also: no tests. Of course. It's a PoC :D

Motivation

I really like Svelte's single file components and wished I had something similar for LiveView:

IMO the sweet spot for single-file components is a medium-to-large template with not too-much elixir code powering it.