Is there any way to add address autocomplete to django forms?

Viewed 43

I am trying to use address autocomplete api and I want to use django forms to save address on database.

forms:

class AdressInputForm(forms.ModelForm):
class Meta:
    model = AdressInput
    fields = '__all__'

views:

def new(request):
    if request.method == 'POST':
        form = AdressInputForm(request.POST)
        form.save()
    else:
        form = AdressInputForm()

    return render(request, 'example_app/index.html', {'form': form})

html:

<div class="autocomplete-container" id="autocomplete-container"></div>

css:

body {
    font: 16px;
    background: #f3f5f6;
}

.autocomplete-container {
    margin-bottom: 20px;
    margin-right: 50%;
}

.input-container {
    display: flex;
    position: relative;
}

.input-container input {
    flex: 1;
    outline: none;
    
    border: 1px solid rgba(0, 0, 0, 0.2);
    padding: 10px;
    padding-right: 31px;
    font-size: 16px;
}

.autocomplete-items {
    position: absolute;
    border: 1px solid rgba(0, 0, 0, 0.1);
    box-shadow: 0px 2px 10px 2px rgba(0, 0, 0, 0.1);
    border-top: none;
    background-color: #fff;

    z-index: 99;
    top: calc(100% + 2px);
    left: 0;
    right: 0;
}

.autocomplete-items div {
    padding: 10px;
    cursor: pointer;
}

.autocomplete-items div:hover {
    /*when hovering an item:*/
    background-color: rgba(0, 0, 0, 0.1);
}

.autocomplete-items .autocomplete-active {
    /*when navigating through the items using the arrow keys:*/
    background-color: rgba(0, 0, 0, 0.1);
}

.clear-button {
    color: rgba(0, 0, 0, 0.4);
    cursor: pointer;
    
    position: absolute;
    right: 5px;
    top: 0;

    height: 100%;
    display: none;
    align-items: center;
}

.clear-button.visible {
    display: flex;
}

.clear-button:hover {
    color: rgba(0, 0, 0, 0.6);
}

js:

function addressAutocomplete(containerElement, callback, options) {

    const MIN_ADDRESS_LENGTH = 3;
    const DEBOUNCE_DELAY = 300;

    // create container for input element
    const inputContainerElement = document.createElement("div");
    inputContainerElement.setAttribute("class", "input-container");
    containerElement.appendChild(inputContainerElement);

    // create input element
    const inputElement = document.createElement("input");
    inputElement.setAttribute("type", "text");
    inputElement.setAttribute("placeholder", options.placeholder);
    inputContainerElement.appendChild(inputElement);

    // add input field clear button
    const clearButton = document.createElement("div");
    clearButton.classList.add("clear-button");
    addIcon(clearButton);
    clearButton.addEventListener("click", (e) => {
    e.stopPropagation();
    inputElement.value = '';
    callback(null);
    clearButton.classList.remove("visible");
    closeDropDownList();
    });
    inputContainerElement.appendChild(clearButton);

    /* We will call the API with a timeout to prevent unneccessary API activity.*/
    let currentTimeout;

    /* Save the current request promise reject function. To be able to cancel the promise when a new request comes */
    let currentPromiseReject;

    /* Focused item in the autocomplete list. This variable is used to navigate with buttons */
    let focusedItemIndex;

    /* Process a user input: */
    inputElement.addEventListener("input", function(e) {
    const currentValue = this.value;

    /* Close any already open dropdown list */
    closeDropDownList();


    // Cancel previous timeout
    if (currentTimeout) {
        clearTimeout(currentTimeout);
    }

    // Cancel previous request promise
    if (currentPromiseReject) {
        currentPromiseReject({
        canceled: true
        });
    }

    if (!currentValue) {
        clearButton.classList.remove("visible");
    }

    // Show clearButton when there is a text
    clearButton.classList.add("visible");

    // Skip empty or short address strings
    if (!currentValue || currentValue.length < MIN_ADDRESS_LENGTH) {
        return false;
    }

    /* Call the Address Autocomplete API with a delay */
    currentTimeout = setTimeout(() => {
        currentTimeout = null;

        /* Create a new promise and send geocoding request */
        const promise = new Promise((resolve, reject) => {
        currentPromiseReject = reject;

        // The API Key provided is restricted to JSFiddle website
        // Get your own API Key on https://myprojects.geoapify.com
        const apiKey = "6dc7fb95a3b246cfa0f3bcef5ce9ed9a";

        var url = `https://api.geoapify.com/v1/geocode/autocomplete?text=${encodeURIComponent(currentValue)}&format=json&limit=5&apiKey=${apiKey}`;

        fetch(url)
            .then(response => {
            currentPromiseReject = null;

            // check if the call was successful
            if (response.ok) {
                response.json().then(data => resolve(data));
            } else {
                response.json().then(data => reject(data));
            }
            });
        });

        promise.then((data) => {
        // here we get address suggestions
        currentItems = data.results;

        /*create a DIV element that will contain the items (values):*/
        const autocompleteItemsElement = document.createElement("div");
        autocompleteItemsElement.setAttribute("class", "autocomplete-items");
        inputContainerElement.appendChild(autocompleteItemsElement);

        /* For each item in the results */
        data.results.forEach((result, index) => {
            /* Create a DIV element for each element: */
            const itemElement = document.createElement("div");
            /* Set formatted address as item value */
            itemElement.innerHTML = result.formatted;
            autocompleteItemsElement.appendChild(itemElement);

            /* Set the value for the autocomplete text field and notify: */
            itemElement.addEventListener("click", function(e) {
            inputElement.value = currentItems[index].formatted;
            callback(currentItems[index]);
            /* Close the list of autocompleted values: */
            closeDropDownList();
            });
        });

        }, (err) => {
        if (!err.canceled) {
            console.log(err);
        }
        });
    }, DEBOUNCE_DELAY);
    });

    /* Add support for keyboard navigation */
    inputElement.addEventListener("keydown", function(e) {
    var autocompleteItemsElement = containerElement.querySelector(".autocomplete-items");
    if (autocompleteItemsElement) {
        var itemElements = autocompleteItemsElement.getElementsByTagName("div");
        if (e.keyCode == 40) {
        e.preventDefault();
        /*If the arrow DOWN key is pressed, increase the focusedItemIndex variable:*/
        focusedItemIndex = focusedItemIndex !== itemElements.length - 1 ? focusedItemIndex + 1 : 0;
        /*and and make the current item more visible:*/
        setActive(itemElements, focusedItemIndex);
        } else if (e.keyCode == 38) {
        e.preventDefault();

        /*If the arrow UP key is pressed, decrease the focusedItemIndex variable:*/
        focusedItemIndex = focusedItemIndex !== 0 ? focusedItemIndex - 1 : focusedItemIndex = (itemElements.length - 1);
        /*and and make the current item more visible:*/
        setActive(itemElements, focusedItemIndex);
        } else if (e.keyCode == 13) {
        /* If the ENTER key is pressed and value as selected, close the list*/
        e.preventDefault();
        if (focusedItemIndex > -1) {
            closeDropDownList();
        }
        }
    } else {
        if (e.keyCode == 40) {
        /* Open dropdown list again */
        var event = document.createEvent('Event');
        event.initEvent('input', true, true);
        inputElement.dispatchEvent(event);
        }
    }
    });

    function setActive(items, index) {
    if (!items || !items.length) return false;

    for (var i = 0; i < items.length; i++) {
        items[i].classList.remove("autocomplete-active");
    }

    /* Add class "autocomplete-active" to the active element*/
    items[index].classList.add("autocomplete-active");

    // Change input value and notify
    inputElement.value = currentItems[index].formatted;
    callback(currentItems[index]);
    }

    function closeDropDownList() {
    const autocompleteItemsElement = inputContainerElement.querySelector(".autocomplete-items");
    if (autocompleteItemsElement) {
        inputContainerElement.removeChild(autocompleteItemsElement);
    }

    focusedItemIndex = -1;
    }

    function addIcon(buttonElement) {
    const svgElement = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
    svgElement.setAttribute('viewBox', "0 0 24 24");
    svgElement.setAttribute('height', "24");

    const iconElement = document.createElementNS("http://www.w3.org/2000/svg", 'path');
    iconElement.setAttribute("d", "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z");
    iconElement.setAttribute('fill', 'currentColor');
    svgElement.appendChild(iconElement);
    buttonElement.appendChild(svgElement);
    }

    /* Close the autocomplete dropdown when the document is clicked. 
    Skip, when a user clicks on the input field */
    document.addEventListener("click", function(e) {
    if (e.target !== inputElement) {
        closeDropDownList();
    } else if (!containerElement.querySelector(".autocomplete-items")) {
        // open dropdown list again
        var event = document.createEvent('Event');
        event.initEvent('input', true, true);
        inputElement.dispatchEvent(event);
    }
    });
}

addressAutocomplete(document.getElementById("autocomplete-container"), (data) => {
    console.log("Selected option: ");
    console.log(data);
}, {
    placeholder: "Enter an address here"
});

The html, css and js make an input field like this:

Is there any ways to connect the csv and js to django forms and save it to the database? (I've coppied the code from api's tutorial)
Thanks

1 Answers

Sounds like you are on the right path. If you want to use javascript to make the API call instead of the default django templates, you might find it easier building a custom API call with something lie django-rest-framework. That might be overkill depending what you are trying to achieve, but it's an option!

A perhaps easier way would be to make sure you are calling the API endpoint you have built in your views after the user has submitted their address.

Here is a short snipped on how to make an axios request:

    let data = new FormData();
    
    data.append("title", document.getElementById('title').value)  
    data.append("note", document.getElementById('note').value)
    data.append("csrfmiddlewaretoken", '{{csrf_token}}')
    
    axios.post('register_address/', data)
     .then(res => alert("Form Submitted"))
     .catch(errors => console.log(errors))

More details on axios requests here.

Related