Can multiple inputs each using typeahead.js with different sources be consolidated?

2.1k Views Asked by At

I have an form with numerous text inputs for which 10 of them I'd like to use typeahead.js with Bloodhound. I have it working for 2 of them - they both include prefetch and remote data sources. But there's a fair amount of jQuery code for each instance of typeahead/Bloodhound and I'm curious if anyone has tried to "genericize" typeahead/Bloodhound to handle multiple different input elements each with a different data source? It might be more trouble than it's worth, but I'm a bit concerned about the amount of code the page will have to load.

My environment is Spring/MVC, Hibernate (Oracle db), Bootstrap.

Here's a sample of one typeahead/Bloodhound instance. It's in a function because I'm adding row's of inputs dynamically so I have to call this function after a new row is added to enable typeahead on the text input in that row.

function initIngredientsTA() {
    //set the options
    var ingredBH = new Bloodhound({
        limit: 20,
        datumTokenizer: function(datum) {
            return Bloodhound.tokenizers.whitespace(datum.name);
        },
        queryTokenizer: Bloodhound.tokenizers.whitespace,       
        prefetch: {
            url: '/myapp/resources/ingredients.json',
            cache: false,
            filter: function(data) {
                console.log("data: " + data.ingredients);
                return $.map(data.ingredients, function (ingredient) {
                    return {
                        id : ingredient.id,
                        name : ingredient.name
                    };
                });
            } 
        },
        remote: {
            url: '/myapp/recipe/addRecipe/getIngredients?searchStr=%QUERY',
            cache: false,
            wildcard: '%QUERY',
            filter: function(data) {
                console.log("data: " + data);
                return $.map(data, function (data) {
                    return {
                        id : data.id,
                        name : data.name
                    };
                });
            } 
        }
    });

    //initialize the suggestion Engine
    ingredBH.initialize();

    $('.ingredDesc').typeahead(
    {
        hint: true,
        highlight: true,
        minLength: 1
    },
    {
        name: 'ingredients',
        displayKey: 'name',
        limit: 20,
        source: ingredBH.ttAdapter(),
    })
};

EDIT: I think what I'm really asking is if anyone has created a "template" version of typeahead/Bootstrap that can then be instantiated for each individual text input.

2

There are 2 best solutions below

0
On

Sorry if this was a question with an obvious answer, but I'm new to the Java, Spring/Hibernate, jQuery, etc., stack. Using Firebug I was able to figure out what both typeahead and Bloodhound required and came up with the following:

//token and filter functions
function ingredDatumToken(datum) {
    return Bloodhound.tokenizers.whitespace(datum.name);
}

function ingredPrefetchFilter(data) {
    return $.map(data.ingredients, function (ingredient) {
        return {
            id : ingredient.id,
            name : ingredient.name
        };
    });
};

function ingredRemoteFilter(data) {
    return $.map(data, function (data) {
        return {
            id : data.id,
            name : data.name
        };
    });
};

//Bloodhound initialization
function initBloodhound(limit, cache, datumToken, queryToken,prefetchUrl,prefetchFilter, remoteUrl, wildcard, remoteFilter)
{
    var token = Bloodhound.tokenizers.whitespace; 

    var options = {};
    var prefetchOptions = {};
    var remoteOptions = {};

    prefetchOptions['url'] = prefetchUrl;
    prefetchOptions['cache'] = cache;
    prefetchOptions['filter'] = prefetchFilter;

    remoteOptions['url'] = remoteUrl;
    remoteOptions['cache'] = cache;
    remoteOptions['wildcard'] = wildcard; 
    remoteOptions['filter'] = remoteFilter;       

    options['limit'] = limit; 
    options['datumTokenizer'] = datumToken === null ? token : datumToken;
    options['queryTokenizer'] = queryToken === null ? token : queryToken;

    if (prefetchUrl != null)
        options['prefetch'] = prefetchOptions;

    if (remoteUrl != null)
        options['remote'] = remoteOptions;

    return new Bloodhound(options);
};

//create two Bloodhound engines
var ingredBH = initBloodhound(50,false,ingredDatumToken,null,'/myapp/resources/ingredients.json',ingredPrefetchFilter,'/myapp/recipeaddRecipe/getIngredients?searchStr=%QUERY','%QUERY',ingredRemoteFilter);
var measureBH = initBloodhound(20,false,null,null,'/myapp/resources/measures.json',null,null,null,null);
//add more Bloodhound engines here

//typeahead options
function initTypeaheadOptions(hint, highlight, minLength) {

    var options = {};

    options['hint'] = hint;
    options['highlight'] = highlight;
    options['minLength'] = minLength;

    return options;
}

//typeahead dataset
function initTypeaheadDataset(name, displayKey, limit, source) {

    var datasets = {};

    datasets['name'] = name;
    datasets['displayKey'] = displayKey;
    datasets['limit'] = limit;
    datasets['source'] = source;

    return datasets;
}

//initialize a typeahead control    
function initIngredientsTA() {

    var options = initTypeaheadOptions(true,true,1);
    var dataset = initTypeaheadDataset('ingredients', 'name', 20, ingredBH);

    $('.ingredDesc').typeahead(options,dataset);
};

//initialize a typeahead control
function initMeasuresTA() {

    var options = initTypeaheadOptions(true,true,1);
    var dataset = initTypeaheadDataset('measures', null, 20, measureBH);

    $('.ingredQtyType').typeahead(options,datasets);
};

//add more typeahead initialization functions here

//call the initialize functions
initIngredientsTA();
initMeasuresTA();
//call more initialize functions here

I'm still working on making it more generic and I'm not crazy about all the parameters in the call to the Bloodhound initialization function, but since I'll have about 10 or more typeahead-enabled controls on the page it will be fairly easy to add the rest of them with just a few lines of code. Those typeahead controls that are not part of the dynamically created rows on the page won't need separate functions for initialization, but can be initialized with just 3 lines of code. I'm definitely open to any comments or suggestions for improvement, including any thoughts that this is a stupid idea.

0
On

I just noticed there was a bounty on this question so I thought I'd share some optimizations I made to my original answer.

The base functions are located in an include file:

typeahead.js

function setBHPrefetchOpts(cache, prefetchUrl, prefetchFilter) {

    var prefetchOptions = {};

    prefetchOptions['url'] = prefetchUrl;
    prefetchOptions['cache'] = cache;
    prefetchOptions['filter'] = prefetchFilter;

    return prefetchOptions;
}

function setBHRemoteOpts(cache, wildcard, remoteUrl, remoteFilter) {

    var remoteOptions = {};

    remoteOptions['url'] = remoteUrl;
    remoteOptions['cache'] = cache;
    remoteOptions['wildcard'] = wildcard; 
    remoteOptions['filter'] = remoteFilter;

    return remoteOptions;
}

function setBHOptions(sufficient, datumToken, queryToken, prefetchOptions, remoteOptions) {

    var token = Bloodhound.tokenizers.whitespace; 

    var options = {};

    options['sufficient'] = sufficient; 
    options['datumTokenizer'] = datumToken === null ? token : datumToken;
    options['queryTokenizer'] = queryToken === null ? token : queryToken;

    if (prefetchOptions != null)
        options['prefetch'] = prefetchOptions;

    if (remoteOptions != null)
        options['remote'] = remoteOptions;

    return options;
}   

function initTypeaheadOptions(hint, highlight, minLength) {

    var options = {};

    options['hint'] = hint;
    options['highlight'] = highlight;
    options['minLength'] = minLength;

    return options;
};

function initTypeaheadDataset(name, displayKey, limit, source) {

    var dataset = {};

    dataset['name'] = name;
    dataset['displayKey'] = displayKey;
    dataset['limit'] = limit;
    dataset['source'] = source;

    return dataset;
};

To initialize a prefetch (json) typeahead:

var prefetchOpts = setBHPrefetchOpts(false, '/recipe/resources/measures.json', null);
var bhOpts = setBHOptions(50, null, null, prefetchOpts, null);
var measureBH = new Bloodhound(bhOpts);

function initMeasuresTA() {

    var options = initTypeaheadOptions(true,true,1);
    var dataset = initTypeaheadDataset('measures', null, 50, measureBH);
    $('.ingredQtyType').typeahead(options,dataset);
};

To initialize a remote typeahead:

var remoteOpts = setBHRemoteOpts(false, '%QUERY', '/recipe/recipe/getQualifiers?searchStr=%QUERY', null);
var bhOpts = setBHOptions(50, null, null, null, remoteOpts);
var qualifierBH = new Bloodhound(bhOpts);   

function initQualifiersTA() {

    var options = initTypeaheadOptions(true,true,1);
    var dataset = initTypeaheadDataset('qualifiers', null, 50, qualifierBH);
    $('.ingredQual').typeahead(options,dataset);
};

In both of the above the json consists of single items, e.g.,

["Cup","Cups","Ounce","Ounces","Pound","Pounds","Teaspoon","Teaspoons","Tablespoon","Tablespoons"]

To initialize both prefetch and remote with a bit more complex json like this:

{"ingredients":[{"id":"142","name":"Flour"},{"id":"144","name":"Sugar"}]}

function ingredDatumToken(datum) {
    return Bloodhound.tokenizers.whitespace(datum.name);
};

function ingredPrefetchFilter(data) {
    return $.map(data.ingredients, function (ingredient) {
        return {
            id : ingredient.id,
            name : ingredient.name
        };
    });
};

function ingredRemoteFilter(data) {
    return $.map(data, function (data) {
        return {
            id : data.id,
            name : data.name
        };
    });
};

var prefetchOpts = setBHPrefetchOpts(false, '/recipe/resources/ingredients.json', ingredPrefetchFilter);
var remoteOpts = setBHRemoteOpts(false, '%QUERY', '/recipe/recipe/getIngredients?searchStr=%QUERY', ingredRemoteFilter);
var bhOpts = setBHOptions(50, ingredDatumToken, null, prefetchOpts, remoteOpts);
var ingredBH = new Bloodhound(bhOpts);

function initIngredientsTA() {

    var options = initTypeaheadOptions(true,true,1);
    var dataset = initTypeaheadDataset('ingredients', 'name', 50, ingredBH);
    $('.ingredDesc').typeahead(options,dataset);
};

An example of a dynamic query:

function setSourceUrl(url, query) {
    var source = $('#inputSource').val();
    var newurl = url + '?searchStr=' + query + '&type=' + source;
    return newurl;
};

var remoteOpts = setBHRemoteOpts(false, '%QUERY', '/recipe/recipe/getSources', null);
var remoteOpts['replace'] = function(url, query) {return setSourceUrl(url, query);}; 
var bhOpts = setBHOptions(50, null, null, null, remoteOpts);
var sourceBH = new Bloodhound(bhOpts);

function initSourceTA() {

    var options = initTypeaheadOptions(true,true,1);
    var dataset = initTypeaheadDataset('source', null, 20, sourceBH);
    $('.srcTA').typeahead(options,dataset);
};

The filters could probably be added as generic functions in typeahead.js as well, but I only had one dataset that required them so I didn't do that. Same for the 'replace' option. As stated before, I am still relatively new to javascript/jQuery so I'm sure this solution could be improved upon, but it made setting up a typeahead a lot easier for me at least.