Alter typeahead to accept synchronous/asynchronous data source #3682

Merged
merged 2 commits into from Jun 3, 2012

Projects

None yet

9 participants

@mlmorg
Contributor
mlmorg commented Jun 3, 2012

@fat: From existing pull request (#3250) against 2.1.0-wip. Sorry for not submitting it against a *-wip branch earlier.

This is a simple change to allow both synchronous and asynchronous data sources for the typeahead widget via a function/callback. This references issue #1336. Example:

$("input").typeahead({
  source: function (query, process) {
    $.get('/autocomplete', { q: query }, function (data) {
      process(data)
    })
  }
})

Because of the recently-added "updater" option, you can also handle objects:

var labels
  , mapped
$("input").typeahead({
  source: function (query, process) {
    $.get('/autocomplete', { q: query }, function (data) {
      labels = []
      mapped = {}

      $.each(data, function (i, item) {
        mapped[item.label] = item.value
        labels.push(item.label)
      })

      process(labels)
    })
  }
, updater: function (item) {
    return mapped[item]
  }
})
@fat fat merged commit 4a276b1 into twbs:2.1.0-wip Jun 3, 2012
@fat
Member
fat commented Jun 3, 2012

this is beautiful - thanks!

@loostro
loostro commented Jul 7, 2012

could you explain how handleing objects works?

  • succes function gets data = json_encoded Array of objects
  • how to specify which field should be matched?
  • how to specify which field should be displayed?
  • and finally, how to specify what value should be inserted into input?

In my case:

Song entity properties:
  - id
  - title
  - album (inverse side, association to Album entity)

Album entity properties:
  - id
  - name
  - artist (inverse side, association to Artist entity)
  - songs (owning side, association to Song entity)

Artist entity properties:
  - id
  - name
  - albums (owning side, association to Album entity)

So what I want to do is have a "Find song" input with typeahead helper.

.1) i want typeahead matcher to look for query in string (artist.name + ' - ' + song.title)

Example nr 1: query "ri" should match:

  • Rihanna - Umbrella (artist name matches)
  • AC/DC - Shoot To Thrill (song title matches)
  • Ricky Martin - Maria (both artist name and song title matches)

Example nr 2: query "ana - End" should match:

  • Oceana - Endless Summer (partly match artist name, partly match song title)

.2) i want typeahead to display helper as string:

Artist.name - Song.title (Album.name, Album.releaseDate)

Where releaseDate is in format YYYY.

Example nr 1: for query "ri" matches should display:

  • Rihanna - Umbrella (Good Girl Gone Bad, 2007)
  • AC/DC - Shoot To Thrill (Back in Black, 1980)
  • Ricky Martin - Maria (A Medio Vivir, 1995)

.3) i want input value (submitted in form) to be Artist.name - Song.title {Song.id}" and some callback function to update hidden field value (set it to Song.id)

Example nr 1: for query "ri"

Given results like in example nr 1
When user clicks / chooses one of results
Then typeahead input value should be set to "Artist.name - Song.title {Song.id}"
And some callback function should be fired, that sets hidden inputs value to Song.id

(only the hidden input will be submitted with the form)

Conclusion:

I am useing typeahead.js from branch 2.1.0-wip (downloaded on 06-07-2012, yesterday) and unfortunately I am getting errors becouse I do not understand exacly what purpose have these options/functions:

  • updater
  • process

And how exacly your code example helps with handleing objects. @mlmorg @fat Could you please explain?

Cheers

@gcoop
Contributor
gcoop commented Jul 7, 2012

I am using this update at the moment. in your case @loostro you need to do something like below.


var labels
  , mapped
$("input").typeahead({
  source: function (query, process) {
    // Query to server.
    $.get('/autocomplete', { q: query }, function (data) {
      // Server returns list of matched results, for example Rihanna - Umbrella (Good Girl Gone Bad, 2007) and AC/DC - Shoot To Thrill (Back in Black, 1980)
      // Each object should have two labels (it would be simpler if you only had one label format) and id. Label is in the format you're after Artist.name - Song.title (Album.name, Album.releaseDate) and the id is what will get put into your hidden input.
      labels = []
      mapped = {}

      // process method expects an array of strings only. Here loop the results from the server and make an array of just the result "label", also create a "mapped" array that contains the result label as a key and the id as the value. This allows you to get the corresponding song id for a selected song.
      $.each(data, function (i, item) {
        mapped[item.label] = { id: item.id, label: item.labelTwo }
        labels.push(item.label)
      })

      process(labels) // Tell typeahead to "process" these results (i.e. render them).
    })
  }
  // Method fired when a result is selected.
, updater: function (item) { // Item will be the selected rows text i.e. "Rihanna - Umbrella (Good Girl Gone Bad, 2007)"
    var selObj = mapped[item];
    // Do some js here to set the value of your hidden input (selObj.id).
    return selObj.labelTwo // Put the labelTwo attribute in the typeahead input. 
  }
})

@loostro
loostro commented Jul 7, 2012

Ok it seems I found the answer to why I got errors: in my application I forgot to serialize objects before returning them as JSON response to the script.

I got it working:

        var labels, mapped;
        $("#artist").typeahead({
          source: function (query, process) {
            $.post('{{ path('radiowww_test_lookup') }}', { q: query, limit: 8 }, function(data) {
              labels = [];
              mapped = {};

              // example response data:
              // [
              //   {
              //     "id":3,
              //     "title":"Shoot To Thrill",
              //     "track_no":1,
              //     "album": {
              //                "id":3,
              //                "name":"Iron Man 2",
              //                "released_at":"2010-04-19T00:00:00+0200",
              //                "artist": {
              //                           "id":2,
              //                           "name":"AC\/DC"
              //                          }
              //              }
              //   },
              //   { ..another song object ..}, 
              //   {.. another song object ..}, 
              //   {.. another song object ..}
              // ]


              $.each(data, function (i, item) {
                // each item is a Song object

                var query_label = item.album.artist.name + ' - ' + item.title;
                // example query_label: "AC/DC - Shoot To Thrill"

                // mapping item object
                mapped[query_label] = item;
                labels.push(query_label);
              });

              process(labels);
            }, 'json')
          }, 
          // Method fired when a result is selected.
          updater: function (query_label) {
            var item = mapped[query_label];
            var input_label = query_label + '{'+ item.id + '}';

            // Gonna do some js to save value to hidden input here...

            return input_label;
          },
        });

But I still don't know how to change what is displayed in the popup menu.

I'd like it to be Artist.name - Song.Title (Album.name, Album.releaseYear).

I'm confused, becouse there are no comments in typeahead.js code, and I'm not sure which function i need to use for that.

@gcoop: thanks for help!

@loostro
loostro commented Jul 7, 2012

Owkay, I figured it out =) Thanks @gcoop for help!

The final version of my code is:

        var labels, mapped;
        $("#artist").typeahead({
          source: function (query, process) {
            $.post('{{ path('radiowww_test_lookup') }}', { q: query, limit: 4 }, function(data) {
              labels = [];
              mapped = {};

              // example response data:
              // [
              //   {
              //     "id":3,
              //     "title":"Shoot To Thrill",
              //     "track_no":1,
              //     "album": {
              //                "id":3,
              //                "name":"Iron Man 2",
              //                "released_at":"2010-04-19T00:00:00+0200",
              //                "artist": {
              //                           "id":2,
              //                           "name":"AC\/DC"
              //                          }
              //              }
              //   },
              //   { ..another song object ..}, 
              //   {.. another song object ..}, 
              //   {.. another song object ..}
              // ]


              $.each(data, function (i, song) {
                var query_label = song.album.artist.name + ' - ' + song.title;
                // example query_label: "AC/DC - Shoot To Thrill"

                // mapping song object
                mapped[query_label] = song;
                labels.push(query_label);
              });

              process(labels);
            }, 'json')
          }, 
          // Method fired when a result is selected.
          updater: function (query_label) {
            var song = mapped[query_label];

            // Here gonna add some js to save song id to hidden input 
            // ...

            // If user selects an item, the inputed value will be for example "AC/DC - Shoot To Thrill {3}"
            var input_label = query_label + '{'+ song.id + '}';
            return input_label;
          },
          // Method responsible for element view
          highlighter: function (query_label) {
            var song = mapped[query_label];
            var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');

            var highlighted_label = query_label.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
              return '<strong>' + match + '</strong>'
            });

            // Item will be viewed as "AC/DC - Shoot To Thrill (Back to Black)
            var view_label = highlighted_label + ' (<i>' + song.album.name + '</i>)';            
            return view_label;
          }
        });
@gcoop
Contributor
gcoop commented Jul 7, 2012

What you have works, but is a bit awkward. Your using the highlighter method to essentially customise the html rendered, which doesn't make sense. Given highlighter has a specific purpose (to highlight the matched part). You can just leave the highlighter method alone if you change.

var query_label = song.album.artist.name + ' - ' + song.title;

To be:

var query_label = song.album.artist.name + ' - ' + song.title + ' (' + song.album.name + ')';

And then change updater from:

var song = mapped[query_label];
var input_label = query_label + '{'+ song.id + '}';
return input_label;

To be:

var song = mapped[query_label];
return song.album.artist.name + ' - ' + song.title + '{'+ song.id + '}';
@loostro
loostro commented Jul 7, 2012

@gcoop: wouldn't that cause highlighter and matcher also look through album.name part? I'd like to only match against Artist.name - Song.Title part.

@gcoop
Contributor
gcoop commented Jul 7, 2012

True it would :)

@loostro
loostro commented Jul 7, 2012

If highlighter is not supposed to be used like in my example, maybe there should be another function introduced for this puprose? @fat ?

@gcoop
Contributor
gcoop commented Jul 7, 2012

Have a look at #4025. You had the same issue I did, at the same time :)

@loostro
loostro commented Jul 8, 2012

Thanks. I will watch this PR, I hope it gets merged :)

@Serhioromano

Question:

In my case $.get() returns all possible values. I would like to return them once and then only search. But it make new request every keyup.

How to make o that typehead gets its values once and that it. No requests any more?

@TotoLaFouine

@Serhioromano: Try to replace the function after his 1st call by the labels received. It seems to work for me...

$(selector).typeahead({
    source: function (query, process) {
        labels = [];
        $.getJSON('your_url', function(data){
            $.each(data, function(i, elem){
                ...
            });
            process(labels);
        });
        this.source = labels;
    },
    updater: function(...
});
@BuhtigithuB

Hello,

I have a question...

Does the updater get it on empty field?

I found a hole in my use case... For example, if a user filled the typeahead field, pick a choice, the updater update the hidden field, but if the user then clear out the field he just filled, the updater doesn't seems to get it on blur for instance and the hidden field stays with the previous id of the key selected previouly...

I try with a naive if like this

updater: function (item) { if(! item) {update the hidden field and return item} else {return false}}

Then I go for this out side of typeahead :

jQuery(document).ready(function(){
$("input#table_field_ac").blur(function(){ if($(this).val().length==0) {$('#table_field').val('')} }); // The latter is the hidden field
});

Thanks to point to the rigth direction if I am wrong and miss something...

@BuhtigithuB

This solution not even works perfectly in case where the user only earase a couples of characters a the end of the entry by mistake... So it needs a kind of double check on blur...

@yli01
yli01 commented Nov 4, 2013

I tried to use typeahead with asp.net mvc 4 application. I used:

<script src="@Url.Content("~/Scripts/jquery-1.8.3.min.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery-ui-1.9.2.custom.min.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<script src='http://twitter.github.com/typeahead.js/releases/latest/typeahead.js'></script> 

or

<script src="http://twitter.github.io/typeahead.js/releases/latest/typeahead.js" ></script>
<script type="text/javascript">
    $(function () {
        $('#search').typeahead({
            name: 'Name',
            limit: 10,
            prefetch: '/ControllerName/ActionName'
});

it works. But if I change to

$('#search').typeahead({
            name: 'Name',
            limit: 10,
            source: function(query, process) { ...},
            updater: function(item) { ...}
});

it does not work. It seems it ask for either local or prefetch. My questions:
Does the typeahead work with asp.net mvc 4?
What is the correct typeahead library I should use?
Thanks

@cvrebert
Member
cvrebert commented Nov 4, 2013

@yli01 This issue is about Bootstrap's old typeahead widget, so your problem isn't relevant here. We recommend Twitter's Typeahead.js. For problems with it, ask at their project: https://github.com/twitter/typeahead.js

@cvrebert cvrebert locked and limited conversation to collaborators Jul 28, 2014
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.