Friday 20th September, 2024
By Allan Jardine

Address auto-complete with Editor

When building data input forms with Editor, there are various ways in which a developer can make the end user's life a lot easier - clean, concise, and clear forms are one of Editor's strengths, but you can also make data entry easier for the end user using lookups and auto-completion. There is a good chance that you'll have come across an address auto-complete when signing up for services or shipping items on the internet - this article will do a deep dive on how we can do that in Editor.

Example

To start, let's skip straight to a working example so you can see what we are going to build - below is a table with three made up addresses. It is fully editable per a regular DataTable with Editor (local editing in this case - no backend database for this example). The part that is of interest in this blog post is if you create a new record or edit an existing one - the second field isn't a data field, rather it is an input that lets the user start to type an address, which will then be looked up using the excellent Geoapify API. A list of options are presented to the end user (you in this case!), refined as they continue to type, and lets them click on one to auto-complete the fields in the form.

Name House Street City County State Postcode Country
Airi Satou The Ivies Glebe Close London London NW11 9TU United Kingdom
Hope Fuentes 392 Maple Street Los Angeles California 90017 United States
Serge Baldwin 28 Chemin Du Lavarin Sud Caen Basse-Normandie 14000 France

How it works

The editable DataTable is a simple table with seven columns, and Editor matches that with a single extra field for the address lookup. You'll find plenty of examples and documentation on how to set up DataTables and Editor on this site - for brevity, I won't be recapping the configuration used here, however, one point of interest is that the lookup field has the fields.submit option set to false since the data from that field is not relevant in submission to the server - it is a client-side only field.

The initialisation code for the DataTables and Editor is:

const editor = new DataTable.Editor({
    fields: [
        {
            label: 'Name:',
            name: 'name'
        },
        {
            label: 'Address lookup:',
            labelInfo: 'Start typing your address to automatically look it up.',
            name: 'lookup',
            submit: false
        },
        {
            label: 'House name / number:',
            name: 'house'
        },
        {
            label: 'Street:',
            name: 'street'
        },
        {
            label: 'City:',
            name: 'city'
        },
        {
            label: 'County:',
            name: 'county'
        },
        {
            label: 'State:',
            name: 'state'
        },
        {
            label: 'Postcode / ZIP:',
            name: 'postcode'
        },
        {
            label: 'Country:',
            name: 'country'
        }
    ],
    table: '#addresses'
});

new DataTable('#addresses', {
    columns: [
        {
            className: 'dtr-control',
            orderable: false,
            targets: 0,
            defaultContent: ''
        },
        { data: 'name' },
        { data: 'house' },
        { data: 'street' },
        { data: 'city' },
        { data: 'county' },
        { data: 'state' },
        { data: 'postcode' },
        { data: 'country' }
    ],
    layout: {
        topStart: {
            buttons: [
                { extend: 'create', editor: editor },
                { extend: 'edit', editor: editor },
                { extend: 'remove', editor: editor }
            ]
        }
    },
    order: [1, 'asc'],
    responsive: {
        details: {
            type: 'column'
        }
    },
    select: true
});

Address lookup

In Editor when you want something to happen based on the input value in a field, we use the dependent() method. In this case, we want to listen for changes on the lookup field, and then to trigger a call to the Geoapify auto-complete API (their API is well documented).

There are two important points about our use of dependent() here:

  1. We want to act while the user in typing, not just waiting for the user to finish typing and move focus away from the field (e.g. the change event, which is the default). For this, we use the third parameter of dependent() to set the listening event to be input.
  2. At the same time, we don't want to fire an Ajax request on every keystroke - we want to group them so as not to waste API resources. For this, we can use the DataTable.util.debounce() utility method.

The structure of our dependent() setup call looks like this:

editor.dependent(
    'lookup',
    DataTable.util.debounce(function (val, data, cb) {
        // Ajax lookup code based on `val`
    }),
    {event: 'input'}
);

Now that we have a value from the lookup field, we can go to the Geoapify API and query it to see what options it thinks are viable candidates. For that, we can use the fetch() API:

if (val) {
    fetch(
        'https://api.geoapify.com/v1/geocode/autocomplete?text=' +
            encodeURIComponent(val) +
            '&apiKey=' +
            encodeURIComponent(apiKey)
    )
        .then((response) => response.json())
        .then((json) => {
            // Got our response
            displayResults(editor, json);
            cb({});
        });
}

Note that the apiKey variable would be set to whatever your API key from your Geoapify project (sign up on their website). Additionally, you will see that cb() is called with an empty object - this callback function is part of the dependent() function and tells Editor that your dependent handler has finished processing. Editor will show the field as "processing" until the callback is called, letting the end user know that something is happening.

Options display

From the auto-complete API we now have options that we should show the end user. To do that, we create a displayResults() function that takes the array returned by the API, makes a div for each option and then inserts that into the field "information" area. The field information can be written by using the field().fieldInfo() method.

This block of code might look a little intimidating at first, but really all it does is create a container div, and then a div per option with a bunch of span elements for each of the address elements. I've created a createElement() function, also shown below, to make the creation of new elements as simple as possible.

// Display the list of addresses that have been found
function displayResults(editor, json) {
    var options = createElement('div', { className: 'lookup-options' });
    var addresses = json.features.slice(0, 6); // Limit to 6

    addresses.forEach((full) => {
        var address = full.properties;
        var option = createElement('div', { className: 'lookup-option' }, null, options);

        // Store for lookup if selected
        option._address = address;

        createElement('span', { className: 'lookup-house' }, address.housenumber || address.name, option);
        createElement('span', { className: 'lookup-street' }, address.street, option);
        createElement('span', { className: 'lookup-city' }, address.city, option);
        createElement('span', { className: 'lookup-county' }, address.county, option);
        createElement('span', { className: 'lookup-state' }, address.state, option);
        createElement('span', { className: 'lookup-postcode' }, address.postcode, option);
        createElement('span', { className: 'lookup-country' }, address.country, option);
    });

    editor.field('lookup').fieldInfo(options);
}

// Helper for creating new DOM elements
function createElement(name, props, content, appendTo) {
    let el = document.createElement(name);

    if (props) {
        Object.assign(el, props);
    }

    if (content) {
        el.textContent = content;
    }

    if (appendTo) {
        appendTo.appendChild(el);
    }

    return el;
}

Finally, CSS is used to display the address options in a grid. I've used flexbox, but you can use CSS Grid, or whatever other option you want. Indeed, with the function above you have complete control over the DOM structure, so you can modify the layout however you like to suit your application!

.lookup-options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5em;
  justify-content: space-between;
}

.lookup-option {
  width: calc(50% - 0.25em);
  box-sizing: border-box;
  padding: 1em;
  border: 1px solid rgba(122, 122, 122, 0.5);
  border-radius: 4px;
  cursor: pointer;
}

.lookup-option span {
    display: block;
}

Selection of an address

The final item we need to complete to get a working system is to allow the end user to click on an address and for that to insert the values into the Editor form. For that, we use a DOM click event listener on the Editor field (field().node()), and filter it to only act on events on the option elements (.lookup-option). If you are familiar with jQuery terminology, this is called a delegated event handler, and it basically allows the DOM elements of the child to change (in this case our options) while requiring only a single event listener to handle the events through event propagation (the event bubbles up through the DOM):

// Handle a click on an address to select it
editor.field('lookup').node().addEventListener('click', function (e) {
    let option = e.target.closest('div.lookup-option');

    if (option) {
        // Write field values
    }
});

To write the field values, we can simply use the Editor field().val() method for each field to write in the value from the selected entry. Note that above in displayResults() each option div had the address object attached to it as the _address property. This is so that when the option is selected, we can simply retrieve it from the node by reading that property:

let address = option._address;

// Address was selected - fill in the Editor field values
editor.field('house').val(address.housenumber || address.name || '');
editor.field('street').val(address.street || '');
editor.field('city').val(address.city || '');
editor.field('county').val(address.county || '');
editor.field('state').val(address.state || '');
editor.field('postcode').val(address.postcode || '');
editor.field('country').val(address.country || '');

// Clear the lookup
editor.field('lookup').fieldInfo('').val('');

The final action is to clear the lookup field's options and value.

Wiring it all together

I've broken everything down into blocks in this post to be able to explain each one. To see everything tied together, have a look at the following files:

The address auto-complete introduced in this blog post is just one example that can be used for auto-completing data in Editor, and uses a specific API. You might have access to a different address auto-complete system, or have a completely different data type that you can perform auto-complete with.

Hopefully this blog post has given you the tools and confidence you need to be able to add similar actions to your own forms. If you come up with any others, let me know in the forum!