README.md

# 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.