Key Combination Events with Phoenix LiveView
8 May 2023

Key Combination Events with Phoenix LiveView

While developing web applications with Elixir, Phoenix, and LiveView you may come across some instances where you want to implement keyboard shortcuts for your application. LiveView makes this farely easy by providing phx-keydown, phx-keyup, phx-key, etc.. Where it gets a little tricky is when you want to implement a keyboard shortcut that includes a key combination. Think Slack’s Shift + Enter to go to a new line instead of sending the message, Command/Control + B to bold text, or maybe even a command prompt (think Apple’s Spotlight Search) to allow users to more easily access some of your features.

The issue with the key event binding is that by default it only allows you to detect one key press at a time. For instance, if you use phx-keydown, you can either specify a specific key for the event to be triggered by (phx-key) or look at the key value that comes through in your params. This is fine if you only need to trigger an event for one key. In order to use a key combination for an event, we need to add some additional code. Let’s dive in!

Requirements

  • Elixir v1.14
  • Phoenix v1.7
  • LiveView v0.18

Initial Setup

For this post, we’ll be displaying an imaginary list of keyboard shortcuts when the user presses Control + k. We’ll be using the newest versions of Elixir at the time of this post (v1.14), Phoenix (1.7), and LiveView (0.18). You can clone a version of the project here and follow along if you don’t want to generate your own. To generate your own project you can follow instructions here. Make sure you are on the latest version of Elixir to ensure you get Phoenix 1.7 and LiveView 0.18.

Utilizing phx-window-keydown

In our use case, we’re going to take advantage of phx-window-keydown since our key combination can be used at any point on our page. phx-keydown/up can be used if you only want to trigger the event while in a specific element(think Slack’s Shift + Enter while in the text input).

We’ll start by adding an event to a div element that wraps our whole page.

<div phx-window-keydown="open_cheatsheet">
  Hello World!
</div>

We’ll also need to add a new handle_event/3 in your LiveView. We’re also going to add an IO.inspect on the params to see our key presses come through.

def handle_event("open_cheatsheet", params, socket) do
  IO.inspect(params)
  {:noreply, socket}
end

You should see any key you press come through in your local server like this:

%{"key" => "k"}

Tracking the Control Key

As you can see, only one key comes through at a time. If we want to track more keys from the Javascript event being fired, we need to add some more code to the LiveSocket definition in app.js. There’s an optional metadata field that we can use to track more information about the keydown event. When you open up your app.js file you’ll most likely see a line that looks like this:

let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

We’ll be adding the metadata key after params.

let liveSocket = new LiveSocket("/live", Socket, {
  params: {
    _csrf_token: csrfToken
  },
  metadata: {
    keydown: (event, element) => {
      return {
        ctrlKey: event.ctrlKey
      }
    }
  }
})

Let’s break it down a little further. keydown is the event we want to pull more information from. It gives us the Javascript KeyboardEvent object and the HTML element that the keydown event is associated with. We’re telling the keydown event to return the value of the ctrlKey. If you want to see what else is available on the KeydownEvent, you can look here.

Now that we’re tracking the ctrlKey in metadata, we should see these values come through in our existing IO.inspect(params). If it is activated at the time of the keydown event, it will come through as true. Now if I press Control + k, I should see:

%{"ctrlKey" => true, "key" => "k"}

Magic! Now we can pattern match and fire our event when we get this value in params.

Handling Our Event

For our use case, we want to toggle a cheat sheet on the page. We’ll add a simple show_cheatsheet boolean to our assigns in our mount.

def mount(_params, _session, socket) do
  {:ok, assign(socket, show_cheatsheet: false)}
end

Now can modify our handle_event/3 to look for our key combination.

def handle_event("open_cheatsheat", %{"ctrlKey" => true, "key" => "k"}, socket) do
  {:noreply, assign(socket, show_cheatsheet: true)}
end

We’ll also need a fallback handle_event/3 when our pattern match fails. We don’t need to do anything in this case so we can simply have:

def handle_event("open_cheatsheat", _params, socket) do
  {:noreply, socket}
end

Displaying the Cheatsheet

Voila! Now we can use our variable to toggle the HTML and display the cheatsheet!

import YourAppNameWeb.CoreComponents
alias Phoenix.LiveView.JS
<div phx-window-keydown="open_cheatsheat">
  <h1 class="text-center mt-16">Hello World!</h1>
  <p class="text-center mt-8">Press "Control + k" to view keyboard shortcuts.</p>
  <.modal id="cheatsheet" :if={@show_cheatsheet} show on_cancel={JS.navigate("/")}>
    <ul>
      <li>Shortcut 1</li>
      <li>Shortcut 2</li>
      <li>Shortcut 3</li>
      <li>Shortcut 4</li>
    </ul>
  </.modal>
</div>

Alt Text

Troubleshooting

The source code for this can be found here. If you’re on a previous version of LiveView, you’ll need to use an if statement in the HTML.

<div phx-window-keydown="open_cheatsheat">
  <h1 class="text-center mt-16">Hello World!</h1>
  <p class="text-center mt-8">Press "Control + k" to view keyboard shortcuts.</p>
  <%= if @show_cheatsheet do %>
    <ul>
      <li>Shortcut 1</li>
      <li>Shortcut 2</li>
    </ul>
  <% end %>
</div>

The above handle_event/3 code does not handle an uppercase “K”. If you want to make the event case insensitive, and without pattern matching, you could use this instead:

def handle_event("open_cheatsheat", %{"ctrlKey" => ctrl_key, "key" => key}, socket) do
  socket =
    if String.downcase(key) == "k" && ctrl_key == true do
      assign(socket, show_cheatsheet: true)
    else
      socket
    end

  {:noreply, socket}
end

Related Posts

Want to learn more about the work we do?

Explore our work

Ready to start your software journey with us?

Contact Us