Code input breakdown

I went to Starbucks the other day. On the receipt was a code to enter on their website, so I went there on my computer. As a front-end developer, I was impressed with the behavior of the form. Unless you do this for a living and/or hobby, you probably might not be.

It looks like a simple enough form, with a much-disliked Captcha. (Inspection reveals that they’re using Google’s ReCaptcha, customized to use their own layout. Pretty cool, guys.) But once I start filling out the “enter your code” input field, I see something delightful.

Every four characters entered on the form, it automatically inserts a space. That looks nice. Having built entry code fields before, this intrigued me. I wondered if they were using multiple fields to simulate a single field, so I decided to dig a little deeper.

A look at the source reveals that it is indeed just a single field. Although you can see they must have tried the separate-fields method prior to deployment, because the markup is commented out.

Don’t leave your commented-out markup in production code, kids.

Multiple fields as a single field

I’ve seen the multiple-fields method employed before, and it’s pretty simply to set up. (In my own job, we eventually decided not to use it because it causes keyboard jumping in touch devices.) I coded up a quick demo, which you can see here:

That outer border is actually just a container, and the input fields themselves would be set up to have no border or outline. (You can toggle the borders by clicking the button in the demo.) From there, it’s just setting up event interceptors to make the whole thing act like a single entry field. (Even so, there is some UX weirdness. But I digress.)

Single field

Anyway, on to the goodness. It took a little more digging to reveal how the single input field behaves, but it’s ultimately quite straightforward, if not simple.

“Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.”

If you’ve gotten this far, I’m sure you’ll recognize this quote. It’s often used to argue against the use of regular expressions, but the point is (as Jeff Atwood has said), it’s not warning against the use of regex, just its overuse.

In this case, however, I was surprised. There’s actually not much done with regular expressions in Starbucks’ code, just a little whitespace removal. Here it is, in all its glory:

var b = $("#input_code").val().length - a.currentTarget.selectionStart,
    c = $("#input_code").val();
if (!(a.keyCode < 48 || a.keyCode >= 91 && a.keyCode < = 93 || a.keyCode >= 112 && a.keyCode < = 123)) {
  $("#code").removeClass("error");
  var d, e = $("#input_code").val().replace(/\s/g, "");
  e.length > 0 && (d = e.substring(0, 4) + " " + e.substring(4, 8) + " " + e.substring(8, 12) + " " + e.substring(12, 16) + " " + e.substring(16, 20) + " " + e.substring(20), d = d.replace(/\s*$/, "")), c != d && ($("#input_code").val(d), a.currentTarget.selectionStart = a.currentTarget.selectionEnd = d.length - b, console.log(a.currentTarget.selectionStart))
}

The one variable you don’t see here is a, but that’s a simple event object. The first few lines are simple enough. Let’s go through it.

var b = $("#input_code").val().length - a.currentTarget.selectionStart,

The second variable b — the first being a — is setting the text cursor insertion point. The length of the field’s value minus wherever the cursor is now. Being a zero-based index, if the value is nothing, it’s 0. If you type “xxx”, then it’s 3.

c = $("#input_code").val()

The third variable is putting the current value of the input field in a placeholder. We’re setting this aside, for validation later.

Now comes the first validation checkpoint:

if (!(a.keyCode < 48 || a.keyCode >= 91 && a.keyCode < = 93 || a.keyCode >= 112 && a.keyCode < = 123)) {

Remember I said a is an event object? Specifically, it’s the event for when a key is pressed in the text field. (It’s actually bound to the keyup event, not keydown, so it is only triggered once no matter how long you hold down a key.) Here, we’re analyzing the character code of the key pressed. The ranges allowed through are 48-90 (0-9 and a-z), 94-111 (keypad numbers and symbols), and above 123 (other symbol and lock keys).

$("#code").removeClass("error");

We can ignore this. It has nothing to do directly with the behavior of the field itself; it’s removing any form styling for a previously attempted entry which resulted in an error. (We can infer by this that form validation, and probably submission too, is done by ajax. But I’m not interested in dissecting everything… at least, not now.)

var d, e = $("#input_code").val().replace(/\s/g, "");

We’re creating two new variables, one of which we’ll be modifying by inserting spaces for display purposes. We’ll use the two to build the new field value. The regular expression here is simple enough — it’s a global removal of whitespace.

e.length > 0 && (d = e.substring(0, 4) + " " + e.substring(4, 8) + " " + e.substring(8, 12) + " " + e.substring(12, 16) + " " + e.substring(16, 20) + " " + e.substring(20), d = d.replace(/\s*$/, "")), c != d && ($("#input_code").val(d), a.currentTarget.selectionStart = a.currentTarget.selectionEnd = d.length - b, console.log(a.currentTarget.selectionStart))

What is that? That’s a block of semicolon-less javascript, executed as a conditional. It’s also doing all the work we’ve set out to inspect, so let’s break it apart into more manageable pieces. (It’s my own opinion that javascript is more readable with semicolons. Some agree with me, while others don’t.)

if (e.length > 0) {
  d = e.substring(0, 4) + " " + e.substring(4, 8) + " " + e.substring(8, 12) + " " + e.substring(12, 16) + " " + e.substring(16, 20) + " " + e.substring(20);
  d = d.replace(/\s*$/, "");
}
if (c != d) {
  $("#input_code").val(d);
  a.currentTarget.selectionStart = a.currentTarget.selectionEnd = d.length - b;
}

Now to break things down a little more.

if (e.length > 0) {

The first if block makes sure the form field’s value, once stripped of all whitespace, is not empty.

d = e.substring(0, 4) + " " + e.substring(4, 8) + " " + e.substring(8, 12) + " " + e.substring(12, 16) + " " + e.substring(16, 20) + " " + e.substring(20);

Now we’re taking the value of the field and inserting spaces every four characters. Starbucks’ input code is 22 characters long, so it will only split the text up to 20 characters.

As an aside, it doesn’t take much to make this dynamic. Setting a constant for the segment length and looping based on increments of that length, I whipped up a quick demo to make it work for any value length.

Anyway, continuing on to the last line in the if block.

d = d.replace(/\s*$/, "");

This is simple: it’s removing all the whitespace at the end of the value. The existing code, remember, is inserting spaces in between the four-character substrings. It will add those spaces even if there is a value less than 20 characters (substring() returns an empty string).

Now let’s look at the last if block.

if (c != d) {

Remember at the top, where we set the value to c for later validation? We’re using it now. We’re comparing the original value with our new, whitespace-injected value, to make sure they don’t match. If they do match — for example, if the user had entered spaces every four characters into their code themselves — then we’ll just ignore this next logic block.

$("#input_code").val(d);

Finally, some payoff. We’re setting that new, whitespace-injected field value into the field itself.

a.currentTarget.selectionStart = a.currentTarget.selectionEnd = d.length - b;

And lastly, we’re setting the selection start and end to the same value, which is the length of our new value minus the previously set cursor insertion point. This makes it so that you can type in the middle of the field and it will still preserve the spaces at the proper breakpoints.

Drum roll please

After all that, here is the form field. Good work, Starbucks front-end devs. It’s a nice piece of code.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related