This week I needed to make a really big form. It’s really big. Most people don’t like forms, but I’ve always been a bit of a contrarian, and my approach for saving user input on this form got me thinking about how much effort goes into things people take for granted. In code, I mean. Life is way out of scope for a blog post.
The general principle here is that you’re updating a load of quantities in a form, to build an order of sorts. The fields need to update on keypress, but they also need one final update on blur, to be sure.
Now we have two problems here: firstly, we don’t want to be hitting our server every time someone types something. Your customer might type “1”, or “12”, or “120” - I don’t want to update my order 3 times here if I don’t have to, or the guy who wrote my backend (me) is going to yell at me. And secondly, our updates can come from two places. We don’t want them crashing or double-updating.
The following flowchart roughly demonstrates the lifecycle of a user input in a single field, all the way to it being successfully updated. I’ve annotated this with numbers so I can refer to specific steps.
And that’s it. If it seems complicated, that’s because it kinda is. For something which, on the surface, might be “well I just typed the value - save it”, there’s a lot more to making it a pleasant and robust experience for your user.
And here’s some example code that I’ve put together to demonstrate this flowchart. You can skip it if that bores you.
One thing I haven’t included in here, which I should, is onbeforeunload. This is something that I should be running when my timers run to help ensure that the user waits for my asynchronous calls to run before closing their browser tab or navigating away.
The main reason this isn’t part of this code, is this isn’t where I’d do it in practice. It’s out of scope for the controller of a single field, of which there are many on the page, to control whether tab-closing is blocked. There should be a controller that listens for timers being started, and then adds its own callbacks if any timer in its domain is running. If this is a Field controller, you would also have a FieldGroup controller which listens for Fields that start timers, and if any of its Fields have timers, it blocks the tab from closing. Then when its last Field timer has run out, and no-one is updating anything, it’s now safe to close the tab.
Of course, the user can just plough through this anyway, but the main hope is that the time it takes the user to process the popup they see, allows the browser enough time to finish all of its requests, rendering it somewhat redundant and simultaneously incredibly effective! Funny how that works.
By the way, I love Stimulus
Am I wrong? Is there a better way to do this that doesn't involve installing some npm package? Let's talk.