Addresses are some of the most common types of HTML form that you are likely to create, and most have terrible UX design. We are going to progressively enhance our form to fix one of the most common mistakes, and maybe learn some fetch and async/await along the way. This is admittedly quite a lot for three lines of code, so scroll to the bottom if you just want the tl;dr code.
Unless you are serving just one country, then you are going to need a country select element in your form. Anyone who has created a country select will probably have asked themself the question of which option should be pre-selected, and in which order the options should be.
The obvious answer, and the one chosen in most cases, is this one:
Alphabetical, with a default options at the top. This has the benefit of being logical. It's relatively easy to find the right country, if a little slow. You probably know that you can find an option in a select element by typing the first few letters, but most of your users don't. Even if they do, it's still annoying (no, I don't want United Arab Emirates).
The other option you might see is this one:
Replace the choice at the top with your country of choice. Perhaps add a few more of your most common choices there too. This has the benefit of saving some scrolling, but can still be annoying, and you still need to choose which options to put at the top. If your user isn't one of the top choices it is if anything even more annoying than alphabetical. Other options include keeping it alphabetical, but pre-selecting your most common country. Poor Canadians.
Some people are now probably shouting "GeoIP!" and that's great. However as anyone who has actually implemented it can tell, while it's theoretically simple it's actually a right PITA to install the right modules and keep the MaxMind databases up to date. The tiny number of pre-selected forms that you see in the wild is testament to this.
There is a better way! We can use the principles of progressive enhancement to select the right option for most users, without spoiling the experience for others, and without delaying loading. Best of all we can do it in three lines of JavaScript, and nothing to install on the server. This is thanks to the generosity of freegeoip.app, which provided a free geolocation API. This uses the GeoLite database from MaxMind, which doesn't have the accuracy of the full, paid database, but is plenty good enough for our purposes, as we only need country-level data. We are going to fetch the JSON file, extract the country_code
, and then select the correct element.
Before you reach for jQuery, let's look at another way, as you probably don't need it. Using standard XmlHttpRequest is annoying, but they made fetch happen. Don't be afraid of promises either, because async/await makes asynchronous programming as simple as writing synchronous code. I'm assuming you have a select with the id "countries", and with the values as the ISO country code. e.g.
<select id="countries">
<option>Choose your country</option>
<option value="AF">Afghanistan</option>
<option value="AX">Åland Islands</option>
<option value="AL">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
<option value="AD">Andorra</option>
<option value="AO">Angola</option>
<!-- ... -->
</select>
Progressive enhancement
The principle of progressive enhancement states that we shouldn't break the experience for less capable browsers, but should instead add functionality to those browsers that support it. We'll do this by loading the GeoIP API after the page has loaded, and if successful we will then select the correct option.
(async () => {
const result = await fetch("//freegeoip.app/json/");
const json = await result.json();
document.getElementById("countries").value = json.country_code;
})();
This is an immediately invoked async function expression. It will return immediately so won't block page rendering.
If you've not used async/await this may look unfamiliar at first. To break it down, we're defining an asynchronous arrow function, which we are immediately invoking. This is because we can't use top level await yet: it has to be inside a function marked as async. The syntax to make a function async is simple: just place the async
keyword either before the word function
or, in an arrow function, before the arguments. You might recognise the syntax as an IIFE: we wrap the function in braces, then immediately invoke it.
The fetch
method returns a Promise, but by using await
, we can ignore this and write the code as if it returns the result once the promise resolves. It doesn't actually block at that point, but in terms of our control flow we can treat it as if it does.
const result = await fetch("//freegeoip.app/json/");
The result
doesn't hold the data itself: we need to call one of the data methods on it to retrieve that. We'll use json()
, which parses the content as JSON data and returns an object. This also returns a Promise, but once again we can ignore this by await
ing it.
const json = await result.json();
One of the really nice things about await
is that it's fine if it receives something that's not a Promise. In that case it just continues as normal. This is great for memoization. You could think of a situation where on first load some data is fetched from a remote API, but is cached locally and returned immediately on future calls. With await you don't need to worry about wrapping it in Promise.resolve()
, because you can return the raw object and it will handle it fine. Similarly, you can easily mock your API calls when doing unit tests.
At this point we have the object holding the location data. We just need the country code, which helpfully is stored as country_code
, so we can then just set the <select>
value.
document.getElementById("countries").value = json.country_code;
You should now hopefully have your country selected.
The neat thing about progressive enhancement is that it's not a big deal if it doesn't work in older browsers. However you can avoid errors by using <script type="module">
, which is ignored by old browsers. All browsers that support modules also support arrow functions, fetch and async/await.
<script type="module">
(async () => {
const result = await fetch("//freegeoip.app/json/");
const json = await result.json();
document.getElementById("countries").value = json.country_code;
})();
</script>
Have a play in this CodePen. Maybe try with a VPN.
Top comments (1)
Very cool, I'll have to remember this next time I need to pull a location!