Svx View Source
A PoC for single-file components for Phoenix LiveView
Table of Contents
Installation
- Add
svx
to your list of dependencies inmix.exs
:
def deps do
[
{:svx, "~> 0.3.2"}
]
end
Note: Requires fswatch
(apt-get fswatch
or brew install fswatch
)
- In
lib/<you_app>/application.ex
add Svx to apps that you start:
{Svx.Compiler, [path: "lib/<your_app>_web/live", namespace: ExampleWeb.Live]}
- Add
@import "./generated.css";
toassets/css/app.css
- Create your views as
.lsvx
inlib/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):
<script lang="elixir">... code ...</script>
that contains your module's Elixir code. Themount
function goes in here, as any other function you would write in yourview.ex
<style>... css ... </style>
. Regular css. CSS from each svx-component will be extracted and placed in a single file atasstes/css/generated.css
. No, the CSS isn't scoped, it requires far more work than is feasible for a PoCregular 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
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
Place component code below at
lib/your_app_web/live/thermostat.lsvx
In your
router.ex
addscope "/", YourAppWeb do pipe_through :browser get "/", PageController, :index live "/thermostat", Live.Thermostat end
Add
@import "./generated.css";
toassets/css/app.css
Run your Phoenix app with
iex -S mix phx.server
, and navigate to http://localhost:4000/thermostatChange Elixir code, HTML, styles, and see them update in the browser
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:
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.