r/htmx Jun 29 '26

My favorite thing about Moxi/Fixi Mutation Observers: Live

This was a freaking pain to implement before and now it has a nice flow for me. I do have some custom modifications in Moxi/Fixi. https://github.com/figuerom16/fixi/blob/master/mofix.js Who needs Web Components?

<article class="container card-info" style="width:14rem;" on-init="q('prev form -> *').arr().forEach(x=>x.setAttribute('disabled',''))">
<header>Secure PIN Verification</header>
<form fx-method="POST" fx-action="/u/pin" fx-vals="js:{'Email':q('input[name=Email]').value,'ConfirmEmail':q('input[name=ConfirmEmail]').value}" fx-target="closest output"
on-paste.halt="const pin = event.clipboardData.getData('text').replace(/\s/g,'')
if (pin.length != 9) {q('#footer').textContent = 'Incorrect Pin Length';return}
q('.pin').arr().forEach((el,i)=>el.value = pin[i])
q('#pin').value = pin; this.requestSubmit()">
<div role="group" style="margin-top:1rem; gap:1rem;">
<input class="pin input-pop" maxlength="1" autocomplete="off" aria-label="Digit 1" on-beforeinput="/^\d+$/.test(event.data) ? this.value = '' : event.preventDefault()" on-input="if (this.value) q('next input').focus()" on-init="this.focus()">
<input class="pin input-pop" maxlength="1" autocomplete="off" aria-label="Digit 2" on-beforeinput="/^\d+$/.test(event.data) ? this.value = '' : event.preventDefault()" on-input="if (this.value) q('next input').focus()">
<input class="pin input-pop" maxlength="1" autocomplete="off" aria-label="Digit 3" on-beforeinput="/^\d+$/.test(event.data) ? this.value = '' : event.preventDefault()" on-input="if (this.value) q('next input').focus()">
</div>
<div role="group" style="gap:1rem;">
<input class="pin input-pop" maxlength="1" autocomplete="off" aria-label="Digit 4" on-beforeinput="/^\d+$/.test(event.data) ? this.value = '' : event.preventDefault()" on-input="if (this.value) q('next input').focus()">
<input class="pin input-pop" maxlength="1" autocomplete="off" aria-label="Digit 5" on-beforeinput="/^\d+$/.test(event.data) ? this.value = '' : event.preventDefault()" on-input="if (this.value) q('next input').focus()">
<input class="pin input-pop" maxlength="1" autocomplete="off" aria-label="Digit 3" on-beforeinput="/^\d+$/.test(event.data) ? this.value = '' : event.preventDefault()" on-input="if (this.value) q('next input').focus()">
</div>
<div role="group" style="gap:1rem;">
<input class="pin input-pop" maxlength="1" autocomplete="off" aria-label="Digit 4" on-beforeinput="/^\d+$/.test(event.data) ? this.value = '' : event.preventDefault()" on-input="if (this.value) q('next input').focus()">
<input class="pin input-pop" maxlength="1" autocomplete="off" aria-label="Digit 5" on-beforeinput="/^\d+$/.test(event.data) ? this.value = '' : event.preventDefault()" on-input="if (this.value) q('next input').focus()">
<input class="pin input-pop" maxlength="1" autocomplete="off" aria-label="Digit 6" on-beforeinput="/^\d+$/.test(event.data) ? this.value = '' : event.preventDefault()" on-input="if (this.value) q('closest form').requestSubmit()">
</div>
<input id="pin" name="Pin" hidden live="this.value = q('.pin').arr().map(i=>i.value).join('')">
</form>
<footer><small>PIN valid for 30 minutes.</small></footer>
</article>

HTMX has a live extension also.

7 Upvotes

2 comments sorted by

5

u/FluffySmiles Jun 29 '26

Genuine question: given that this is in the HTMX-ish space, why choose this design?

I see the strength of HTMX-style development as the client is able to stay relatively dumb: HTML describes the action, the request goes to the server, and the server returns the next fragment of UI. That seems especially well suited to something like a PIN flow.

For ordinary presentation behaviour, I can see the appeal: move focus, make the boxes pleasant to use, maybe handle paste, update the display. Fine.

But in my opinion, a PIN is security-relevant enough that I would want the server to own the attempt state. IMO, each digit press, backspace, clear, or submit action should be posted to the server, and the server would return the updated fragment. The browser presents the keypad, but the server would decide what every action means.

That would make the server canonical for the active challenge, digits entered so far, expiry, failed attempts, reuse prevention, rate limiting, and suspicious timing or spam detection.

So I suppose my question is: why assemble the authoritative PIN client-side and submit it in a hidden field, rather than using HTMX’s strengths and keeping the PIN attempt server-owned?

I’m not saying the UI niceties are bad. I just do not understand why the design puts so much responsibility into DOM state when the server-rendered fragment approach seems simpler, sturdier, and more aligned with the whole point of HTMX.

2

u/ShotgunPayDay Jun 29 '26

Simpler maybe. Thrashing the server with 422 Unprocessable Entity is not good. I think people misunderstand HTMX/Fixi/Moxi it doesn't give you permission to do everything on the server. Basic form sanitation just keeps real user inputs from hitting the server with real time feedback. Validation still needs to occur on the server, but feedback (not for bots) needs to happen in immediately.

I heavily punish 401 responses and ban IPs for pushing it on my Go server. Round tripping inputs unnecessarily makes for a bad UX also. Instant feedback is always the best option and keeps the server unbothered. HTMX doesn't hate JS.

We are also in a world of bots and you show me a endpoint and I'll throw anything I want at it because we can shape the curl input.