# 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): - `` that contains your module's Elixir code. The `mount` function goes in here, as any other function you would write in your `view.ex` - ``. 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 `` 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 ``` <%= for x <- [1,2,3], do: "#{x}" %>

10}"}>Hello, temperature is: <%= @temperature %>

``` 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}" %>

10}"}>Hello, temperature is: <%= @temperature %>

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