Let’s make a form that puts current location to use in a map!
I love shopping online. I can find what I need and get most things for a decent price. I am Nigerian currently working and studying in India, and two things I dread when shopping online are:
- Filling out a credit card form
- Filling out shipping and billing address forms
Maybe I’m just lazy, but these things are not without challenges! For the first one, thanks to payment processing services like PayPal and e-wallets, I neither have to type in my 12-digit credit card number for every new e-commerce site I visit, nor have to save my credit card details with them.
For the second, the only time-saving option given by most shopping websites is to save your shipping address, but you still have to fill the form (arrghh!). This is where the challenge is. I’ve had most of my orders returned because my address (which I thought was the right address) could not be located or confirmed by the app for one reason or another.
Address inputs are challenging
Getting a user’s address through an input form is a clean solution but can also be a herculean task to the user. Here’s why:
- If the user is new in a particular city, they might not know their full address
- If the user wants to ship to a new address which isn’t saved (e.g shipping to a workplace or a friend’s address instead of the saved home address)
- If the user resides in a city with very difficult address systems
- If the user is plain lazy like me
A potential solution: get the address automatically
Getting the user’s address by the tap/click of a button. Yup, that’s easy! The UX is great as it saves the user both the time and effort of filling out some form. It will also save the store owner time, effort, and even money in some cases, as there’ll likely be a reduction in the number of incorrectly placed orders or shipments.
Let’s build a mini app that gets a user’s address and shows it on a Google Map interface using vanilla JavaScript. The concept is as follows:
- Get the HTML button and listen for a click event
- Get the user’s location (latitude and longitude) on a button click
- Show the user’s location on a Google map
- Pass the latitude and longitude to the Google Maps Geocode API URL
- Display the returned address (or list of addresses) on the UI for the user to select one
- Listen for map events and repeat steps 4 and 5
- Pre-fill the form with the address data the user selects
Getting started and setting up
To build this app, we’re going to use the Materialize CSS framework to save us some time fussing with styles. Materialize is a modern responsive front-end framework based on Google’s Material Design system. The beta version works with vanilla JavaScript.
A basic setup using Materialize’s CSS and JavaScript files in a document is like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Address Locator</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
<script src="js/main.js"></script>
</body>
</html>
We’re also going to use the Google Maps API for displaying the map and getting the user’s human-readable address. We’ll need an API key to do this. Here’s how to get one:
- Sign in to your Google Developer’s Console Account
- Create a new project or select an existing one
- Click on “Enable APIs and Services”
- Select the “Maps Javascript API” option
- Click “Enable” on the new page that comes up. Go back to the previous page and do a search for “Geocoding API,” click on it and enable it as well
- Then, on the right nav of the page, click on Credentials, copy the API key on the page and save it to a file
Now, let’s update our document to show the map and let the user know it can be used to get their current location as the address. Also, we will add a form that’s pre-filled with the address the user selects.
...
<body>
<div class="container">
<h3>Shipping Address</h3>
<p>You can click the button below to use your current location as your shipping address</p>
<div id="map">
</div>
<button id="showMe" class="btn">Use My Location</button>
<form id="shippingAddress">
<div id="locationList"></div>
<br>
<div class="input-field">
<textarea class="input_fields materialize-textarea" id="address" type="text"></textarea>
<label class="active" for="address">Address (Area and Street)</label>
</div>
<div class="input-field">
<input class="input_fields" id="locality" type="text">
<label class="active" for="locality">Locality</label>
</div>
<div class="input-field">
<input class="input_fields" id="city" type="text">
<label class="active" for="city">City/District/Town</label>
</div>
<div class="input-field">
<input class="input_fields" id="postal_code" type="text">
<label class="active" for="pin_code">Pin Code</label>
</div>
<div class="input-field">
<input class="input_fields" id="landmark" type="text">
<label class="active" for="landmark">Landmark</label>
</div>
<div class="input-field">
<input class="input_fields" id="state" type="text">
<label class="active" for="State">State</label>
</div>
</form>
<!-- You could add a fallback address gathering form here -->
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"></script>
<script src="js/main.js"></script>
</body>
</html>
While we are here, let’s style things a bit to make this look a little better:
.container {
width: 50%;
max-width: 800px;
}
#map {
height: 50vh;
margin-bottom: 10px;
display: none;
}
#locationList .card {
padding: 10px;
}
#toast-container {
top: 50%;
bottom: unset;
}
.toast {
background-color: rgba(0, 0, 0, 0.8);
}
@media only screen and (max-width: 768px) {
.container {
width: 80%;
}
}
This CSS hides the map until we are ready to view it. Our app should look like this:
Let’s plan this out
Our app will be making use of the HTML5 Geolocation API to determine our user’s current location as well as Google’s Geocode API with a technique called Reverse Geocoding. The Geocode API takes a human-readable address and changes it into geographical (latitudinal and longitudinal) coordinates and marks the spot in the map.
Reverse Geocoding does the reverse. It takes the latitude and longitude and converts them to human-readable addresses. Geocoding and Reverse Geocoding are well documented.
Here’s how our app will work:
- The user clicks on the “Use My Location” button
- The user is located with the HTML5 Geolocation API (
navigator.geolocation
) - We get the user’s geographic coordinates
- We pass the coordinates to the geocode request API
- We display the resulting addresses to the user
Most times, the geocode returns more than one address, so we would have to show the user all the returned addresses and let them choose the most accurate one.
Phew! Finally, we can get to the fun part of actually writing the JavaScript. Let’s go through each of the steps we outlined.
Step 1: Clicking the button
In our main.js
file, let’s get a reference to the HTML button. While we are at it, we’ll set up some other variables we’ll need, like our API key.
//This div will display Google map
const mapArea = document.getElementById('map');
//This button will set everything into motion when clicked
const actionBtn = document.getElementById('showMe');
//This will display all the available addresses returned by Google's Geocode Api
const locationsAvailable = document.getElementById('locationList');
//Let's bring in our API_KEY
const __KEY = 'YOUR_API_KEY';
//Let's declare our Gmap and Gmarker variables that will hold the Map and Marker Objects later on
let Gmap;
let Gmarker;
//Now we listen for a click event on our button
actionBtn.addEventListener('click', e => {
// hide the button
actionBtn.style.display = "none";
// call Materialize toast to update user
M.toast({ html: 'fetching your current location', classes: 'rounded' });
// get the user's position
getLocation();
});
When the click handler for our button runs, it:
- Hides the button
- Alerts the user that we are getting their current location (a “toast” in Materialize is like a popup notification)
- Calls the
getLocation
function
Step 2: Get the user’s location (latitude and longitude)
When our getLocation
function is invoked, we need to do some more work. First, let’s check if we can even use the Geolocation API:
getLocation = () => {
// check if user's browser supports Navigator.geolocation
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(displayLocation, showError, options);
} else {
M.toast({ html: "Sorry, your browser does not support this feature... Please Update your Browser to enjoy it", classes: "rounded" });
}
}
When we have support, it calls geolocation’s getCurrentPosition
method. If it doesn’t have support, then the user is alerted that there’s no browser support.
If there is support, then the getCurrentLocation
method is used to get the current location of the device. The syntax is like this:
navigator.geolocation.getCurrentPosition(*success, error, [options]*)
success
: This is a callback function that takes a position as its only parameter. For us, our success callback function is thedisplayLocation
function.error
: [optional] This is a callback function that takes aPositionError
as its sole input parameter. You can read more about this here. Our error callback function is theshowError
function.options
: [optional] This is an object which describes theoptions
property to be passed to thegetCurrentPosition
method. You can read more about this here. Our options parameter is theoptions
object.
Before writing our displayLocation
function, let’s handle the showError
function and options
object:
// Displays the different error messages
showError = (error) => {
mapArea.style.display = "block"
switch (error.code) {
case error.PERMISSION_DENIED:
mapArea.innerHTML = "You denied the request for your location."
break;
case error.POSITION_UNAVAILABLE:
mapArea.innerHTML = "Your Location information is unavailable."
break;
case error.TIMEOUT:
mapArea.innerHTML = "Your request timed out. Please try again"
break;
case error.UNKNOWN_ERROR:
mapArea.innerHTML = "An unknown error occurred please try again after some time."
break;
}
}
//Makes sure location accuracy is high
const options = {
enableHighAccuracy: true
}
Now, let’s write the code for our displayLocation
function inside our main.js
file:
displayLocation = (position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
}
We now have our user’s latitude and longitude and we can view them in the console by writing the code below inside displayLocation
:
console.log( `Current Latitude is ${lat} and your longitude is ${lng}` );
Step 3: Show the user’s current location on a Google Map
To do this, we will be adding these lines of code to our displayLocation
function.
const latlng = {lat, lng}
showMap(latlng);
createMarker(latlng);
mapArea.style.display = "block";
The first line takes our lat
and lng
values and encapsulates it in the latlng
object literal. This makes it easy for us to use in our app.
The second line of code calls a showMap
function which accepts a latlng
argument. In here, we get to instantiate our Google map and render it in our UI.
The third line invokes a createMarker
function which also accepts our object literal (latlng
) as its argument and uses it to create a Google Maps Marker for us.
The fourth line makes the mapArea
visible so that our user can now see the location.
displayLocation = (position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
const latlng = { lat, lng }
showMap(latlng);
createMarker(latlng);
mapArea.style.display = "block";
}
Now, let’s get to creating our functions. We will start with the showMap
function.
showMap = (latlng) => {
let mapOptions = {
center: latlng,
zoom: 17
};
Gmap = new google.maps.Map(mapArea, mapOptions);
}
The showMap
function creates a mapOptions
objects that contain the map center (which is the lat
and lng
coordinates we got from displayLocation
) and the zoom level of the map. Finally, we create an instance of the Google Maps class and pass it on to our map. In fewer words, we instantiate the Google Maps class.
To create a map instance, we specify two parameters in the constructor: the div
the map will be displayed and the mapOptions
. In our case, our div is called mapArea
and our mapOptions
is called mapOptions
. After this, our created map will show up, but without a marker. We need a marker so the user can identify their current position on the map.
Let’s create our marker using the createMarker
function:
createMarker = (latlng) => {
let markerOptions = {
position: latlng,
map: Gmap,
animation: google.maps.Animation.BOUNCE,
clickable: true
};
Gmarker = new google.maps.Marker(markerOptions);
}
A few things to note in this code:
- The
position
property positions the marker at the specifiedlatlng
- The
map
property specifies the map instance where the marker should be rendered (in our case, it’sGmap
) - The
animation
property adds a littleBOUNCE
to our marker - The
clickable
property set totrue
means our marker can be clicked - Finally, we instantiate the Marker class in our
Gmarker
instance variable
So far, our user’s location has been fetched, the map has rendered and the user can see their current location on the map. Things are looking good! ?
Step 4: Pass the latitude and longitude to the Geocode API
Google’s Geocoding API will be used to convert our user’s numeric geographical coordinates to a formatted, human-readable address using the reverse geocoding process we covered earlier.
The URL takes this form:
https://maps.googleapis.com/maps/api/geocode/outputFormat?parameters
…where the outputFormat
may either be a json
or xml
which determines the the format used to deliver the data. The parameters
part is a list of parameters needed for the request.
Our request URL will look like this:
https://maps.googleapis.com/maps/api/geocode/json?latlng=${latlng}&key=${__KEY}
Let’s go ahead and connect to the API. We would do this in a function called getGeolocation
.
getGeolocation = (lat, lng) => {
const latlng = lat + "," + lng;
fetch( `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latlng}&key=${__KEY}` )
.then(res => res.json())
.then(data => console.log(data.results));
}
The getGeolocation
function takes two arguments ( lat and lng
} concatenates them to form a new latlng
variable that is passed to the URL.
Using the Fetch API (more on this here), we add the new latlng
and __KEY
into the Geocode request URL. Then, on the response object we get back, we pass the .json
method to resolve the promise with JSON. Finally, we log the response in our console.
To make use of our newly created function, we have to call it in the displayLocation
function. So let’s update our displayLocation
function to contain the getGeolocation
function call:
displayLocation = (position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
const latlng = { lat, lng }
showMap(latlng);
createMarker(latlng);
mapArea.style.display = "block";
getGeolocation(lat, lng)// our new function call
}
The returned data should look something like this:
{
"results" :
{
"address_components":
{
"long_name": "1600",
"short_name": "1600",
"types": ["street_number"]
},
{
"long_name": "Amphitheatre Pkwy",
"short_name": "Amphitheatre Pkwy",
"types": ["route"]
},
{
"long_name": "Mountain View",
"short_name": "Mountain View",
"types": ["locality", "political"]
},
{
"long_name": "Santa Clara County",
"short_name": "Santa Clara County",
"types": ["administrative_area_level_2", "political"]
},
{
"long_name": "California",
"short_name": "CA",
"types": ["administrative_area_level_1", "political"]
},
{
"long_name": "United States",
"short_name": "US",
"types": ["country", "political"]
},
{
"long_name": "94043",
"short_name": "94043",
"types": ["postal_code"]
}
],
"formatted_address": "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
"geometry": {
"location": {
"lat": 37.4224764,
"lng": -122.0842499
},
"location_type": "ROOFTOP",
"viewport": {
"northeast": {
"lat": 37.4238253802915,
"lng": -122.0829009197085
},
"southwest": {
"lat": 37.4211274197085,
"lng": -122.0855988802915
}
}
},
"place_id": "ChIJ2eUgeAK6j4ARbn5u_wAGqWA",
"types": ["street_address"]
}
],
"status" : "OK"
}
Step 5: Display the returned address(es) for the user to select
At this stage, we have made a request to Google’s Geocoding API and have gotten our result logged in the console. Now, we have to display the results in a UI for our user. This requires two things:
- Create a new function that handles creating HTML elements
- Update our
getGeolocation
function to make the function call
Let’s create the function that would take care of creating the HTML elements and updating the DOM.
populateCard = (geoResults) => {
geoResults.map(geoResult => {
// first create the input div container
const addressCard = document.createElement('div');
// then create the input and label elements
const input = document.createElement('input');
const label = document.createElement('label');
// then add materialize classes to the div and input
addressCard.classList.add("card");
input.classList.add("with-gap");
// add attributes to them
label.setAttribute("for", geoResult.place_id);
label.innerHTML = geoResult.formatted_address;
input.setAttribute("name", "address");
input.setAttribute("type", "radio");
input.setAttribute("value", geoResult.formatted_address);
input.setAttribute("id", geoResult.place_id);
addressCard.appendChild(input);
addressCard.appendChild(label)
return (
// append the created div to the locationsAvailable div
locationsAvailable.appendChild(addressCard)
);
})
}
In this function, we iterate through our results and create some HTML elements (div
, input
and a label
), append the input
and the label
to the div
and finally append the new div
to a parent div
(which is locationsAvailable
). Once we get the result from our API call, our DOM will be created and displayed to the user.
Next, we update our getGeolocation
function to call our populateCard
function by replacing the last line of getGeolocation
with this:
.then(data => populateCard(data.results));
…which means our updated function should look this:
getGeolocation = (lat, lng) => {
const latlng = lat + "," + lng;
fetch( `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latlng}&key=${__KEY}` )
.then(res => res.json())
.then(data => populateCard(data.results));
}
At this point, everything should be working fine. Our user clicks a button, gets a location displayed on the map along with a list of addresses that match the current location.
Step 6: Listen for map events and repeat steps 4 and 5
If our user decides to move the map or marker, nothing happens to the UI — new addresses aren’t not displayed and everything remains static. We’ve got to fix this, so let’s make our app dynamic by listening for map events. You can read all about Google Map Events here.
There are three events we want to listen for:
drag
: This is fired once the user starts dragging and continues to drag the mapdragend
: This is fired once the user stops dragging the mapidle
: This is fired once every event has been fired and the map is idle
Quick Question: Why are these events best suited for our app?
Quick Answer: The first two events will make sure that our map marker stays in the center of the map during the drag event while the idle event will make a geocoding request with the new coordinates.
To listen for these events we have to update the showMap
function with the following:
Gmap.addListener('drag', function () {
Gmarker.setPosition(this.getCenter()); // set marker position to map center
});
Gmap.addListener('dragend', function () {
Gmarker.setPosition(this.getCenter()); // set marker position to map center
});
Gmap.addListener('idle', function () {
Gmarker.setPosition(this.getCenter()); // set marker position to map center
if (Gmarker.getPosition().lat() !== lat || Gmarker.getPosition().lng() !== lng) {
setTimeout(() => {
updatePosition(this.getCenter().lat(), this.getCenter().lng()); // update position display
}, 2000);
}
});
As explained above, the first two event listeners simply ensure that the marker remains in the center of our map. Pay closer attention to the idle
event listener because that is where the action is.
Once the idle event is fired, the marker goes to the center, then a check is done to find out if the current position of the marker is the same with the lat
or lng
values received by the displayLocation
function. If it is not the same, then we call the updatePosition
function after two seconds of idleness.
Having said that, we have to make a few updates to the showMap
function. First, on the function header, we have to include more parameters and on the showMap
function call. We need to add the new arguments there, too. Our showMap
function should look like this:
showMap = (latlng, lat, lng) => {
let mapOptions = {
center: latlng,
zoom: 17
};
Gmap = new google.maps.Map(mapArea, mapOptions);
Gmap.addListener('drag', function () {
Gmarker.setPosition(this.getCenter()); // set marker position to map center
});
Gmap.addListener('dragend', function () {
Gmarker.setPosition(this.getCenter()); // set marker position to map center
});
Gmap.addListener('idle', function () {
Gmarker.setPosition(this.getCenter()); // set marker position to map center
if (Gmarker.getPosition().lat() !== lat || Gmarker.getPosition().lng() !== lng) {
setTimeout(() => {
updatePosition(this.getCenter().lat(), this.getCenter().lng()); // update position display
}, 2000);
}
});
}
And our displayLocation
function should look like this:
displayLocation = (position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
const latlng = { lat, lng }
showMap(latlng, lat, lng); //passed lat and lng as the new arguments to the function
createMarker(latlng);
mapArea.style.display = "block";
getGeolocation(lat, lng);
}
Having listened for the map events, let’s repeat Step 4 and Step 5.
We start by writing our updatePosition
function. This function will perform only one action for now, which is to pass the new lat
and lng
values to the getGeolocation
function:
updatePosition = (lat, lng) => {
getGeolocation(lat, lng);
}
After getting the new position and fetching addresses, our DOM should re-render for the user, right? Well, it doesn’t. And to fix that, we create a function that will force the DOM to re-render:
// check if the container has a child node to force re-render of dom
function removeAddressCards(){
if (locationsAvailable.hasChildNodes()) {
while (locationsAvailable.firstChild) {
locationsAvailable.removeChild(locationsAvailable.firstChild);
}
}
}
It checks if the locationsAvailable
div
has any childNode
s, and if it does, it deletes them before creating new address cards. The populateCard
function is now updated to this:
populateCard = (geoResults) => {
// check if a the container has a child node to force re-render of dom
removeAddressCards();
geoResults.map(geoResult => {
// first create the input div container
const addressCard = document.createElement('div');
// then create the input and label elements
const input = document.createElement('input');
const label = document.createElement('label');
// then add materialize classes to the div and input
addressCard.classList.add("card");
input.classList.add("with-gap");
// add attributes to them
label.setAttribute("for", geoResult.place_id);
label.innerHTML = geoResult.formatted_address;
input.setAttribute("name", "address");
input.setAttribute("type", "radio");
input.setAttribute("value", geoResult.formatted_address);
input.setAttribute("id", geoResult.place_id);
addressCard.appendChild(input);
addressCard.appendChild(label);
return (
locationsAvailable.appendChild(addressCard);
);
})
}
We are done and are now able to fully get and display the user’s address!
Step 7: Pre-fill the form with the address data the user selects
The final step is to fill the form with the address the user selects. We need to add a click event listener to the address card and pass the address as argument to the callback function.
Here’s how we add the event listener in the populateCard
function:
input.addEventListener('click', () => inputClicked(geoResult));
You should note that the geoResult
argument in the above callback is the selected address object from the results
array. That said, update the populateCard
function to accommodate our new line of code.
The inputClicked
function uses a series of if
statements to assign values to our form elements. so before working on it, let’s bring our form elements into the equation:
const inputAddress = document.getElementById('address'),
inputLocality = document.getElementById('locality'),
inputPostalCode = document.getElementById('postal_code'),
inputLandmark = document.getElementById('landmark'),
inputCity = document.getElementById('city'),
inputState = document.getElementById('state');
Having done this, let us now work on pre-filling the form with the address_components
in the inputClicked
function.
inputClicked = result => {
result.address_components.map(component => {
const types = component.types
if (types.includes('postal_code')) {
inputPostalCode.value = component.long_name
}
if (types.includes('locality')) {
inputLocality.value = component.long_name
}
if (types.includes('administrative_area_level_2')) {
inputCity.value = component.long_name
}
if (types.includes('administrative_area_level_1')) {
inputState.value = component.long_name
}
if (types.includes('point_of_interest')) {
inputLandmark.value = component.long_name
}
});
inputAddress.value = result.formatted_address;
// to avoid labels overlapping pre-filled input contents
M.updateTextFields();
// removes the address cards from the UI
removeAddressCards();
}
The above block of code iterates over the clicked (or selected) address component, checks the types of components and finally assigns them to the input fields if they match.
M.updateTextFields()
function is from Materialize and it ensures that the label does not overlap with the input fields values and the removeAddressCards()
function removes the address cards from the UI.
With this, we are done with our app and have saved our users lots of typing and headaches! Surely they will thank us for implementing such a hassle free solution.
This whole UX experiment can be seen as a shortcut that will help the user pre-fill the shipping address form. We should clearly state here that the returned addresses isn’t always 100% accurate. But that’s why we allow the address to be manually edited in the UI.
Demo!
See the Pen Prefill Address Form with Geolocation and Google Maps by CSS-Tricks (@css-tricks) on CodePen.
The post Let’s make a form that puts current location to use in a map! appeared first on CSS-Tricks.