163 lines
5.3 kB
1
tags: programming,elixir,fun
2
3
# A Quick Typewriter-Set Letter Project/Experiment
4
5
I decided to start off by coding up a vim-style scroller for live skeets.
6
7
Let's say I have a context called `Social`, with a function `sample/1` that returns lists of skeets.
8
9
Who cares where they come from, its a simple enough API that we can use as the basis here.
10
11
I wanted to have a simple setup where we would scroll through a list of posts with `j` and `k`.
12
I make that database table of skeets get populated live.
13
I ingest the entire network, and every second I save about 1 of the 60ish skeets coming over the network.
14
So this page is always fresh, and you will want to quickly mindlessly scroll.
15
16
I started off with a pretty simpe LiveView:
17
18
```elixir
19
defmodule BlogWeb.VimTweetsLive do
20
use BlogWeb, :live_view
21
22
@window_size 25
23
24
def mount(_params, _session, socket) do
25
tweets = Social.sample(100) |> Enum.map(& &1.skeet)
26
27
socket = socket
28
|> assign(
29
cursor: 0,
30
tweets: tweets,
31
visible_tweets: Enum.take(tweets, @window_size),
32
page_title: "Thoughts and Tidbits Blog: Bobby Experiment - vim navigation",
33
meta_attrs: @meta_attrs
34
)
35
36
{:ok, socket}
37
end
38
39
def handle_event("keydown", %{"key" => "j"}, socket) do
40
new_cursor = min(socket.assigns.cursor + 1, length(socket.assigns.tweets) - 1)
41
visible_tweets = get_visible_tweets(socket.assigns.tweets, new_cursor)
42
{:noreply, assign(socket, cursor: new_cursor, visible_tweets: visible_tweets)}
43
end
44
45
def handle_event("keydown", %{"key" => "k"}, socket) do
46
new_cursor = max(socket.assigns.cursor - 1, 0)
47
visible_tweets = get_visible_tweets(socket.assigns.tweets, new_cursor)
48
{:noreply, assign(socket, cursor: new_cursor, visible_tweets: visible_tweets)}
49
end
50
51
def handle_event("keydown", _key, socket), do: {:noreply, socket}
52
53
defp get_visible_tweets(tweets, cursor) do
54
start_idx = max(0, cursor - 2)
55
Enum.slice(tweets, start_idx, @window_size)
56
end
57
58
def render(assigns) do
59
~H"""
60
<.head_tags meta_attrs={@meta_attrs} page_title={@page_title} />
61
<div class="mt-4 text-gray-500">
62
Cursor position: <%= @cursor %>
63
</div>
64
<div class="p-4" phx-window-keydown="keydown">
65
<div class="space-y-4">
66
<%= for {tweet, index} <- Enum.with_index(@visible_tweets) do %>
67
<div class={"p-4 border rounded #{if index == 2, do: 'bg-blue-100'}"}>
68
<%= tweet %>
69
</div>
70
<% end %>
71
</div>
72
</div>
73
"""
74
end
75
end
76
```
77
78
Let's break this down into pieces.
79
80
### Key Events
81
We can really easily intercept key events in Phoenix/LiveView.
82
83
Let's take a look at the core of how that works:
84
85
```elixir
86
# liveview definition with mount
87
88
require Logger
89
90
def handle_event("keydown", _key, socket), do: {:noreply, socket}
91
Logger.info("Pressed: #{key}")
92
{:noreply, socket}
93
end
94
95
def render(assigns)
96
~H"""
97
<div class="p-4" phx-window-keydown="keydown">
98
hi
99
</div>
100
"""
101
end
102
```
103
104
Now, this starts off with `phx-window-keydown` which is set to `"keydown"`.
105
106
We can get [key events](https://hexdocs.pm/phoenix_live_view/bindings.html#key-events) from the provided APIs using this.
107
108
```
109
The onkeydown, and onkeyup events are supported via the phx-keydown, and phx-keyup bindings.
110
Each binding supports a phx-key attribute, which triggers the event for the specific key press.
111
If no phx-key is provided, the event is triggered for any key press.
112
When pushed, the value sent to the server will contain the "key" that was pressed, plus any user-defined metadata.
113
For example, pressing the Escape key looks like this:
114
115
%{"key" => "Escape"}
116
```
117
118
Great, so with this, we are now logging what we are pressing.
119
120
So now, we can wire into `j` and `k` and make it so the "visible" batch of skeets is offset by the change in index.
121
122
With that change, we make a new "cursor" which is just an index position, and a new batch of "visible skeets" that are simply the ones from the batch we have deemed currenty viewable.
123
124
This all is quite simple and elegant, in my opinion.
125
126
If we go and look at the HTML we can see how this ties together so simply:
127
128
```elixir
129
def render(assigns) do
130
~H"""
131
<.head_tags meta_attrs={@meta_attrs} page_title={@page_title} />
132
<div class="mt-4 text-gray-500">
133
Cursor position: <%= @cursor %>
134
</div>
135
<div class="p-4" phx-window-keydown="keydown">
136
<div class="space-y-4">
137
<%= for {tweet, index} <- Enum.with_index(@visible_tweets) do %>
138
<div class={"p-4 border rounded #{if index == 2, do: 'bg-blue-100'}"}>
139
<%= tweet %>
140
</div>
141
<% end %>
142
</div>
143
</div>
144
"""
145
end
146
```
147
148
We are handling keydown and for the indexed tweet, highlighting its coor.
149
Then if we key up or down, we redefine whats viewable here, and the rest are just displayed.
150
151
What we end up with is beautifully simple looking.
152
153
You can check it out [here](https://thoughts-and-tidbits.fly.dev/vim).
154
155
## Making this
156
This all was fun, and inspired something else:
157
158
A letter writer.
159
Where you cannot copy and paste.
160
You must take the time to write.
161
You must be truly original and keep your mistakes except for backspace being allowed.
162
163
That project is partially shipped and I will write more about it later.
164