ASP.NET Core 2.2 - Autocomplete Search

Ken Haggerty
Created 08/22/2019 - Updated 12/30/2021 17:00 GMT

This article will demonstrate an autocomplete list for a search of the name property from a list of entities. When the name is selected the entity details are displayed on a Bootstrap modal. I will assume you have created a new ASP.NET Core 2.2 Razor Pages project. I won't use Identity or Individual User Accounts.

The FREE ASP.NET Core 6.0 - Demos And Utilities Project includes a Table Functions Demo. I created a topic, ASP.NET Core 6.0 - Demos And Utilities Project for discussions. Access to the source code is free for registered users at Manage > Assets.

I used instructions from How TO - Autocomplete with modifications to simplify the functions and accommodate the data from this series.

Edit Index.cshtml.cs, add the page handlers:
public async Task<JsonResult> OnGetNameSearchAsync(string searchString)
{
    var foodNames = await _context.Foods
        .Where(f => f.Name.ToLower().Contains(searchString.ToLower()))
        .OrderBy(f => f.Name)
        .Select(f => f.Name).Take(10)
        .ToListAsync();

    return new JsonResult(foodNames);
}

public async Task<JsonResult> OnGetFoodByNameAsync(string name)
{
    if (name == null)
    {
        throw new InvalidOperationException($"OnGetFoodByNameAsync - name = null.");
    }

    var foodDetails = await _context.Foods
            .Where(f => f.Name == name)
            .Select(f => new FoodJsonModel
            {
                Status = "Success",
                Id = f.Id,
                Name = f.Name,
                FoodType = f.FoodType.ToString(),
                ColdStore = f.ColdStore,
                Date = string.Format("{0:MM/dd/yyyy hh:mm tt}", f.Date)
            })
            .FirstOrDefaultAsync();

    if (foodDetails == null)
    {
        return new JsonResult(new { Status = "Failed", Errors = "Details Not Found" });
    }

    return new JsonResult(foodDetails);
}
Edit Index.cshtml, add the input and a button:
<div class="row mb-2">
    <div class="col-8 col-md-6 col-lg-4">
        <input id="GetNameInputId" type="search" class="form-control" placeholder="Name Search" />
    </div>
    <div class="col-4 col-md-3 col-lg-2">
        <button id="GetFoodByNameButtonId" type="button" class="btn btn-primary" data-toggle="modal" data-target=".modal">
            Get Food
        </button>
    </div>
</div>
Edit site.css, add the autocomplete classes:
.autocomplete-items {
    position: absolute;
    border: 1px solid #d4d4d4;
    border-bottom: none;
    border-top: none;
    z-index: 99;
    /*position the autocomplete items to be the same width as the container:*/
    top: 100%;
    left: 0;
    right: 0;
}

    .autocomplete-items div {
        padding: 10px;
        cursor: pointer;
        background-color: #fff;
        border-bottom: 1px solid #d4d4d4;
    }

        .autocomplete-items div:hover {
            /*when hovering an item:*/
            background-color: #e9e9e9;
        }
Edit Index.cshtml, add the JavaScript:
function closeAllAutocompleteLists() {
    var x = document.getElementsByClassName('autocomplete-items');
    for (var i = 0; i < x.length; i++) {
        x[i].parentNode.removeChild(x[i]);
    }
}

//Finds y value of given object
function findPos(obj) {
    var curtop = 0;
    if (obj.offsetParent) {
        do {
            curtop += obj.offsetTop;
        } while (obj = obj.offsetParent);
        return [curtop];
    }
}

function setAutocomplete(inp, arr) {
    window.scroll(0, findPos(inp));
    var a, b, i;
    closeAllAutocompleteLists();
    a = document.createElement('div');
    a.setAttribute('id', this.id + 'autocomplete-list');
    a.setAttribute('class', 'autocomplete-items');
    inp.parentNode.appendChild(a);
    for (i = 0; i < arr.length; i++) {
        b = document.createElement('div');
        b.innerHTML = arr[i];
        b.addEventListener('click', function (e) {
            inp.value = e.target.innerText;
            closeAllAutocompleteLists();
            $('.modal').modal('show');
            modalBody.innerHTML = '';
            var spinnerDiv = document.createElement('div');
            spinnerDiv.classList.add('spinner-border');
            spinnerDiv.classList.add('text-alert');
            spinnerDiv.classList.add('d-flex');
            spinnerDiv.classList.add('mt-2');
            spinnerDiv.classList.add('ml-auto');
            spinnerDiv.classList.add('mr-auto');
            spinnerDiv.setAttribute('role', 'status');
            var span = document.createElement('span');
            span.classList.add('sr-only');
            span.innerText = 'Loading...';
            spinnerDiv.appendChild(span);
            modalBody.appendChild(spinnerDiv);
            getFoodByName(e.target.innerText);
        });
        a.appendChild(b);
    }
}

function getFoodNames(search) {

    return fetch('/index?handler=NameSearch&searchString=' + search,
        {
            method: 'get',
            headers: {
                'Content-Type': 'application/json;charset=UTF-8'
            }
        })
        .then(function (response) {
            if (response.ok) {
                return response.text();
            } else {
                throw Error('FoodNameSearch Response Not OK');
            }
        })
        .then(function (text) {
            try {
                return JSON.parse(text);
            } catch (err) {
                throw Error('FoodNameSearch Method Not Found');
            }
        })
        .then(function (responseJSON) {
            setAutocomplete(document.querySelector('#GetNameInputId'), responseJSON);
        })
        .catch(function (error) {
            modalBody.innerHTML = '';
            var alertDiv = document.createElement('div');
            alertDiv.classList.add('alert', 'alert-danger');
            alertDiv.textContent = 'Error = ' + error;
            modalBody.appendChild(alertDiv);
        });
}

function getFoodByName(name) {

    return fetch('/index?handler=FoodByName&name=' + name,
        {
            method: 'get',
            headers: {
                'Content-Type': 'application/json;charset=UTF-8'
            }
        })
        .then(function (response) {
            if (response.ok) {
                return response.text();
            } else {
                throw Error('FoodByName Response Not OK');
            }
        })
        .then(function (text) {
            try {
                return JSON.parse(text);
            } catch (err) {
                throw Error('FoodByName Method Not Found');
            }
        })
        .then(function (responseJSON) {
            var dl, dt, dd;
            modalBody.innerHTML = '';
            if (responseJSON.Status === 'Success') {
                dl = document.createElement('dl');
                for (prop in responseJSON) {
                    dt = document.createElement('dt');
                    dt.textContent = prop;
                    dl.appendChild(dt);
                    dd = document.createElement('dd');
                    dd.textContent = responseJSON[prop].length === 0 ? 'empty' : responseJSON[prop];
                    dl.appendChild(dd);
                }
                var successDiv = document.createElement('div');
                successDiv.classList.add('alert', 'alert-success');
                successDiv.appendChild(dl);
                modalBody.appendChild(successDiv);
            }
            else {
                dl = document.createElement('dl');
                for (prop in responseJSON) {
                    dt = document.createElement('dt');
                    dt.textContent = prop;
                    dl.appendChild(dt);
                    dd = document.createElement('dd');
                    dd.textContent = responseJSON[prop];
                    dl.appendChild(dd);
                }
                var alertDiv = document.createElement('div');
                alertDiv.classList.add('alert', 'alert-danger');
                alertDiv.appendChild(dl);
                modalBody.appendChild(alertDiv);
            }
        })
        .catch(function (error) {
            modalBody.innerHTML = '';
            var alertDiv = document.createElement('div');
            alertDiv.classList.add('alert', 'alert-danger');
            alertDiv.textContent = 'Error ' + error;
            modalBody.appendChild(alertDiv);
        });
}


// Wait for the page to load first
document.addEventListener('DOMContentLoaded', function () {

    /* close autocompletes when someone clicks in the document:*/
    document.addEventListener('click', function (e) {
        closeAllAutocompleteLists();
    });

    if (document.querySelector('#GetNameInputId')) {
        document.querySelector('#GetNameInputId').addEventListener('keyup', function (e) {
            if (e.target.value.length === 0) {
                closeAllAutocompleteLists();
            } else {
                getFoodNames(e.target.value);
            }
        });
    }

    if (document.querySelector('#GetFoodByNameButtonId')) {
        document.querySelector('#GetFoodByNameButtonId').addEventListener('click', function () {
            var nameInput = document.querySelector('#GetNameInputId');
            if (nameInput.value.length === 0) {
                modalBody.innerHTML = '';
                var alertDiv = document.createElement('div');
                alertDiv.classList.add('alert', 'alert-danger');
                alertDiv.textContent = 'Error - Name is required.';
                modalBody.appendChild(alertDiv);
                return;
            }
            modalBody.innerHTML = '';
            var spinnerDiv = document.createElement('div');
            spinnerDiv.classList.add('spinner-border');
            spinnerDiv.classList.add('text-alert');
            spinnerDiv.classList.add('d-flex');
            spinnerDiv.classList.add('mt-2');
            spinnerDiv.classList.add('ml-auto');
            spinnerDiv.classList.add('mr-auto');
            spinnerDiv.setAttribute('role', 'status');
            var span = document.createElement('span');
            span.classList.add('sr-only');
            span.innerText = 'Loading...';
            spinnerDiv.appendChild(span);
            modalBody.appendChild(spinnerDiv);
            getFoodByName(nameInput.value);
        });
    }

});

Notice the window.scroll(0, findPos(inp)); in the setAutocomplete function. This moves the input to the top of window which allows more space on mobile screens with the keyboard displayed. The findPos(obj) function's while statement uses assignment rather than equality. See Using the assignment operator instead of the equality operator for more.

Autocomplete Search
Update 12/30/2021

Announced the ASP.NET Core 6.0 - Demos And Utilities Project. Updated articles, asset and topic links.

Article references:
Successfully completed. Thank you for contributing.
Contribute to enjoy content without advertisments.
Something went wrong. Please try again.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image
DisplayedName - Member Since ?