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.
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. π€
A Collection of Useful React Hooks for Geocoding Purposes and More
Claudio Cortese γ» Dec 31 '19
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:
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.
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)