{"id":32163978,"url":"https://github.com/dmitriid/svx","last_synced_at":"2026-03-01T04:34:04.438Z","repository":{"id":62430339,"uuid":"428686719","full_name":"dmitriid/svx","owner":"dmitriid","description":"A PoC for single-file components for Phoenix LiveView","archived":false,"fork":false,"pushed_at":"2022-02-18T10:32:59.000Z","size":82,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-02-18T05:09:53.210Z","etag":null,"topics":["elixir","phoenix-framework","phoenix-liveview"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmitriid.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-11-16T14:24:14.000Z","updated_at":"2023-07-25T14:51:33.000Z","dependencies_parsed_at":"2022-11-01T20:18:36.775Z","dependency_job_id":null,"html_url":"https://github.com/dmitriid/svx","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dmitriid/svx","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmitriid%2Fsvx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmitriid%2Fsvx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmitriid%2Fsvx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmitriid%2Fsvx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmitriid","download_url":"https://codeload.github.com/dmitriid/svx/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmitriid%2Fsvx/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29960253,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-01T01:47:18.291Z","status":"online","status_checked_at":"2026-03-01T02:00:07.437Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir","phoenix-framework","phoenix-liveview"],"created_at":"2025-10-21T14:41:30.888Z","updated_at":"2026-03-01T04:34:04.433Z","avatar_url":"https://github.com/dmitriid.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Svx\n\nA PoC for single-file components for [Phoenix](https://www.phoenixframework.org) [LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html)\n\n## Table of Contents\n\n- [Installation](#installation)\n- [How to](#how-to)\n  - [Component structure](#component-structure) \n  - [Module names](#module-names)\n  - [Generated CSS](#generated-css)\n  - [Errors](#errors)\n  - [Example](#example)\n- [Generated CSS](#generated-css)\n- [Caveats](#caveats)\n- [Motivation](#motivation)\n\n## Installation\n\n1. Add `svx` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:svx, \"~\u003e 0.3.2\"}\n  ]\nend\n```\n\n**Note**: Requires `fswatch` (`apt-get fswatch` or `brew install fswatch`)\n\n2. In `lib/\u003cyou_app\u003e/application.ex` add Svx to apps that you start:\n\n```\n{Svx.Compiler, [path: \"lib/\u003cyour_app\u003e_web/live\", namespace: ExampleWeb.Live]}\n```\n\n\n3. Add `@import \"./generated.css\";` to `assets/css/app.css`\n4. Create your views as `.lsvx` in `lib/your_app_web/live`\n\nThey will be available under `ExampleWeb.Live`.\n\n## How to\n\nSee also [priv/example](https://github.com/dmitriid/svx/tree/master/priv/example) for a full example\n\n### Component structure\n\nAn svx-component is a file with a `.lsvx` extension that contains three parts\n(the order in which they appear in the file is not important):\n\n- `\u003cscript lang=\"elixir\"\u003e... code ...\u003c/script\u003e` that contains your module's Elixir code. \n  The `mount` function goes in here, as any other function you would write in your `view.ex`\n\n- `\u003cstyle\u003e... css ... \u003c/style\u003e`. Regular css. CSS from each svx-component will be extracted\n  and placed in a single file at `asstes/css/generated.css`. No, the CSS isn't scoped, it\n  requires far more work than is feasible for a PoC\n\n- regular HTML/HEex. Everything else in the file is assumed to be regular HTML/Heex and will\n  form the basis of the `render/1` function\n\n### Module names\n\nModule names are generated by a simple substitution:\n\n- take path relative to lib\n- remove all underscores (`_`)\n- Title Case everything\n- join with periods (`.`)\n\nSo, `your_app/lib/your_app_web/live/ui/some_module.lsvx` becomes `YourAppWeb.Live.Ui.SomeModule`\n\nWhen you set up your `router.ex`, you can `*Web`:\n\n```elixir\n  scope \"/\", SvxWeb do\n    pipe_through :browser\n\n    get \"/\", PageController, :index\n    live \"/thermostat\", Live.Thermostat\n  end\n```\n\nSvx compiler will output component names to stdout, so you you can see what names are actually generated\n\n### Generated CSS\n\nAll code in `\u003cstyle\u003e\u003c/style\u003e` is extracted and placed at `assets/css/generated.css`.\nThe easiest way to make sure that it's reloaded when you change it is to add\n`@import \"./generated.css\";` to `assets/css/app.css`\n\n### Errors\n\nIf you have errors in your markup, Svx will still attempt to compile your component,\nbut will replace component content with the error from Heex tokenizer or other errors\nthat may arise when compiling the component.\n\n### Example\n\n- Place component code below at `lib/your_app_web/live/thermostat.lsvx`\n- In your `router.ex` add\n  ```elixir\n  scope \"/\", YourAppWeb do\n    pipe_through :browser\n\n    get \"/\", PageController, :index\n    live \"/thermostat\", Live.Thermostat\n  end\n  ```\n- Add `@import \"./generated.css\";` to `assets/css/app.css`\n- Run your Phoenix app with `iex -S mix phx.server`, and navigate to http://localhost:4000/thermostat\n- Change Elixir code, HTML, styles, and see them update in the browser\n\n#### Component code\n\n```\n\u003cscript type=\"elixir\"\u003e\n  use ExampleWeb, :live_view\n\n  def mount(_params, _p, socket) do\n    temperature = 11\n    {:ok, assign(socket, :temperature, temperature)}\n  end\n\u003c/script\u003e\n\n\u003c%= for x \u003c- [1,2,3], do: \"#{x}\" %\u003e\n\n\u003cdiv title={@temperature}\u003e\n  \u003cp class={\"temp-#{@temperature \u003e 10}\"}\u003eHello, temperature is: \u003c%= @temperature %\u003e\u003c/p\u003e\n\u003c/div\u003e\n\n\u003cstyle\u003e\n  .temp-false {\n    color: blue;\n    font-size: 24pt;\n    text-decoration: underline;\n  }\n  .temp-true {\n    color: red;\n    font-size: 24pt;\n    text-decoration: underline;\n  }\n\u003c/style\u003e\n```\n\nThe code above is equivalent to\n\n```elixir\ndefmodule YourAppWeb.Live.Thermostat do\n  use ExampleWeb, :live_view\n\n  def mount(_params, _p, socket) do\n    temperature = 11\n    {:ok, assign(socket, :temperature, temperature)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003c%= for x \u003c- [1,2,3], do: \"#{x}\" %\u003e\n\n    \u003cdiv title={@temperature}\u003e\n      \u003cp class={\"temp-#{@temperature \u003e 10}\"}\u003eHello, temperature is: \u003c%= @temperature %\u003e\u003c/p\u003e\n    \u003c/div\u003e\n    \"\"\"\n  end\nend\n```\n\nAnd the css will be located at `assets/css/generated.css`\n\n## Caveats\n\nIt's a proof of concept. So things will definitely break :)\n\nThe code uses LiveView's `Phoenix.LiveView.HTMLTokenizer.tokenize/5` directly:\n\n- If that API changes, is removed or becomes private, svx breaks\n- This API isn't aware of Eex constructs, so the code does some string replacement:\n  - replace Eex-like tokens and Elixir-like tokens inside Eex with placeholders\n  - tokenize\n  - replace placeholders back\n  \n  I didnt' do any exhaustive checking on this, so there will d,efinitely be some constructs\n  that break\n\nAdditionally, all I do is create a string with module code, and run `Code.compile_string/2`\non it. So this can break :)\n\nAlso: no tests. Of course. It's a PoC :D\n\n## Motivation\n\nI really like [Svelte's single file components](https://svelte.dev/tutorial/basics) and wished\nI had something similar for LiveView:\n\n- Templating code isn't split into a separate file\n- Templating code isn't in a string\n- Styling code isn't in a separate file in an entirely different directory\n\nIMO the sweet spot for single-file components is a medium-to-large template with not too-much\nelixir code powering it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmitriid%2Fsvx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmitriid%2Fsvx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmitriid%2Fsvx/lists"}