Learning Elm Part 13 - Bug fixes
After I finished my last post and deployed, I noticed that on my iPhone, it looked weird, and when I added it to my Home Screen (PWA) it looked worse. To top it off, the infinite scroll was broken! So much for end-to-end tests!
Fixing infinite scrolling
Let’s start with that last one. While developing, I usually don’t use encryption because reloading means typing the password. No fun. The bug was in my encryption abstraction. Pagination relies on the limit to actually work. I forgot that I was filtering out the encryption mark. This resulted in the code thinking “Oh, there are no more documents!”. Given that I don’t have a personal Mac, it is quite hard to debug iPhone stuff. After a bit of deploy/test, I found the bug.
So now, my first end-to-end test looks like this:
Feature: Pagination should be automatic when users scroll down
Scenario: A user with many, many transactions
Given an encrypted app with password "my cool password"
And I have 200 test transactions with description Transaction1, Transaction2...
Then I see 50 transactions
When I scroll to the bottom
Then I see 100 transactions
And no transaction is repeated
And sure enough, it fails.
The fix, is to increase the limit by one if we receive it, and truncate back to the requested limit. If we haven’t seen the encryption mark, we’d be returning an extra document.
// src/Db.js
class EncryptedDb extends Db {
//...
async allDocs(options) {
let truncate = null;
if (options && options.limit !== undefined) {
truncate = options.limit;
options.limit += 1;
}
const result = await this.db.allDocs(options);
const rows = result.rows.filter((row) => row.id != ENCRYPTED_ID);
for (const row of rows) {
if (row.doc) {
row.doc = await this.encryption.decrypt(row.doc);
}
}
result.rows = truncate ? rows.slice(0, truncate) : rows;
return result;
}
//...
}
Now the test passes, one less bug! As you can see, off-by-one errors are not only limited to arrays and pointers.
Fixing the UI
My CSS hack to have a bottom bar worked more or less ok for my desktop browsers. But it did not work on the iPhone. The default is to have a bottom bar which doesn’t play nice, plus the buttons were just too large:

When used as a PWA, which is what I will be doing, the scroll events went to the body (i.e. it tried to scroll the whole body) and that looked bad and got in the way of the real scrolling.
The design is so simple… two <div/> elements (content + bar) inside of another </div>! So I reread a bit about the flexbox and asked ChatGPT, and sure enough, it recommended something that I had thought I had already tried:
- Set
display: flexto the outer container - Set
flex: 1to the main-content so it occupies all the remaining space - Fix my bottom-bar to the bottom.
This time I copied/pasted and used no Semantic UI classes. While I was at it, I asked how to prevent the body from scrolling:
.container {
display: flex;
flex-direction: column;
height: 100%;
}
.main-content {
flex: 1; /* Grow to fill remaining space */
overflow-y: auto; /* Enable vertical scrolling */
overscroll-behavior: contain; /* Prevent body scrolling */
}
.bottom-bar {
flex-shrink: 0; /* Do not shrink */
display: flex;
}
It worked! Thanks ChatGPT.
Adding dropdown menus
I decided to continue digging myself info the Semantic UI hole and implement the dropdown menus. For this to work, I need to create my Elm port and call it when rendering the menu:
-- src/Main.elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SetPage EditSettings ->
( { model
| currentPage = EditSettings
, editSettingsState = getSettingsFormInput model.editSettingsState
}
, initializeMenus ()
)
-- (...)
port initializeMenus : () -> Cmd msg
And in my JavaScript src/ElmPort.js:
app.ports.initializeMenus.subscribe(function () {
requestAnimationFrame(function () {
$(".needs-js-menu").dropdown();
});
});
Two things to note:
- If I use the
$('.ui.dropdown').dropdown()as per the documentation, it will also activate the Semantic UI dropdowns for my<select>- and I did not want that. So I added my ownneeds-js-menuclass to target only the ones I want to be transformed. - The
requestAnimationFrame()is needed to synchronize the Elm runtime with the JavaScript. It assures that the DOM elements have been created before this code runs. You can read about it in Richard Feldman’s book Elm in Action. Without it, the JavaScript may get called before the DOM is there, so it does nothing.
With that in place, I moved the buttons to a dropdown:

As always, you can find a deployed version here, with the Elm Debugger here, and the source code here.
Bye!