Advent of JavaScript, Day 10

Advent of JS Homepage

Challenge #10 is creating a code verification screen, like you see with 2-factor authentication:

Screenshot of app

I decided to mix it up and use Svelte instead of Vue for this one!

With the project files downloaded and codesandbox‘d into a live CodeSandbox, I’m ready to get going!

(CodeSandbox lets you drag-and-drop files to upload, too!)


User Requirements

Again, let’s start with the User Requirements and speculate how I can solve these:

  • type in a digit and automatically be taken to the next input

I like these UIs because they make the length requirements clear via distinct inputs. However, what’s weird is when you fix one of the digits out of order. Normally, the text would be inserted, but with a fixed amount, then what?

I’m going to limit maxLength to 1 and onKeyUp find the next <input> and .focus() it.

  • paste in a 4-digit code

There’s a paste event that I can listen to.

When this happens only on the 1st input, hopefully I can capture the full code and replace all <input> values.

Wiring it Up

  1. Adapting to Svelte

    Unlike my previous Vue implementations, Svelte’s index.html is bare, just like most SPAs.

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>10 - 4 Digit Code || Advent of JavaScript</title>
    
        <link rel="stylesheet" href="public/bundle.css" />
      </head>
    
      <body>
        <script src="bundle.js"></script>
      </body>
    </html>

    To be fair, Vue would also be bare if I were using templates & components instead of progressive-enhancement.

    But since this is my 1st bundled challenge, it was a bit of a shock to not just “wire things up” and instead have to port code.

    I copy/pasted what was inside the original index.html’s <body> into App.svelte:

    <script>
      import './public/styles.css'
    </script>
    
    <div class="wrapper">
      <h1>Authorization Code</h1>
      <p>Please enter the code that we sent to (***) *** - 2819.</p>
      <form action="">
        <div class="fields">
          <input type="text" maxlength="1" value="1" name="verification_1" />
          <input type="text" maxlength="1" value="" name="verification_2" />
          <input type="text" maxlength="1" value="" name="verification_3" />
          <input type="text" maxlength="1" value="" name="verification_4" />
        </div>
        <button>Verify</button>
      </form>
    </div>

    Just like in modern bundlers like esbuild, rollup, & webpack, I can import "styles.css" and the bundler figures it out for me.

    (I prefer this over shoving everything in /public – colocation is a good thing.)

  2. bind:value

    Rather than 4 distinct <input> fields, I used Svelte’s {#each} & bind:value={digits[i]}:

    <script>
      let digits = new Array(4)
    </script>
    
    {#each digits as digit, i}
    <input
      bind:value="{digits[i]}"
      min="1"
      max="9"
      maxlength="1"
      size="1"
      type="number"
    />
    {/each}

    Note – There’s apparently no way for vanilla HTML to restrict the value to a single 1-9 digit.

  3. Autofocus Inputs

    Using bind:this, I assigned each <input> to the inputs array and advanced on:input:

    <script>
      import './styles.css'
    
      let digits = new Array(4)
      let inputs = new Array(4)
      $: code = digits.join('')
    
      function nextInput(event) {
        const input = event.target
        const i = inputs.indexOf(input)
        const nextInput = inputs[i + 1]
    
        if (nextInput) {
          nextInput.focus()
        } else {
          input.blur()
        }
      }
    </script>
    
    {#each digits as digit, i}
    <input
      bind:this="{inputs[i]}"
      bind:value="{digits[i]}"
      on:input="{nextInput}"
      min="1"
      max="9"
      maxlength="1"
      size="1"
      type="number"
    />
    {/each}
  4. Handling paste events

    Thanks to MDN, this was a simple on:paste|preventDefault bound to handlePaste:

    function handlePaste(event) {
      let paste = (event.clipboardData || window.clipboardData).getData('text')
      digits = paste.split('').slice(0, 4)
    
      event.target.blur()
    }

    I replaced digits with the value of paste (e.g. "1234"). Svelte is really nice in that it lets me mutate values and I don’t need to manage state directly.

    Then, I .blur() so that extra input isn’t accidentally entered.

    I decided to allow paste within any of the inputs since the expectation is that the entire code will be in the clipboard anyway.

    <input
      bind:this="{inputs[i]}"
      on:input="{handleInput}"
      on:paste|preventDefault="{handlePaste}"
      ...
    />
  5. 1-Digit Only

    Eventually I decided to have on:input force the single-digit:

    input.value = input.value[0]

    Yes, this assumes the 1st digit is the correct one, but I was getting annoyed by this quirk.

Conclusion

I have to say, despite Svelte not having a CDN, run-time version, it was a breeze to work with in CodeSandbox and I really enjoyed the strict linting.

This was especially true when I did {#each item in items} and the linter said, “Did you mean as?”

I’m going to give Svelte another try soon and see when (and if) I run into limitations…