ASP.NET Core 2.2 - Autocomplete Search


Ken Haggerty
Created 08/22/2019 - Updated 08/25/2019 16:04

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 research project I used for the articles about table functions is published to tables.kenhaggerty.com. I created a topic, ASP.NET Core 2.2 - Table Functions Project for discussions. Access to the source code may be purchased 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 08/25/2019

Updated articles, asset and topic links.

Article references:


Comment Count = 2

gchris
gchris
Posted 05/28/2020 11:10
Member Since 05/28/2020

Nice work but I'm having trouble understanding how this line: "return fetch('/index?handler=NameSearch&searchString=' + search" in the getfoodsname function calls the OnGetNameSearchAsync task.

Ken Haggerty
Ken Haggerty *
Posted 05/28/2020 12:31
Member Since 01/04/2019

I am not sure if you are having an issue with fetch or handlers. For the former, see ASP.NET Core 2.2 - Fetch And Promise. For the latter, see Handler Methods in Razor Pages. As a registered user you can download the free ASP.NET Core 2.2 - Table Functions Project from Manage > Assets.

Please log in to comment or follow.

Login Register
Follow to get web notifications when new comments are posted to this article.
Logged in users receive web notifications for new articles, topics and assets.
Web Notifications