# Svx
A PoC for single-file components for [Phoenix](https://www.phoenixframework.org) [LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html)
## Table of Contents
- [Installation](#installation)
- [How to](#how-to)
- [Component structure](#component-structure)
- [Module names](#module-names)
- [Generated CSS]()
- [Example](#example)
- [Generated CSS](#generated-css)
- [Caveats](#caveats)
- [Motivation](#motivation)
## Installation
1. Add `svx` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:svx, "~> 0.2.0"}
]
end
```
2. In `mix.exs` add `:svx` to `:compilers`:
```
def project do
[
#...
compilers: Mix.compilers() ++ [:svx],
]
end
```
3. In `config/config.exs` add add `:reloadable_compilers` under `Endpoint`:
```
config :your_app, YourAppWeb.Endpoint,
#...
live_view: [signing_salt: "OiSdJKnT"],
reloadable_compilers: reloadable_compilers: Mix.compilers() ++ [:svx]
```
4. Add `:prelude` config to `config/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:
```elixir
config :your_app, :svx,
prelude: ["use YourAppWeb, :live_view"]
```
5. In `config/dev.exs` set Phoenix to live-reload your templates:
```elixir
config :your_app, YourAppWeb.Endpoint,
live_reload: [
patterns: [
#...
~r"lib/your_app_web/(live|views)/.*(svx)$"
]
]
```
6. Add `@import "./generated.css";` to `assets/css/app.css`
7. Create your views as `.lsvx` files anywhere in `lib/your_app_web`
## How to
See also [priv/example](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`
When you set up your `router.ex`, you omit `*Web`:
```elixir
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`
### Example
- Place component code below at `lib/your_app_web/live/thermostat.lsvx`
- In your `router.ex` add
```elixir
scope "/", YourAppWeb do
pipe_through :browser
get "/", PageController, :index
live "/thermostat", 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
```elixir
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](https://svelte.dev/tutorial/basics) 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.