Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Twitter typeahead add custom data to dataset

I am using twitter typeahead to show category suggestions which works fine for me. The problem is with the display of sub-categories. If a category has sub-categories then expand/collapse icon is shown along with the suggestion, if the user selects one them the sub-categories has to be shown below it, i have succeeded in the display part but cannot update the dataset of typeahead so that when the user selects one the sub-categories it should be displayed in the input. Please use keyboard on Jsfiddle because i have tried only in keypress. The following is my json array

JSFIDDLE here

[{
    "id": "38",
    "value": "Entertaintment",
    "parent_id": "27",
    "children": "1",
    "childCategories": [{
        "id": "28",
        "value": "Movies",
        "parent_id": "38"
    }, {
        "id": "29",
        "value": "Sports",
        "parent_id": "38"
    }]
}, {
    "id": "40",
    "value": "eeen",
    "parent_id": "27",
    "children": "1",
    "childCategories": [{
        "id": "41",
        "value": "een child 1",
        "parent_id": "40"
    }]
}, {
    "id": "41",
    "value": "een child 1",
    "parent_id": "40",
    "children": "0",
    "childCategories": false
}]

I have tried the following:

_onEnterKeyed: function onEnterKeyed(type, $e) {
    var cursorDatum, topSuggestionDatum;
    cursorDatum = this.dropdown.getDatumForCursor();
    topSuggestionDatum = this.dropdown.getDatumForTopSuggestion();
    //My custom condition, if category has children show sub-categories below 
    if (cursorDatum.raw && cursorDatum.raw.children == "1") {
        //below line is my failed try  
        //this.dropdown.updateChild('',cursorDatum.raw.childCategories,false); 
        var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave;
        var childHTML='';
        $('.ttchild').remove();
        //adding the sub-categories to dropdown
        $.each(cursorDatum.raw.childCategories,function(i,v){
            childHTML += '<div class="tt-suggestion ttchild"><div class="suggestions"><span>'+this.value+'</span></div></div>';
        });
        $(childHTML).insertAfter('.tt-cursor');
        return;
    }

    if (cursorDatum) {
        this._select(cursorDatum);
        $e.preventDefault();
    } 
    else if (this.autoselect && topSuggestionDatum) {
        this._select(topSuggestionDatum);
        $e.preventDefault();
    }
},

//This is my custom function trying to update the dataset, but i know its totally wrong.
updateChild: function updateChild(query,childs) {
    var that = this;
    this.query = query;
    this.canceled = false;
    this.source(query, render);

    function render(childs) {
        if (!that.canceled && query === that.query) {
            that._render(query, childs);
        }
    }
},

JSFIDDLE here

e.g: The Entertainment category has subcategories movies and sports.

Hope any one would help me....

like image 578
Nouphal.M Avatar asked Aug 21 '14 06:08

Nouphal.M


2 Answers

Edit v.2

I refactored again, now it works perfect.

I removed my previous edits and refactored onEnterKeyed function as follows

var dataSet = this.dropdown.datasets[0];

// childHTML is replaced by childDom
// var childHTML='';
var childDom = $('<div></div>');
$('.ttchild').remove();
$.each(cursorDatum.raw.childCategories,function(i,v){

    var div = $('<div class="tt-suggestion ttchild"></div>');
    div.append('<div class="suggestions"><span>'+this.value+'</span></div>');


    var suggestion = v;

    // TODO console log this is duplicated from dataset
    var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum";


    // this is what makes it show in the input box
    // set these data for the child element.
    div.data(datasetKey, dataSet.name);
    div.data(valueKey, dataSet.displayFn(suggestion));
    div.data(datumKey, suggestion);

    childDom.append(div);
});

// childHtml is replaced by childDom
$('.tt-cursor').append(childDom.children());

Deprecated Edit v.1

I refactored my solution, now it works as expected.

I removed my previous edits and added this line in the getSuggestionNode method.

for (var key in suggestion.childCategories) {
    var value = suggestion.childCategories[key];
    $el.append(getSuggestionNode(value));
}

Deprecated Below

I solved your problem, though it needs improvement on your part.

// I added this line
// add the child nodes to the suggestions.
suggestions = suggestions.reduce(function(res, num) { res.push(num); for (var i=0;i<num.children;i++) res.push(num.childCategories[i]);  return res; }, [])

// nodes are calculated based on suggestions
// but child nodes are not added to suggestions so their data is not set (see below line)
nodes = _.map(suggestions, getSuggestionNode);

// inside the suggestions.map function
// here .data sets the necessary values required to render the input on cursor change.
// since child nodes are not mapped they are ignored.
$el = $(html.suggestion).append(that.templates.suggestion(suggestion))
    .data(datasetKey, that.name)
    .data(valueKey, that.displayFn(suggestion))
    .data(datumKey, suggestion);

So you don't need a custom updateChild function, though I don't know how you implement the dropdown stuff, this solution will add the child to the dropdown and show them instantly, without pressing enter in the parent node. You should configure that on your own.

like image 122
user3995789 Avatar answered Sep 30 '22 21:09

user3995789


Here's a different approach

You could treat each group of childCategories as it's own datasource. Then use the Multiple Datasets paradigm to automatically apply a header for child items.

The second parameter to $.typeahead(options, [*datasets]) takes an array of datasets. We can convert each set of child elements into it's own dataset like this:

var dataSources = [];
$.each(movies, function(index, movieItem) {
    if (movieItem.childCategories) {
        var bloodhoundEngine = createBlounhoud(movieItem.childCategories, 'value');
        var dataSource = {
            name: movieItem.value,
            displayKey: 'value',
            source: bloodhoundEngine.ttAdapter(),
            templates: {
                header: '<h3 class="typeahead-group">' + movieItem.value + '</h3>'
            }
        };
        dataSources.push(dataSource);
    }
});

Notice that we've defined a header template for items in this datasource. We can style it like this:

.typeahead-group {
  margin: 0 20px 5px 10px;
  padding: 3px 0;
  border-bottom: 1px solid #ccc;
}

Elements that were not part of a child group could could all get added as part of a single ungrouped datasource:

var ungrouped = [];
$.each(movies, function(index, movieItem) {
    if (!movieItem.childCategories) {
        ungrouped.push(movieItem);
    }
});

function addUngroupedItems() {
    var bloodhoundEngine = createBlounhoud(ungrouped, 'value');
    var dataSource = {
        name: 'ungrouped',
        displayKey: 'value',
        source: bloodhoundEngine.ttAdapter()
    };
    // add to first position of array so it doesn't look like it's part of the group above it
    dataSources.unshift(dataSource);
};

This solution has the benefit of minimally overriding existing typeahead and bloodhound code while working within the framework to provide grouping functionality.

Also, it provides an intuitive UI for users that might know the name of the name of a child item they're looking for, but don't want to have to click through each category to find it. Most local searches can trivially search on all fields.

Working Demo in jsFiddle

screenshot

Aside: I would try to avoid modifying the source code directly. It makes it very difficult to see where you have made your own modifications. Instead, try using a CDN for the whole library and writing a plugin or wrapper on top of it. This will make incoporating future revisions much easier.

like image 24
KyleMit Avatar answered Sep 30 '22 20:09

KyleMit