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>
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