Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alter typeahead to accept synchronous/asynchronous data source #3682

Merged
merged 2 commits into from
Jun 3, 2012

Conversation

mlmorg
Copy link
Contributor

@mlmorg 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 added a commit that referenced this pull request Jun 3, 2012
Alter typeahead to accept synchronous/asynchronous data source
@fat fat merged commit 4a276b1 into twbs:2.1.0-wip Jun 3, 2012
@fat
Copy link
Member

fat commented Jun 3, 2012

this is beautiful - thanks!

@ioleo
Copy link

ioleo 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
Copy link
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. 
  }
})

@ioleo
Copy link

ioleo 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!

@ioleo
Copy link

ioleo 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
Copy link
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 + '}';

@ioleo
Copy link

ioleo 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
Copy link
Contributor

gcoop commented Jul 7, 2012

True it would :)

@ioleo
Copy link

ioleo 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
Copy link
Contributor

gcoop commented Jul 7, 2012

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

@ioleo
Copy link

ioleo commented Jul 8, 2012

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

@Serhioromano
Copy link

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
Copy link

@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
Copy link

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
Copy link

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
Copy link

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
Copy link
Collaborator

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

@twbs twbs 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.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants