Part #1 of our series on building web console for Elixir & Phoenix applications with Phoenix LiveView.

In this series:

  1. Part 1: Project set up and simple code executor
  2. Part 2: Making imports and macros work
  3. Part 3: Managing multiple supervised sessions
  4. Part 4: Making it real-time

Video version

Watch the video version below or or directly on YouTube for a complete step-by-step walkthrough.

Setting up the project

The project set up has been based on Phoenix LiveDashboard. I have copied over the LiveDashboard files, batch-renamed the modules and files with sed and find and removed all of the business logic, templates and assets specific to Phoenix LiveDashboard.

What I was left with is a simple infrastructure for a generic LiveView project, that can be mounted under a route of the user’s choice in the application’s Router. We will cover this part in future episodes when the Backdoor will be actually useful to be embedded in production apps.

You can track the initial set up and addition of Tailwind / dummy layout for the Backdoor app by looking at the 4 oldest commnits in project history.

Backdoor LiveView

We start off with simple LiveView to render the layout:

    defmodule Backdoor.BackdoorLive do
        use Phoenix.LiveView,
            layout: {Backdoor.LayoutView, "live.html"},
            container: {:div, class: "font-sans antialiased h-screen flex"}

        use Backdoor.Web, :live_view

        @impl true
        def mount(_params, _session, socket) do
            {:ok, socket}
        end

        @impl true
        def render(assigns) do
            ~L"""
            <div class="font-sans antialiased h-screen flex w-full">
                <!-- Sidebar: -->
                <div class="flex-none w-1/6 pb-6 hidden md:block">
                    Sidebar
                </div>
                <!-- Main column -->
                <div class="flex-1 flex flex-col overflow-hidden">
                    <!-- Titlebar -->
                    <div class="border-b flex px-6 py-2 items-center flex-none">
                        <h3 class="text-grey-darkest mb-1 font-extrabold">Title bar</h3>
                    </div>
                    <!-- Output -->
                    <div class="px-6 py-4 flex-1 overflow-y-scroll">
                        <%= for i <- (1..100) do %>
                            <div class="flex items-start text-sm">
                                <pre>This is a text message</pre>
                            </div>
                        <% end %>
                    </div>
                    <!-- Input -->
                    <div class="pb-6 px-4 flex-none">
                        <div class="flex rounded-lg border-2 border-grey overflow-hidden">
                            <span class="text-xl text-grey border-r-0 border-grey p-2">
                                iex&gt;
                            </span>
                            <input type="text" class="w-full px-4" placeholder="Message #general" />
                        </div>
                    </div>
                </div>
            </div>
            """
        end
    end

Phoenix LiveDashboard authors came up with a way to start testing Phoenix application, which includes and mounts the library: dev.exs file. With this file present, we have a handy method to start the development project with mix dev.

When visiting http://localhost:4000/backdoor one would see the 2-column template we will use for our Backdoor web console:

Initial dummy layout

Executing Elixir code

In order to execute Elixir code we need to do 4 things:

1: Set up bindings, which will become updated after each executed block of code. We will start with simple empty list for the purpose of this demonstration:

...
    @impl true
    def mount(_params, _session, socket) do
    {:ok, socket |> assign(bindings: [], output: [])}
end
...

2: Replace the dummy list of output messages with actual code that takes individual messages from socket.assigns.output list, and prints them all out to the user:

    ...
    <!-- Output -->
    <div class="px-6 py-4 flex-1 overflow-y-scroll">
        <%= for message <- @output do %>
            <div class="flex items-start text-sm">
                <pre><%= message %></pre>
            </div>
        <% end %>
    </div>
    ...

3: Turn the dummy input into LiveView form that sends code to back-end on Enter:

    ...
    <!-- Input -->
    <div class="pb-6 px-4 flex-none">
        <div class="flex rounded-lg border-2 border-grey overflow-hidden">
            <span class="text-xl text-grey border-r-0 border-grey p-2">
                iex&gt;
            </span>
            <%= f = form_tag "#", [phx_submit: :execute, class: "flex w-full"] %>
                <%= text_input :command, :text, [placeholer: "Write Elixir code to execute and hit 'Enter'...", value: "", class: "w-full px-4 outline-none"] %>
            </form>
        </div>
    </div>
    ...

4: Finally, execute the code thanks to Elixir’s own Code module:

...
    def handle_event("execute", %{"command" => %{"text" => command}}, socket) do
        {:ok, ast} = Code.string_to_quoted(command)
        {result, bindings} = Code.eval_quoted(ast, socket.assigns.bindings, [])

        output = socket.assigns.output ++ ["iex> " <> command] ++ [inspect(result)]
        {:noreply, socket |> push_event("command", %{text: ""}) |> assign(bindings: bindings, output: output)}
    end
...

With the full source code for resulting LiveView we can now execute the one-line Elixir code, including sending messages, starting processes, getting system info and defining modules:

Our Backdoor working!

Next steps

Our little web console works when you enter valid Elixir one-liners. It doesn’t handle syntax errors, exceptions nor any other problems correctly yet, however. In any failure the whole LiveView will crash and restart. In such a case the output from the commands we already executed is going to be lost as well. It doesn’t capture and output anything sent to :stdout or :stderr either, so puts statements and Logger entries do not get displayed to the user.

In future we will correct these issues, as well as add ability to start sessions within the cluster and persist the sessions beyond the lifespan of LiveView / connected browser, add ability to send multi-line Elixir code for execution, add syntax highlighting, command history and better handle output.

Notes

Watch the screencast directly on YouTube

You can find this and future videos on our YouTube channel.

The full code from this episode can be found on GitHub

Post by Hubert Łępicki

Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.