DEV Community

Cover image for Let's Build an Address Autocomplete in ReactJS
Claudio Cortese
Claudio Cortese

Posted on

Let's Build an Address Autocomplete in ReactJS

Times to times we need to ask the user to provide us with an address.
It might be because we are gathering shipping information, for example. πŸ“¦

Anyway, we are in need of accurate results but, guess what?

We do not trust our users, of course. πŸ˜‚

So, how do you obtain an accurate address out of a distracted user which doesn't really pay attention? πŸ€”

One solution is to use an address autocomplete so that a partial address like via cervantes 55 would be expanded to its correct form Italia, Napoli, Via Miguel Cervantes De Saavedra, 55 without the user writing it all down by himself. 😎

A good example of an address autocomplete is Algolia Places.

Algolia Places

Actually, we will come up with something that resembles it!

As a matter of fact, I'm going to show you how to build such a component with ReactJS and the help of use-here-api, a library that exposes a collection of convenient hooks that let you easily integrate HERE RESTful API services in your projects.

If you want to learn more about use-here-api, please, take a look at this post. πŸ€“

Without further due, let's get our hands dirty!

Let's set up the project πŸ‘¨β€πŸ’»

First of all, we should create a basic React project.

To accomplish this task, simply open up a terminal and type in the following:

yarn create react-app address-autocomplete --template typescript

Note that it may take a couple of minutes for this command to complete.

We will be using Sass, so we need to install node-sass as well:

yarn add node-sass

We are now going to delete a couple of unneeded files, namely:

  • logo.svg
  • App.css
  • App.test.tsx

Now, before actually writing some real code, we are going to create a new folder components inside src and, in that folder, create another one called AddressAutocomplete.

Inside that folder we are going to create a couple of more files:

  • package.json
{
  "main": "AddressAutocomplete.tsx"
}
  • AddressAutocomplete.tsx
import React, { FC } from 'react'
import Styles from './AddressAutocomplete.module.scss';

const AddressAutocomplete: FC = () => <input placeholder="Enter an address" />

export default AddressAutocomplete
  • AddressAutocomplete.module.scss

Finally, let's open up App.tsx and modify it like so:

import React, { FC } from 'react';
import AddressAutocomplete from './components/AddressAutocomplete';

const App: FC = () => (
  <AddressAutocomplete
    onAutocomplete={(address: any) => {
      console.log(address);
    }}
  />
);

export default App;

You should come up with a folder structure just like this image below:

Correct folder structure for this project

Ready to play with use-here-api πŸ—ΊοΈ

It's now time to

yarn add @cloudpower97/use-here-api

Create a .env file at the root of your project and type in the following:

REACT_APP_HERE_APP_CODE="..."
REACT_APP_HERE_APP_ID="..."

Obviously, make sure to actually replace "..." with the correct pieces of information. πŸ‘€

We are now going to authenticate our requests towards HERE API with the help of configureAutentication.

import {
  configureAuthentication,
  useAutocomplete
} from '@cloudpower97/use-here-api';

const {
  REACT_APP_HERE_APP_ID: app_id,
  REACT_APP_HERE_APP_CODE: app_code
} = process.env;

if (app_code && app_id) {
  configureAuthentication({
    app_code,
    app_id
  });
}

From now on, every request we make with any of the hooks exposed by use-here-api is going to use the provided credentials. πŸ”

To learn more about credentials and authentication methods, you can read this.

Creating the interface 🧱

We should now create an interface to define AddressAutocomplete props

interface AddressAutocompleteProps extends HTMLAttributes<HTMLInputElement> {
  value?: string;
  onAutocomplete?: Function;
}

onAutocomplete is an optional callback that is going to be invoked, if provided, once the user selects one of the results.

As we are going to spread every other prop in an input element, we extends HTMLAttributes<HTMLInputElement>.

Implementing the component πŸ› οΈ

Now, as use-here-api is a typesafe wrapper around HERE API, we can also have a look at the official documentation, that reads:

The HERE Geocoder Autocomplete API is a REST API that you can integrate into web applications to help users obtain better results for address searches with fewer keystrokes. Spatial and region filters can be used to return suggestions with greater relevance to end-users, such as results that are within a specified country or in the proximity of the current location.

The Geocoder Autocomplete API retrieves a complete address and an ID. You can subsequently use the Geocoder API to geocode the address based on the ID and thus obtain the geographic coordinates of the address.

-- Geocoder Autocomplete API

And, if we head over to API Reference we will see that the only required parameter is query, the actual search text.

For this component, we are going to use beginHighlight and endHighlight as well, in order to highlight the matched characters.

The wrapper around this API is exposed through useAutocomplete hook. 🎣

At this point, we just need to make sure that we make use of this hook with the query params fed with user input.

const AddressAutocomplete: FC<AddressAutocompleteProps> = ({
  value = '',
  placeholder = 'Enter an address',
  onAutocomplete,
  ...props
}) => {
  const [{ data }, fetchSuggestions] = useAutocomplete();
  const [location, setLocation] = useState<string>(value);
  const [isMenuOpen, setMenuOpen] = useState<boolean>(false);

  useEffect(() => {
    if (data) {
      setMenuOpen(true);
    }
  }, [data]);

  return (
    <>
      <div className={Styles.SuggestionWrapper}>
        <input
          placeholder={placeholder}
          onChange={({ currentTarget: { value } }) => {
            setLocation(value);

            if (value.length >= 3) {
              fetchSuggestions({
                query: value,
                beginHighlight: '<b>',
                endHighlight: '</b>'
              });
            }
          }}
          className={Styles.SuggestionInput}
          value={location}
          tabIndex={0}
          {...props}
        />
        {data&& isMenuOpen && (
          <ul className={Styles.SuggestionsList}>
            {data?.suggestions?.length === 0 && (
              <li className={Styles.SuggestionItem}>No results found...</li>
            )}
            {data?.suggestions?.map(suggestion => (
              <li
                key={suggestion.locationId}
                className={Styles.SuggestionItem}
                onClick={() => {
                  setLocation(suggestion.label.replace(/<[^>]+>/g, ''));
                }}
                tabIndex={1}
              >
                <span
                  className={Styles.SuggestionLabel}
                  dangerouslySetInnerHTML={{ __html: suggestion.label }}
                />{' '}
                <span
                  className={Styles.AdditionalSuggestion}
                  dangerouslySetInnerHTML={{
                    __html: `${suggestion.address.county}, ${suggestion.address.state}, ${suggestion.address.country}`
                  }}
                />
              </li>
            ))}
          </ul>
        )}
      </div>
    </>
  );
};

Retrieve Geographic Coordinates πŸ“

The component is getting shape!

The user can now type in a partial address and then select the correct one in the dropdown filled with results retrieved from the HERE API!

However, we should now retrieve geographic coordinates for the selected address as well. 🌍

As a matter of fact, as also noted in the guide section,

after the user has selected a suggestion, you can retrieve the location details such as the geographic coordinates via the HERE Geocoder API, using look-up by locationId.

In order to accomplish this task, we are going to use another hook exposed by use-here-api, namely useForwardGeocoding.

const [{ data: geocodeData }, fetchLocation] = useForwardGeocoding();


useEffect(() => {
    if (geocodeData) {
        onAutocomplete && 
   onAutocomplete(geocodeData.response.view[0].result[0]);
    }
}, [geocodeData, onAutocomplete]);

We just need to call fetchLocation when clicking on a result, providing the locationid of the latter.

<li
  key={suggestion.locationId}
  className={Styles.SuggestionItem}
  onClick={() => {
    setLocation(suggestion.label.replace(/<[^>]+>/g, ''));
    setMenuOpen(false);
    fetchLocation({
      locationid: suggestion.locationId,
      jsonattributes: 1
    });
  }}
  tabIndex={1}
>
  <span
    className={Styles.SuggestionLabel}
    dangerouslySetInnerHTML={{ __html: suggestion.label }}
  />{' '}
  <span
    className={Styles.AdditionalSuggestion}
    dangerouslySetInnerHTML={{
      __html: `${suggestion.address.county}, ${suggestion.address.state}, ${suggestion.address.country}`
    }}
  />
</li>

Add the needed styles πŸ’…

We are now going to add some styles to our component.

Let's get straight to AddressAutocomplete.module.scss:

.SuggestionWrapper {
  position: relative;
  width: 100%;
}

.SuggestionInput {
  line-height: 1.15;
  overflow: visible;
  background-color: transparent;
  border: none;
  border-bottom: 1px solid #9e9e9e;
  border-radius: 0;
  outline: none;
  height: 3rem;
  width: 100%;
  font-size: 16px;
  margin: 0 0 8px 0;
  padding: 0;
  box-shadow: none;
  box-sizing: content-box;
  transition: box-shadow .3s, border .3s;
}

.SuggestionsList {
  background-color: white;
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
    0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
  left: 0;
  list-style-type: none;
  margin: 0px;
  max-height: 225px;
  overflow: auto;
  padding: 0;
  position: absolute;
  top: 49px;
  user-select: none;
  width: 100%;
  z-index: 2;
}

.SuggestionItem {
  cursor: pointer;
  padding: 14px 16px;

  &:hover,
  &:focus {
    background-color: #eee;
  }
}

.SuggestionLabel {
  font-size: medium;
  margin-left: 20px;
}

.AdditionalSuggestion {
  color: grey;
  font-size: small;
  margin-left: 10px;
}

.SuggestionIcon {
  color: grey;
  font-size: 25px !important;
  vertical-align: bottom;
}

.ToggleSuggestionListButton {
  color: gray;
  cursor: pointer;
  position: absolute;
  right: 15px;
  top: 15%;
  transition: transform ease-in-out 0.25s;

  &:global(.open) {
    transform: rotate(180deg);
  }

  &:hover,
  &:focus {
    color: var(--dark-green);
  }
}

Final touches ✌️

We now have a fully styled and functional component! πŸ₯³

Just let's add some icons as well.

I'm going to create another component inside, namely SuggestionIcon, that is going to receive matchLevel as the only prop and will return a proper icon based on it.

I'm going to use FontAwesome icons, so let's install it:

yarn add @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome

and then add these lines of code:

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
  faRoad,
  faCity,
  faMapMarkerAlt,
  faAngleDown
} from '@fortawesome/free-solid-svg-icons';

interface SuggestionIconProps {
  matchLevel: string;
}

const SuggestionIcon: FC<SuggestionIconProps> = ({ matchLevel }) => {
  switch (matchLevel) {
    case 'street':
      return (
        <FontAwesomeIcon
          icon={faRoad}
          size="2x"
          className={Styles.SuggestionIcon}
          fixedWidth
        />
      );

    case 'city':
      return (
        <FontAwesomeIcon
          icon={faCity}
          size="2x"
          className={Styles.SuggestionIcon}
          fixedWidth
        />
      );

    default:
      return (
        <FontAwesomeIcon
          icon={faMapMarkerAlt}
          size="2x"
          className={Styles.SuggestionIcon}
          fixedWidth
        />
      );
  }
};

and then, simply add this component as follow:

{data?.suggestions?.map(suggestion => (
              <li
                key={suggestion.locationId}
                className={Styles.SuggestionItem}
                onClick={() => {
                  setLocation(suggestion.label.replace(/<[^>]+>/g, ''));
                  setMenuOpen(false);
                  fetchLocation({
                    locationid: suggestion.locationId,
                    jsonattributes: 1
                  });
                }}
                tabIndex={1}
              >
                <SuggestionIcon matchLevel={suggestion.matchLevel} />
                <span
                  className={Styles.SuggestionLabel}
                  dangerouslySetInnerHTML={{ __html: suggestion.label }}
                />{' '}
                <span
                  className={Styles.AdditionalSuggestion}
                  dangerouslySetInnerHTML={{
                    __html: `${suggestion.address.county}, ${suggestion.address.state}, ${suggestion.address.country}`
                  }}
                />
              </li>
            ))}

Let's now add an icon to open and close the dropdown results menu as well:

{data?.suggestions?.length && (
  <FontAwesomeIcon
    icon={faAngleDown}
    size="2x"
    className={cx(Styles.ToggleSuggestionListButton, {
      open: isMenuOpen
    })}
    onClick={() => {
      setMenuOpen(prevMenuOpen => !prevMenuOpen);
    }}
  />
)}

All set!πŸ‘Œ

Congratulations, you have just created an address autocomplete!

As you can see, with the help of use-here-api we were able to build a complex component with ease 😎

I've created a CodeSandbox where you can play with the code.

Feel free to fork and modify it as you wish! πŸ˜‰

Note: please, do remember to replace REACT_APP_HERE_APP_CODE and REACT_APP_HERE_APP_ID in .env

What's next? πŸ€”

There is still place to further improve this component, like using throttling and/or debouncing techniques to avoid hampering the performance.

We can (actually should πŸ˜…) also enhance accessibility, following ARIA design pattern for a dropdown select.

Let me know in a comment down here if you would like me to expand upon these concepts! πŸ¦„


Before leaving, I wanted to thank you for making it all the way down here! πŸ€—

Don't forget to {...❀️} if you enjoyed this post! 🀭

That's all for this post, see you next one and ... Happy hacking until then! πŸ‘¨β€πŸ’»

Top comments (0)