home overview tags

Herman verschooten

These things I recently learned, that may be useful to myself and others in the future.

Triggering page reloads in development

When I am writing these articles on my dev machine, it pains me to have to call the refresh-link, each and everytime I save the file. Wouldn't it be great if I could do this automatically?

What to use to watch my files?

There is javascript utility called chokidar, but I do not want to add npm-dependencies.
Can I write something in Go or Rust, neither language I am familiar with.
There is something written that you can use from Ruby.

But wait a minute! 🤦 Phoenix does this already!

It has a definition within your config/dev.exs that tells it to watch certain directories and recompile/reload when one of the files therein changes.

Ok, let's investigate... It's in phoenix_live_reload, hmm, hmm, aha, there it is, it uses FileSystem to watch for file changes.

FileSystem

{:ok, pid} = FileSystem.start_link(dirs: [article_path])
FileSystem.subscribe(pid)

This handles the watching part, and this the listening part.

    def handle_info({:file_event, _watcher_pid, {_path, _events}}, state) do
      IO.inspect("File changed, reloading articles")
      {:noreply, state}
    end

Now I only want this in development mode, so my ArticleServer, which is a GenServer, can handle this for me.

  if Mix.env() == :dev do
    def init(_) do
      article_path = Application.get_env(:til, :article_path)
      {:ok, pid} = FileSystem.start_link(dirs: [article_path])
      FileSystem.subscribe(pid)

      {:ok, %__MODULE__{}, {:continue, :load_articles}}
    end

    def handle_info({:file_event, _watcher_pid, {_path, _events}}, _state) do
      IO.inspect("File changed, reloading articles")
      {:noreply, load_articles()}
    end
  else
    def init(_) do
      {:ok, %__MODULE__{}, {:continue, :load_articles}}
    end
  end

Triggering the reload

Now for the final part we need to tell our browser through phoenix_live_reload that the page should reload.
I started digging through the code, asked on Slack for a documented way to do this, but in the end it was sooooo easy!
The live reloader lives in an iframe and listens on a channel, I just needed to check what is being sent on a page reload.

[null,null,"phoenix:live_reload","assets_change",{"asset_type":"heex"}]

So let's do the same thing.

TilWeb.Endpoint.broadcast("phoenix:live_reload", "assets_change", %{asset_type: "heex"})

And it works! And as @kernel pointed out, this is the closest I am going to get to a 'supported' way, as the channel api is public.

Full code

So here is the complete code I added:

  if Mix.env() == :dev do
    def init(_) do
      article_path = Application.get_env(:til, :article_path)
      {:ok, pid} = FileSystem.start_link(dirs: [article_path])
      FileSystem.subscribe(pid)

      {:ok, %__MODULE__{}, {:continue, :load_articles}}
    end

    def handle_info({:file_event, _watcher_pid, {_path, _events}}, _state) do
      IO.inspect("File changed, reloading articles")
      TilWeb.Endpoint.broadcast("phoenix:live_reload", "assets_change", %{asset_type: "heex"})
      {:noreply, load_articles()}
    end
  else
    def init(_) do
      {:ok, %__MODULE__{}, {:continue, :load_articles}}
    end
  end

Writing this article was a pleasure with my page auto-reloading on each save.
And it will also be the first article to be published with my newly added Github Action.