Svx View Source

A PoC for single-file components for Phoenix LiveView

Table of Contents

Installation

  1. Add svx to your list of dependencies in mix.exs:
def deps do
  [
    {:svx, "~> 0.1.0"}
  ]
end
  1. In mix.exs add :svx to :compilers:
  def project do
    [
      #...
      compilers: Mix.compilers() ++ [:svx],
    ]
  end
  1. In config/config.exs add :svx to :reloadable_compilers under Endpoint:
config :your_app, YourAppWeb.Endpoint,
  #...
  live_view: [signing_salt: "OiSdJKnT"],
  reloadable_compilers: [:svx]
  1. Add @import "./generated.css"; to assets/css/app.css
  2. Create your views as .lsvx files anywhere in lib

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):

  • <script lang="elixir">... code ...</script> that contains your module's Elixir code. The mount function goes in here, as any other function you would write in your view.ex

  • <style>... css ... </style>. Regular css. CSS from each svx-component will be extracted and placed in a single file at asstes/css/generated.css. No, the CSS isn't scoped, it requires far more work than is feasible for a PoC

  • regular HTML/HEex. Everything else in the file is assumed to be regular HTML/Heex and will form the basis of the render/1 function

Module names

Module names are generated by a simple substitution:

  • take path relative to lib
  • remove all underscores (_)
  • Title Case everything
  • join with periods (.)

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

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

Example

  • Place component code below at lib/your_app_web/live/thermostat.lsvx

  • In your router.ex add

    scope "/", YourAppWeb do
      pipe_through :browser
    
      get "/", PageController, :index
      live "/thermostat", YourAppWeb.Live.Thermostat
    end
  • Add @import "./generated.css"; to assets/css/app.css

  • Run your Phoenix app with iex -S mix phx.server, and navigate to http://localhost:4000/thermostat

  • Change Elixir code, HTML, styles, and see them update in the browser

Component code

<script type="elixir">
  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
  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:

  • If that API changes, is removed or becomes private, svx breaks

  • This API isn't aware of Eex constructs, so the code does some string replacement:

    • replace Eex-like tokens and Elixir-like tokens inside Eex with placeholders
    • tokenize
    • replace placeholders back

    I didnt' do any exhaustive checking on this, so there will d,efinitely be some constructs that break

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:

  • Templating code isn't split into a separate file
  • Templating code isn't in a string
  • Styling code isn't in a separate file in an entirely different directory

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