Avatar of deleyd
deleyd
Flag for United States of America asked on

Asp.Net razor <autocomplete> tag

I have a Asp.Net razor website project.


In the project is a file "autocomplete.razor" under the "Shared" folder, which has a <input ...> tag in it.


Then another page has an <autocomplete ...> tag it uses in place of an <input ...> tag.


My question is, is this <autocomplete> tag DEFINED by the autocomplete.razor file? I haven't found documentation on defining tags this way.

ASP.NET* Razor

Avatar of undefined
Last Comment
deleyd

8/22/2022 - Mon
Dorababu M

Unless it is an custom/user control there will be no such per defined pages in razor view. In general mark up pages will end with .cshtml. What do you see inside that file?
deleyd

ASKER
Autocomplete.razor:
@using Newtonsoft.Json.Linq;
@inject IJSRuntime JSRuntime
@implements IDisposable

<input autocomplete="off" id="@this.UniqueId" @bind="this.CurrentText" @bind:event="oninput" @onkeyup="@(e => HandleKeyUpAutoCompleteSearchSearch(e))" type="text" />

@code {
    private string currentText = string.Empty;

    [Parameter]
    public string UniqueId { get; set; }

    [Parameter]
    public string CurrentText
    {
        get => this.currentText;
        set
        {
            if (value != this.currentText)
            {
                this.currentText = value;
                this.StateHasChanged();
            }
        }
    }

    [Parameter]
    public int Index { get; set; }

    [Parameter]
    public string DisplayOptionOne { get; set; }

    [Parameter]
    public string DisplayOptionTwo { get; set; }

    [Parameter]
    public Func<string, JArray> OnSearchFinish { get; set; }

    [Parameter]
    public Func<int, JObject, bool> OnUserClick { get; set; }

    [Parameter]
    public int FlexMarginShift { get; set; }

    private System.Timers.Timer debounceTimer;

    protected override void OnInitialized()
    {
        this.debounceTimer = new System.Timers.Timer(200);
        this.debounceTimer.Elapsed += this.OnUserFinish;
        this.debounceTimer.AutoReset = false;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            if (string.IsNullOrEmpty(this.UniqueId))
            {
                throw new ArgumentNullException(nameof(this.UniqueId), "You must specify a unique ID for this to work");
            }

            var dotNetReference = DotNetObjectReference.Create(this);
            await JSRuntime.InvokeVoidAsync("autocompleteBuilder", this.UniqueId, dotNetReference, this.DisplayOptionOne, this.DisplayOptionTwo, this.FlexMarginShift);
        }
    }

    [JSInvokable]
    public void OnComplete(string uniqueId, string jsonData)
    {
        JObject clickedItem = JObject.Parse(jsonData);
        this.OnUserClick(this.Index, clickedItem);
    }

    private void HandleKeyUpAutoCompleteSearchSearch(KeyboardEventArgs e)
    {
        this.debounceTimer.Stop();
        this.debounceTimer.Start();
    }

    private async void OnUserFinish(Object source, System.Timers.ElapsedEventArgs e)
    {
        JArray result = this.OnSearchFinish(this.CurrentText);
        if (result != null)
        {
            await JSRuntime.InvokeVoidAsync("updateFound", this.UniqueId, result.ToString());
        }
    }


    public void Dispose()
    {
        if (this.debounceTimer != null)
        {
            this.debounceTimer.Elapsed -= this.OnUserFinish;
            this.debounceTimer.Dispose();
        }
    }
}


Open in new window

I'm not sure where this generic file originated form.

It also looks like the debounceTimer doesn't really do anything as far as I can tell.

I also do not understand how the @bind:event="oninput" works, in the <input ...> tag near the top. What is it binding the OnInput event to? I do note that autocomplete does work. When a letter is entered into the input field, a list of options does appear.

Here's a sample page which uses the <autocomplete ...> tag:

File SchedulerCalendarPage.razor:
@using BlazorDateRangePicker
@using VibrationServer.Hubs.Services
@using VibrationServer.Models
@using Microsoft.AspNetCore.Http
@using System.IO;
@using Newtonsoft.Json.Linq;
@using Itenso.TimePeriod;

@inject IJSRuntime JSRuntime
@inject AccessService accessService
@inject IHttpContextAccessor _httpContextAccessor
@inject Blazored.LocalStorage.ILocalStorageService localStorage
@inject CalendarService calendarService
@implements IDisposable

...

    <Autocomplete UniqueId="program1" CurrentText="@this.CurrentCalendarEvent.AssociatedProgram.Name" DisplayOptionOne="name" DisplayOptionTwo="count" OnSearchFinish="this.ProgramSearchFinished" OnUserClick="this.ProgramUserClick" FlexMarginShift="30" />



Open in new window

I haven't found any documentation indicating that <autocomplete> is a tag defined by razor or html.

There's also a autocomplete.jsfile:
var usersFound = [];
function updateFound(id, newData) {
    newData = JSON.parse(newData);
    usersFound.length = 0;

    for (var i = 0; i < newData.length; i++) {
        usersFound.push(newData[i]);
    }

    var inputBox = $("#" + id); 

    processInput(inputBox);
    return true;
}

function autocompleteBuilder(id, dotNetRef, displayOption1, displayOption2, flexMarginShift) {
    $("#" + id).awesomecomplete({
        noResultsMessage: '<p>Not Found</p>',
        staticData: usersFound,
        flexMarginShift: flexMarginShift,
        autoCompleteId: id,
        onComplete: function (autoCompleteId, dataItem) {
            dotNetRef.invokeMethodAsync("OnComplete", autoCompleteId, JSON.stringify(dataItem));
        },
        valueFunction: function (dataItem) {
            var result = dataItem[displayOption1];
            if (dataItem[displayOption2]) {
                result += dataItem[displayOption2];
            }

            return result;
        }
    });
}

Open in new window


And there's an awesomecomplete.js file, which appears to come from https://github.com/issa-tseng/awesomecomplete, except it's not. Part of it matches.

// Data callback.  If you're using callbacks to a server,
// call this on the autocompleted text field to complete the
// callback process after you have your matching items.
var onDataProxy = function ($this, term) {
    return function (data) {
        processData($this, data, term);
    };
}

// private helpers
var processInput = function ($this) {
    var term = $this.val();
    if (typeof $this.data('awesomecomplete-config').dataMethod === 'function')
        $this.data('awesomecomplete-config').dataMethod(term, $this, onDataProxy($this, term));
    else
        processData($this, $this.data('awesomecomplete-config').staticData, term);
};

var processData = function ($this, data, term) {
    var $list = $this.data('awesomecomplete-list');
    $list.empty().hide();
    if (term === '')
        return;

    var config = $this.data('awesomecomplete-config');

    var results = [];
    for (var item = 0; item < data.length; item++) {
        var dataItem = jQuery.extend({}, data[item]);
        var matchCount = 0;

        var maxFieldMatches = 0;
        var topMatch = null;
        var matchedTerms = [];

        for (var field in dataItem) {
            if ((typeof dataItem[field] === 'function') || (typeof dataItem[field] === 'object'))
                continue;

            var skippedField = false;
            for (var j = 0; j < config.dontMatch.length; j++)
                if (field == config.dontMatch[j])
                    skippedField = true;
            if (skippedField)
                continue;

            var dataString = dataItem[field].toString();
            var terms = [term];
            if (config.splitTerm)
                terms = term.split(config.wordDelimiter);

            for (var j = 0; j < terms.length; j++) {
                if (terms[j] === '')
                    continue;

                terms[j] = terms[j].replace(/([\\*+?|{}()^$.#])/g, '\\$1');
                var regex = new RegExp('(' + terms[j] + ')', (config.ignoreCase ? 'ig' : 'g'));

                var matches = [];
                if (matches = dataString.match(regex)) {
                    matchCount += matches.length;

                    if ((field != config.nameField) && (matches.length > maxFieldMatches)) {
                        maxFieldMatches = matches.length;
                        topMatch = field;
                        matchedTerms[j] = true;
                    }
                }
            }

            if (config.highlightMatches) {
                var regex = new RegExp('(' + terms.join('|') + ')', (config.ignoreCase ? 'ig' : 'g'));
                dataItem[field] = dataString.replace(regex, '<span class="' + config.highlightClass + '">$1</span>');
            }
        }

        var matchedTermCount = 0;
        for (var j = 0; j < matchedTerms.length; j++)
            if (matchedTerms[j] === true)
                matchedTermCount++;

        if (matchCount > 0)
            results.push({
                dataItem: dataItem,
                originalDataItem: data[item],
                matchCount: matchCount,
                topMatch: topMatch,
                matchedTermCount: matchedTermCount
            });
    }

    results.sort(function (a, b) {
        return config.sortFunction(a, b, term);
    });

    results = results.slice(0, config.resultLimit);

    for (var i in results) {
        $('<li>' + config.renderFunction(results[i].dataItem, results[i].topMatch, results[i].originalDataItem, config) + '</li>')
            .data('awesomecomplete-dataItem', results[i].originalDataItem)
            .data('awesomecomplete-value', config.valueFunction(results[i].originalDataItem, config))
            .appendTo($list)
            .click(function () {
                var $listItem = $(this);
                //$this.val($listItem.data('awesomecomplete-value'));
                config.onComplete.call($this, config.autoCompleteId, $listItem.data('awesomecomplete-dataItem'));
            })
            .mouseover(function () {
                $(this).addClass(config.activeItemClass)
                    .siblings().removeClass(config.activeItemClass);
            });
    }

    if ((config.noResultsMessage !== undefined) && (results.length == 0))
        $list.append($('<li class="' + config.noResultsClass + '">' + config.noResultsMessage + '</li>'));

    if ((results.length > 0) || (config.noResultsMessage !== undefined))
        $list.show();
};

// default functions
var defaultRenderFunction = function (dataItem, topMatch, originalDataItem, config) {
    if ((topMatch === config.nameField) || (topMatch === null))
        return '<p class="title">' + dataItem[config.nameField] + '</p>';
    else
        return '<p class="title">' + dataItem[config.nameField] + '</p>' +
            '<p class="matchRow"><span class="matchedField">' + topMatch + '</span>: ' +
            dataItem[topMatch] + '</p>';
};

var defaultValueFunction = function (dataItem, config) {
    return dataItem[config.nameField];
};

var defaultSortFunction = function (a, b, term) {
    return (a.matchedTermCount == b.matchedTermCount) ?
        (b.matchCount - a.matchCount) :
        (b.matchedTermCount - a.matchedTermCount);
};

(function ($) {
    var ident = 0,
        scrollIntoView = ('scrollIntoView' in document.createElement('li'));

    // Initializer. Call on a text field to make things go.
    $.fn.awesomecomplete = function (options) {
        var options = $.extend({}, $.fn.awesomecomplete.defaults, options);

        return this.each(function () {
            var $this = $(this);
            var config = $.meta ? $.extend({}, options, $this.data()) : options;
            $this.data('awesomecomplete-config', config);

            var $attachTo = $(config.attachTo || $this);
            var marginShift = $this.data('awesomecomplete-config').flexMarginShift;

            var newUl = '<ul style="z-index: 100;';

            if (marginShift > 0) {
                newUl += "margin-top: " + marginShift + "px;";
            }

            newUl += '"/>';

            var $list = $(newUl);
            if (!config.wrapSuggestions) {
                $list.insertAfter($attachTo);
            } else {
                $list.appendTo($attachTo);
            }
            $list.hide()
                .addClass(config.suggestionListClass)
                .css('width', $attachTo.innerWidth());
            $this.data('awesomecomplete-list', $list);

            var typingDelayPointer;
            var suppressKey = false;
            $this.keyup(function (event) {
                if (suppressKey) {
                    suppressKey = false;
                    return;
                }

                // ignore arrow keys, shift
                if (((event.which > 36) && (event.which < 41)) ||
                    (event.which == 16))
                    return;

                if (config.typingDelay > 0) {
                    clearTimeout(typingDelayPointer);
                    typingDelayPointer = setTimeout(function () { processInput($this); }, config.typingDelay);
                }
                else {
                    processInput($this);
                }
            });

            $this.keydown(function (event) {
                // enter = 13; up = 38; down = 40; esc = 27
                var $active = $list.children('li.' + config.activeItemClass);
                if (typeof config.beforeKeyAction === 'function')
                    config.beforeKeyAction($active.get(), event);

                switch (event.which) {
                    case 13:
                        if (($active.length !== 0) && ($list.is(':visible'))) {
                            event.preventDefault();
                            $this.val($active.data('awesomecomplete-value'));
                            config.onComplete.call($this, $active.data('awesomecomplete-dataItem'));
                            $list.hide();
                        }
                        $list.hide();
                        suppressKey = true;
                        break;
                    case 38:
                        event.preventDefault();
                        if ($active.length === 0) {
                            $list.children('li:last-child').addClass(config.activeItemClass);
                        }
                        else {
                            $active.prev().addClass(config.activeItemClass);
                            $active.removeClass(config.activeItemClass);
                        }
                        break;
                    case 40:
                        event.preventDefault();
                        if ($active.length === 0) {
                            $list.children('li:first-child').addClass(config.activeItemClass);
                        }
                        else if ($active.is(':not(:last-child)')) {
                            $active.next().addClass(config.activeItemClass);
                            $active.removeClass(config.activeItemClass);
                        }
                        break;
                    case 27:
                        $list.hide();
                        suppressKey = true;
                        break;
                }
                if (scrollIntoView && $list.is(':visible')) {
                    var $active = $list.children('li.' + config.activeItemClass);
                    if ($active.length > 0) {
                        $active.get(0).scrollIntoView(false);
                    }
                }2

                if (typeof config.afterKeyAction === 'function') {
                    config.afterKeyAction($active.get(), $this.attr('id'), $this.val(), event["key"]);
                    $list.show();
                }
            });

            // opera wants keypress rather than keydown to prevent the form submit
            $this.keypress(function (event) {
                var $active = $list.children('li.' + config.activeItemClass);

                if ((event.which == 13) && ($list.children('li.' + config.activeItemClass).length > 0)) {
                    event.preventDefault();
                }
            });

            // stupid hack to get around loss of focus on mousedown
            var mouseDown = false;
            var blurWait = false;
            $(document).bind('mousedown.awesomecomplete' + ++ident, function () {
                mouseDown = true;
            });
            $(document).bind('mouseup.awesomecomplete' + ident, function () {
                mouseDown = false;
                if (blurWait) {
                    blurWait = false;
                    $list.hide();
                }
            });
            $this.blur(function () {
                if (mouseDown) {
                    blurWait = true;
                }
                else {
                    var $active = $list.children('li.' + config.activeItemClass);
                    if ($list.is(':visible') && ($active.length !== 0))
                        $this.val($active.data('awesomecomplete-value'));
                    $list.hide();
                }
            });
            $this.focus(function () {
                if ($list.children(':not(.' + config.noResultsClass + ')').length > 0)
                    $list.show();
            });
        });
    };

    $.fn.awesomecomplete.defaults = {
        activeItemClass: 'active',
        attachTo: undefined,
        wrapSuggestions: false,
        dataMethod: undefined,
        dontMatch: [],
        highlightMatches: true,
        highlightClass: 'match',
        ignoreCase: true,
        nameField: 'name',
        noResultsClass: 'noResults',
        noResultsMessage: undefined,
        autoCompleteId: 0,
        onComplete: function (id, dataItem) { },
        sortFunction: defaultSortFunction,
        splitTerm: true,
        staticData: [],
        flexMarginShift: 0,
        suggestionListClass: "autocomplete",
        renderFunction: defaultRenderFunction,
        resultLimit: 10,
        typingDelay: 0,
        valueFunction: defaultValueFunction,
        wordDelimiter: /[^\da-z]+/ig
    };
})(jQuery);


Open in new window

Dorababu M

So for auto complete they have designed a re-usable control if I am not wrong.  OnInput will fire when ever you enter a character inside the textbox. You can check the example here

https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_oninput_html
This is the best money I have ever spent. I cannot not tell you how many times these folks have saved my bacon. I learn so much from the contributors.
rwheeler23
Dorababu M

Where this file resides in your folder structure? Autocomplete.razor is it in any solution or with in the solution. Can you show the solution explorer of your project and let me know where exactly that file is placed. If it is placed as part of any other project then it might be referred in one of the paths here

@using BlazorDateRangePicker
@using VibrationServer.Hubs.Services
@using VibrationServer.Models
@using Microsoft.AspNetCore.Http
@using System.IO;
@using Newtonsoft.Json.Linq;
@using Itenso.TimePeriod;

@inject IJSRuntime JSRuntime
@inject AccessService accessService
@inject IHttpContextAccessor _httpContextAccessor
@inject Blazored.LocalStorage.ILocalStorageService localStorage
@inject CalendarService calendarService
@implements IDisposable

Open in new window


ASKER CERTIFIED SOLUTION
Chinmay Patel

THIS SOLUTION ONLY AVAILABLE TO MEMBERS.
View this solution by signing up for a free trial.
Members can start a 7-Day free trial and enjoy unlimited access to the platform.
See Pricing Options
Start Free Trial
GET A PERSONALIZED SOLUTION
Ask your own question & get feedback from real experts
Find out why thousands trust the EE community with their toughest problems.
deleyd

ASKER
I learned this is called a "Blazor Component" (or "Razor Component").

(If I knew Blazor I'd have known that, as "Blazor Components" are the fundamental building blocks of Blazor.)