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.0"}
]
end
- In
lib/<you_app>/application.ex
add Svx to apps that you start:
{Svx.Compiler, [path: "lib/<your_app>_web/live"]}
- Add
:prelude
config toconfig/config.exs
. Here you can add a list of imports, requires, uses etc. that you want to appear at the top of your views. You must have at least"use YourAppWeb, :live_view"
here:
config :your_app, :svx,
prelude: ["use YourAppWeb, :live_view"]
- In
config/dev.exs
set Phoenix to live-reload your templates:
config :your_app, YourAppWeb.Endpoint,
live_reload: [
patterns: [
#...
~r"lib/your_app_web/(live|views)/.*(svx)$"
]
]
- Add
@import "./generated.css";
toassets/css/app.css
- Create your views as
.lsvx
inlib/your_app_web/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 omit *Web
:
scope "/", SvxWeb do
pipe_through :browser
get "/", PageController, :index
live "/thermostat", YourAppWeb.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
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">
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.