{"id":15063626,"url":"https://github.com/ndrean/demo-solidjs-websockets","last_synced_at":"2026-02-06T07:07:27.567Z","repository":{"id":249847593,"uuid":"832724247","full_name":"ndrean/demo-solidjs-websockets","owner":"ndrean","description":"Phoenix LiveView with Solidjs with WebSockets","archived":false,"fork":false,"pushed_at":"2024-07-24T22:02:07.000Z","size":216,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-22T08:37:13.954Z","etag":null,"topics":["apexcharts","elixir","liveview","solidjs","websocket","websockets-client"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ndrean.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-23T15:39:10.000Z","updated_at":"2024-11-25T15:26:11.000Z","dependencies_parsed_at":"2024-09-25T00:05:18.520Z","dependency_job_id":"ff08a63d-aca6-4106-8a9e-bde30e61c339","html_url":"https://github.com/ndrean/demo-solidjs-websockets","commit_stats":null,"previous_names":["ndrean/demo-solidjs-websockets"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fdemo-solidjs-websockets","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fdemo-solidjs-websockets/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fdemo-solidjs-websockets/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fdemo-solidjs-websockets/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ndrean","download_url":"https://codeload.github.com/ndrean/demo-solidjs-websockets/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243778789,"owners_count":20346616,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["apexcharts","elixir","liveview","solidjs","websocket","websockets-client"],"created_at":"2024-09-25T00:05:00.222Z","updated_at":"2026-02-06T07:07:22.548Z","avatar_url":"https://github.com/ndrean.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Phoenix LiveView with Solidjs, ApexCharts\n\n## What is this?\n\nA small `Phoenix LiveView` app to showcase:\n\n- navigation with tabs.\n- code splitting,\n- prefetching data on image hovering.\n- include a `SolidJS` component that renders a table with the WebSocket connection to an endpoint to preprend data in realtime. This component sends the data to the server via an `Elixir.Channel` where it is saved into an `SQLite` database.\n- include a `SolidJS` component that uses the lightweight Javascript charting `ApexCharts`. We visualize data sent from a WebSocket client (a server module powered by [Fresh](https://github.com/bunopnu/fresh)). He set a PubSub between the Fresh server and an `Elixir.Channel`, and then push via the Channel to the browser.\n- compare image classification with pre-trained models, BLIP server-side, mediaPipe \u0026 ML5 client-side based on RESNET.\n\n## Esbuild plugins\n\nWe are going to use `Esbuild` plugins. This is explained [in the documentation](https://hexdocs.pm/phoenix/asset_management.html#esbuild-plugins).\n\n### Package.json\n\nBesides bringing in `esbuild` and `tailwind`, we use `solidjs` and `apexcharts`.\n\nAt the time of writting, you have:\n\n```bash\npnpm init\npnpm add -D @tailwindcss/forms esbuild esbuild-plugin-solid tailwindcss fs\npnpm add solid-js @solid-primitives/ref apexcharts solid-apexcharts ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view\n```\n\nYou should have (at the time of writing):\n\n```json\n\"type\": \"module\",\n\"devDependencies\": {\n  \"@tailwindcss/forms\": \"^0.5.7\",\n  \"esbuild\": \"^0.23.0\",\n  \"esbuild-plugin-solid\": \"^0.6.0\",\n  \"tailwindcss\": \"^3.4.6\",\n  \"fs\": \"0.0.1-security\",\n},\n\"dependencies\": {\n  \"@solid-primitives/refs\": \"^1.0.8\",\n  \"@solidjs/router\": \"^0.14.1\",\n  \"apexcharts\": \"^3.51.0\",\n  \"phoenix\": \"link:../deps/phoenix\",\n  \"phoenix_html\": \"link:../deps/phoenix_html\",\n  \"phoenix_live_view\": \"link:../deps/phoenix_live_view\",\n  \"solid-apexcharts\": \"^0.3.4\",\n  \"solid-js\": \"^1.8.18\",\n  \"topbar\": \"^3.0.0\"\n}\n```\n\n## Custom Esbuild configuration\n\nSolidJS uses JSX for templating, we have to be sure Esbuild compiles the JSX files for SolidJS.\nPhoenix compiles and bundles all `js` and `jsx` files into the \"priv/static/assets\" folder.\nWe also evaluate bundle size mapping.\n\n\u003cdetails\u003e\n\u003csummary\u003eBuild.js file\u003c/summary\u003e\n\n```js\n// new file: /assets/build.js\n\nimport { context, build } from \"esbuild\";\nimport { solidPlugin } from \"esbuild-plugin-solid\";\nimport fs from \"fs\";\n\nconst args = process.argv.slice(2);\nconst watch = args.includes(\"--watch\");\nconst deploy = args.includes(\"--deploy\");\n\n// Define esbuild options\nlet opts = {\n  entryPoints: [\"js/app.js\"],\n  bundle: true,\n  logLevel: \"info\",\n  target: \"es2022\",\n  outdir: \"../priv/static/assets\",\n  external: [\"*.css\", \"fonts/*\", \"images/*\"],\n  loader: { \".js\": \"jsx\", \".svg\": \"file\" },\n  plugins: [solidPlugin()],\n  nodePaths: [\"../deps\"],\n  format: \"esm\",\n};\n\nif (deploy) {\n  opts = {\n    ...opts,\n    minify: true,\n    splitting: true,\n  };\n  let result = await build(opts);\n  fs.writeFileSync(\"meta.json\", JSON.stringify(result.metafile, null, 2));\n}\n\nif (watch) {\n  opts = {\n    ...opts,\n    sourcemap: \"inline\",\n  };\n\n  context(opts)\n    .then((ctx) =\u003e (watch ? ctx.watch() : build(opts)))\n    .catch((error) =\u003e {\n      console.log(`Build error: ${error}`);\n      process.exit(1);\n    });\n}\n```\n\n\u003c/details\u003e\n\u003cbr/\u003e\n\nIn \"config/dev/exs\", add:\n\n```elixir\n# config/devs.exs\n\nconfig :solidjs, SolidjsWeb.Endpoint,\n  http: [ip: {127, 0, 0, 1}, port: 4000],\n  check_origin: false,\n  code_reloader: true,\n  debug_errors: true,\n  secret_key_base: \"aaiv+mgJIO4sLLpE7GUxg45/HQeETt98/a8ff6zlCwPEd4mOYSzDU7UEWoLyuzzv\",\n  watchers: [\n    node: [\"build.js\", \"--watch\", cd: Path.expand(\"../assets\", __DIR__)],\n    ^^^\n    tailwind: {Tailwind, :install_and_run, [:solidjs, ~w(--watch)]}\n  ]\n```\n\nand remove the config for \"esbuild\" (as `node` will run `esbuild`).\n\nThe `Mix` tasks in the \"mix.exs\" file are:\n\n```elixir\n# mix.exs\n\ndefp aliases do\n  [\n    setup: [\"deps.get\", \"ecto.setup\", \"assets.setup\", \"assets.build\"],\n    \"ecto.setup\": [\"ecto.create\", \"ecto.migrate\", \"run priv/repo/seeds.exs\", \"cmd --cd assets pnpm install\"],\n    \"ecto.reset\": [\"ecto.drop\", \"ecto.setup\"],\n    test: [\"ecto.create --quiet\", \"ecto.migrate --quiet\", \"test\"],\n    \"assets.deploy\": [\n      \"tailwind solidjs --minify\",\n      \"cmd --cd assets node build.js --deploy\",\n      \"phx.digest\"\n    ]\n  ]\nend\n```\n\n## Tab navigation\n\n- In the `handle_params/3` callback, parse the \"uri\" argument and add a `live_action` assign accordingly. For example, we want to navigate to \"/chart\", we add `live_action: :chart`.\n- In the router, add the routes corresponding to the tabs, such as:\n\n```elixir\nlive \"/chart\", MainLive, :chart\n```\n\n- add a `render/1` with a guard clause to render the appropriate markup.\n\n```elixir\ndef render(assigns) when assigns.live_action == :chart do\n  ~H\"\"\"\n   .....\n  \"\"\"\nend\n```\n\nThe navigation action is triggered by a link that looks like:\n\n```elixir\n\u003c.link\n  replace\n  phx-click={JS.patch(uri)}\n  class=\"...\"\n\u003e\n  Go the chart\n\u003c/.link\u003e\n```\n\n## Javascript components with context pattern\n\nWe used the **\"context\" pattern**: you parametrize a function that returns a Component.\n\nThe pattern is:\n\n```js\nexport const component = (ctx) =\u003e  {\n  // get stuff from the context\n  const {state, setState} = ctx\n  [...]\n  return Component(props) {\n    do stuff...;\n    return HTMLComponent\n  }\n}\n```\n\nTo use it, do:\n\n```js\nimport context from \"./context\";\nimport {componen}t from \"./component.jsx\"\n\nconst Component = component(context);\n```\n\nYou want to design a component as stateless as possible. You centralize everything related to the configuration, styles.\n\nLocal state is still possible though if it only belongs to this component where reactivity is need.\n\nFor example, `userSocket` and `useChannel` are declared in the \"context\". We can use them in any component.\n\n## Re-usable channels client-side\n\nTo establish the \"user socket\" and channel, we firstly build the userSocket client endpoint\n\n- connect client-side:\n\n:exclamation:note that we did not pass a \"user-token\" as an object to the \"params\" key in the `new Socket` constructor, but this should be the case.\n\n```js\nimport { Socket } from \"phoenix\";\nconst userSocket = new Socket(\"/socket\", {});\nuserSocket.connect();\nexport default userSocket;\n```\n\n- define the \"userSocket\" server-side:\n\n```elixir\n# add to endpoint.ex\nsocket \"/socket\", SolidjsWeb.UserSocket,\n    websocket: true,\n```\n\n- define the server-side handler:\n\n:exclamation: we should receive the \"user-token\" in the connect \"params\" and check it.\n\n```elixir\n# create user_socket.ex\ndefmodule SolidjsWeb.UserSocket do\n  use Phoenix.Socket\n\n  channel \"currency:*\", SolidjsWeb.CurrencyChannel\n\n  @impl true\n  def connect(_params, socket) do\n    {:ok, socket}\n  end\n\n  @impl true\n  def id(_socket), do: nil\nend\n```\n\nNote that we already declared the channels we will use in this module. For example, `CurrencyChannel` with the topics \"currecny:\\*\".\n\nTo establish channels on top of the \"user socket\", we define a generic `useChannel` client-side function:\n\n```js\n// userChannel.js\n\nexport default function useChannel(socket, topic) {\n  if (!socket) return null;\n  const channel = socket.channel(topic, {});\n  channel\n    .join()\n    .receive(\"ok\", () =\u003e {\n      console.log(`Joined successfully: ${topic}`);\n    })\n    .receive(\"error\", (resp) =\u003e {\n      console.log(`Unable to join ${topic}`, resp.reason);\n    });\n  return channel;\n}\n```\n\nTo use it, we add the topic client-side which matches the one declared in \"user_socket.ex\".\n\n```js\nimport userSocket from \"./context\";\nconst currencyChannel = useChannel(userSocket, \"currency:bitcoin\");\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e It remains to build the channel \"CurrencyChannel\" with this topic server-side. An example here\u003c/summary\u003e\n\n```elixir\ndefmodule SolidjsWeb.CurrencyChannel do\n  use Phoenix.Channel\n\n  @impl true\n  def join(\"currency:\"\u003c\u003etype, _params, socket) do\n    topic = \"streamer:#{type}\"\n    :ok = SolidjsWeb.Endpoint.subscribe(topic)\n\n    {:ok, assign(socket, :currency, type)}\n  end\n\n  # received FROM the browser, to be saved in the database\n  @impl true\n  def handle_in(\"currency:\"\u003c\u003ecurrency,  payload, socket)\n      when socket.assigns.currency == currency do\n    save_to_db(payload)\n    {:noreply, socket}\n  end\n\n  @impl true\n  # received as we subscribed to a PubSub topic\n  # brodcasted FROM the WebSocket client Solidjs.Streamer, then forward TO the browser chartHook via the channel\n  def handle_info(%{topic: \"streamer:\"\u003c\u003ecurrency, event: \"update\", payload: payload}, socket)\n      when currency == socket.assigns.currency do\n    broadcast!(socket, \"update\", payload)\n    {:noreply, socket}\n  end\n\n  defp save_to_db(payload) do\n    Solidjs.Repo.save(payload)\n  end\nend\n```\n\n\u003c/details\u003e\n\u003cbr/\u003e\n\n## Cleanup between SolidJS and LiveView\n\n`LiveView` shadows the `SolidJS` method `onCleanup`.\nTo properly stop a channel and a websocket connection when you leave a tab that runs these features, we may need to pass a reference from the Javascript hook, and use the \"ref.current\" in the `destroyed` hook lifecycle (check \"table.jsx\" and \"tableHook.js\")\n\n```js\nconst componentHook = {\n  channelRef: { current: null },\n  socketRef: { current: null },\n  mounted() {\n    Component(channelRef, socketRef);\n  },\n  destroyed() {\n    this.channelRref.leave();\n    this.socketRef.close();\n  },\n};\n```\n\n## Bundle size with code splitting\n\nThe \"build.js\" will produce a [metafile](https://esbuild.github.io/api/#metafile) when running `mix assets.deploy`.\n\nAnalyse it:\n\n\u003chttps://esbuild.github.io/analyze/\u003e\n\nThis displays:\n\n\u003cimg width=\"1372\" alt=\"Screenshot 2024-07-23 at 20 12 08\" src=\"https://github.com/user-attachments/assets/50cb0255-896b-4cf7-838b-a19ba198a3e6\"\u003e\n\nThe details without ApexCharts:\n\u003cimg width=\"1106\" alt=\"Screenshot 2024-07-24 at 10 02 32\" src=\"https://github.com/user-attachments/assets/6c323f59-32f0-4dee-b8de-66b529b39ef6\"\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndrean%2Fdemo-solidjs-websockets","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fndrean%2Fdemo-solidjs-websockets","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndrean%2Fdemo-solidjs-websockets/lists"}