Premise
I was recently chatting with someone about an application for use in the mining sector. One of the topics was a simple input form for operators who notoriously hate using technology to input data. One of the terms used was ‘fat-fingered operators’ (hence the title of the post). Before anyone gets upset, it was not meant as a derogatory term but rather as a flag to think of simple, robust methods to make peoples jobs more comfortable.
We talked about QR Codes, Image Recognition, and AI to automatically understand what the operator was looking at and what type of data they are trying to capture and record. It would be fun to work on all of these solutions, but I started to think about a fast, simple solution that might be viable.
Idea
I had recently seen someone walking along using the voice to text function on their phone, and it occurred to me that it might be a simple solution to save our operators having to search or type input when recording data. I spent a couple of hours playing around with Liveview and came up with the following POC:
The Monitoring device input is an autocomplete text-input
, using my iPhone; I selected the dictation icon and said ‘orange’ which provided a list of options. After selecting the device, I entered the value using a large number pad.
The Code
I started by creating a Phoenix Live application called fat_fingers
> mix phx.new --live --no-ecto
I am currently in love with Tailwind so spent a little bit of time setting it up. I have removed the CSS from the code below for simplicity.
I modified the default PageLive
for my POC adding in assigns to capture matches
, value
, and record the device
once selected.
defmodule FatFingersWeb.PageLive do
use FatFingersWeb, :live_view
alias FatFingers.Devices
@impl true
def mount(_params, _session, socket) do
socket =
assign(socket,
device: nil,
matches: [],
value: "_"
)
{:ok, socket}
end
end
I added a module to act as a search context
defmodule FatFingers.Devices do
def suggest(""), do: []
def suggest(val) do
Enum.filter(list(), fn d ->
String.contains?(String.downcase(d), String.downcase(val)) end
)
end
defp list() do
[
"Black",
"Green",
"Blue",
"blue",
"Red",
"Orange",
"Purple"
]
end
end
I knew I wanted to allow the user to search for a device
and provide a list to select from so added an event handler to return matches and another one to handle the selection.
@impl true
def handle_event("search", %{ "device" => device }, socket) do
socket = assign(socket, matches: Devices.suggest(device))
{:noreply, socket}
end
@impl true
def handle_event("select_match", %{ "match" => match}, socket) do
socket = assign(socket, device: match, matches: [])
{:noreply, socket}
end
The next step was to allow the user to enter a value using a large number pad. Because I am simulating a cursor for the value
as a default this event handler tests to see if value
is in the default state, otherwise, it appends the added value.
@impl true
def handle_event("number", %{ "number" => number}, socket) do
cond do
is_nil(socket.assigns.device) -> {:noreply, socket}
true ->
case socket.assigns.value do
"_" ->
socket = assign(socket, value: number)
{:noreply, socket}
_ ->
socket = assign(socket, value: socket.assigns.value <> number)
{:noreply, socket}
end
end
end
Finally, I wanted to allow the user to clear the value if they made an error.
@impl true
def handle_event("clear", _, socket) do
socket = assign(socket, value: "_")
{:noreply, socket}
end
In the page_live.html.leex
I created a form and a large number pad.
<form class="" phx-change="search">
<div>
<label>Monitoring Device</label>
<div>
<input value="<%= @device %>" placeholder="Device name ..."/>
<div>
<ul>
<%= for m <- @matches do %>
<li select-none phx-value-match="<%= m %>">
<a href="#"><%= m %></a>
</li>
<% end %>
</ul>
</div>
</div>
</div>
<div>
<label>Enter Value:</label>
<div>
<div><%= @value %></div>
</div>
<input type="hidden" id="value" name="value" value="<%= @value %>">
</div>
<ul class="grid gap-6 grid-cols-3 mt-4 px-2">
<%= for n <- 1..9 do %>
<a href="#">
<div phx-click="number" phx-value-number="<%= n %>" >
<span><%= n %></span>
</div>
</a>
<% end %>
<a href="#">
<div phx-click="number" phx-value-number=".">
<span>.</span>
</div>
</a>
<a href="#">
<div phx-click="number" phx-value-number="0">
<span>0</span>
</div>
</a>
<a href="#">
<div phx-click="clear">
Clear
</div>
</a>
</ul>
<button type="submit">
<span class="text-2xl">Save</span>
</button>
</form>
I love working with Liveview, and the more I use it, the more I am impressed with its simplicity and efficiency.
You can find the project code here