# 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.3.2"}
]
end
```
**Note**: Requires `fswatch` (`apt-get fswatch` or `brew install fswatch`)
2. In `lib/<you_app>/application.ex` add Svx to apps that you start:
```
{Svx.Compiler, [path: "lib/<your_app>_web/live", namespace: ExampleWeb.Live]}
```
3. Add `@import "./generated.css";` to `assets/css/app.css`
4. Create your views as `.lsvx` in `lib/your_app_web/live`
They will be available under `ExampleWeb.Live`.
## How to
See also [priv/example](https://github.com/dmitriid/svx/tree/master/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 can `*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">
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
```elixir
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](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.