Typeahead works if I give a static array as the source, but when I try to generate the array with function it won't populate its dropdown list.
$("#mix-artist" ).typeahead({
source: function(query, process) {
Mix.searchArtist(query);
},
minLength: 3,
});
The function for the source:
searchArtist: function(query) {
$.get(API_MAIN_URL, {a: "search.artists", q: query},
function(data) {
artists = [];
query = query.toLowerCase();
if (data['aData']) {
for (var i = 0; i <= data['aData'].length; i++) {
if (data['aData'][i] && data['aData'][i]['sName'].toLowerCase().indexOf(query) == 0) {
artists.push(data['aData'][i]['sName']);
}
}
}
console.log(artists);
return artists;
});
}
Sample output of the source function:
["Arcangel", "Arc Angels", "Arcade Fire", "Arctic Monkeys", "Archers of Loaf", "Architecture in Helsinki", "Archie Shepp", "Arcade", "Arch Enemy", "Arcadia", "Archie Eversole"]
What can I do to get typeahead to accept this array?
You need your searchArtist function to take the process callback. Instead of returning the array you should call process(artists).
Related
When I use select2, the list of choice doesn't reduce while searching, which is pretty annoying when I have more than 50 choices..
I use symfony4 framework, with a route providing my tags that are in a DB:
/**
* #Route("/miseenpage/keywords.json", name="keywords", defaults={"_format": "json"})
*/
Then I init my select2 with this code provided by the symfony Tags plugin:
$( document ).ready(function() {
initTags($('input[name$="[tagsText]"]'));
});
// TAGS
function initTags($input) {
if($input.length) {
$input.attr('type', 'hidden').select2({
width: '85%',
tags: true,
tokenSeparators: [","],
createSearchChoice: function (term, data) {
if ($(data).filter(function () {
return this.text.localeCompare(term) === 0;
}).length === 0) {
return {
id: term,
text: term
};
}
},
multiple: true,
ajax: {
url: $input.data('ajax'),
dataType: "json",
data: function (term, page) {
return {
q: term
};
},
results: function (data, page) {
return {
results: data
};
}
},
initSelection: function (element, callback) {
var data = [];
function splitVal(string, separator) {
var val, i, l;
if (string === null || string.length < 1) {
return [];
}
val = string.split(separator);
for (i = 0, l = val.length; i < l; i = i + 1) {
val[i] = $.trim(val[i]);
}
return val;
}
$(splitVal(element.val(), ",")).each(function () {
data.push({
id: this,
text: this
});
});
callback(data);
}
});
}
}
Here is the result in my webpage before searching:
Here is after searching:
As you can see, the list is still the same, just the result is underlined. I'd like to filter the result depending on what I search, so I don't have to scroll all the way until I find my underlined term.
As it is the basic working of Select2, I guess there is an configuration problem in my JS.
I instanciated this value on load:
let currentSearch = false;
Then I changed the "AJAX" section of my JS so it loops around the list with the searched value:
ajax: {
url: $input.data('ajax'),
dataType: "json",
data: function (term) {
currentSearch = term;
return {
q: term
};
},
results: function (data) {
let returnTab = [];
data.forEach(function(e) {
if(e.text.includes(currentSearch)){
returnTab.push(e);
}
});
return {
results: returnTab
};
}
},
I'm pretty sure there is a cleaner/simpler way of doing this, but for moment it's a quickfix.
Just a basic example with some code to show how we use Select2 (v4.0.3, 3.5 may be quite different) with Ajax
On a twig, we have for example :
$("#users").css('width', '100%').select2({
minimumInputLength: 3,
ajax: {
url: "{{ path('api_search_user') }}",
dataType: 'json',
delay: 500,
data: function (params) {
return {
needle: params.term // Here we send user input to the controller
};
},
processResults: function (data) {
return {
results: data
};
},
cache: false
}
});
And we have a controller with a function to search for users :
/**
* #Route("/search_users", name="api_search_users")
* #param Request $request
* #return JsonResponse
*/
public function searchUsersAction(Request $request)
{
$needle = $request->get('needle'); // Here we retrieve user input
$users = $this->get(User::class)->searchUsers($needle);
return new JsonResponse($users );
}
EDIT following you last comment
This value is not valid
That's because your value isn't part of the select when the formbuilder add the field. You must add an addEventListener on PRE_SUBMIT to dinamically add this value. You can do it this way :
// Define form modifier
$usersFormModifier = function (FormInterface $form, $users) use ($options) {
$choices = array();
if(is_array($users)) {
$choices = $users;
}
$form->add(
'users',
EntityType::class,
array(
'label' => 'conversation.form.users',
'multiple' => true,
'class' => 'AppBundle\Entity\Security\User',
'choices' => $choices,
'choice_label' => function (User $user) {
return $user->getLastName() . " " . $user->getFirstName();
},
'attr' => array(
'placeholder' => 'default.search_person'
)
)
);
};
// On PRE_SET_DATA, we load users from our object (which only contains their IDs)
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($usersFormModifier) {
$usersFormModifier($event->getForm(), $event->getData()->getUsers()->toArray());
}
);
// On PRE_SUBMIT, we add the options, so the select will accept newly added values
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($usersFormModifier) {
$data = $event->getData();
$users = null;
if(isset($data['users'])) {
$users = $this->entityManager->getRepository('AppBundle:Security\User')->findBy(array('id' => $data['users']));
}
$usersFormModifier($event->getForm(), $users);
}
);
As I said at the beginning, if you don't have so muchs tags, and they don't evolve regularly, you can construct your select with all your tags and avoid all the Ajax part of the Select2 javascript code ! (I think it will be easier and sufficient for your case)
I am trying to construct my own JSON object from multiple online image/photography sources. The below code should explain what I am trying to accomplish:
var searchUnsplash = require('./apis/unsplash');
var searchFlickr = require('./apis/flickr');
function combineObjs(callback) {
var obj = {}
var key = 'item';
obj[key] = [];
searchFlickr.searchFlickr(searchTerm, searchCount, searchPage,
function (callback) { // each API call is in a separate file with these functions exported
obj[key].push(flickrObj); // this does not work
// console.log(flickrObj) // this works
});
searchUnsplash.searchUnsplash(searchTerm, searchCount, searchPage,
function (callback) {
obj[key].push(unsplashObj);
// console.log(unsplashObj)
});
console.log(obj)
}
combineObjs();
The goal is to end up with a JSON object like below:
["item": {
"id": 1,
"title": 2,
"content": 3,
"source": "flickr"
},
"item": {
"id": 1,
"title": 2,
"content": 3,
"source": "unsplash"
}]
etc, which I can use to power my front end.
I am a beginner to javascript and I am just working off what I have learned in tutorials and articles, so I might be using the wrong approach entirely for what I am aiming to achieve. Happy to take any pointers
search function:
function searchUnsplash(term, count, page, callback) {
request(`https://api.unsplash.com/search/photos/?per_page=${count}&page=${page}&query="${term}"&client_id=KEY&`,
function searchResult(error, response, body) {
if (!error && response.statusCode == 200) {
var info = JSON.parse(body)
for ( var i = 0; i < info.results.length; i++) {
obj = {
id: `us-${info.results[i].id}`,
}
callback(obj);
}
}
})
}
module.exports.searchUnsplash = searchUnsplash;
First, your intended result is not correct. You can't name "item" the individual array entries. A corrected and working example would be this one.
[ {
"id": 1,
"title": 2,
"content": 3,
"source": "flickr"
},
{
"id": 1,
"title": 2,
"content": 3,
"source": "unsplash"
}]
Second, you mistake JSON for your data structure. JSON is just the text notation. So, let's see first how to build a suitable data array.
let results = [];
results.push( { id:1, title:2, content:3, source:"flickr" });
results.push( { id:2, title:4, content:6, source:"unsplash" });
And then with JSON.stringify(results) will code your results into JSON.
Finally, you mix up the aynchronous calls in your code with synchronous invocations. You need to save the results on the callback of the individual functions, that is when you really obtain the responses asynchronously. Also, you need to count the pending results and invoke the final callback when all done.
So, putting all the pieces together, in a contrived fake example, we just invoke twice the search functions and so we callback when two results are combined.
function combineObjs(callback) {
let results = [];
function partialResult(obj) {
results.push(obj);
if (results.length=2) callback(results);
};
searchFlickr(searchTerm, searchCount, searchPage, partialResult);
searchUnsplash(searchTerm, searchCount, searchPage,partialResult);
}
combineObjs( function(results) { console.log(JSON.stringify(results)) });
This is excessive but it would work. It can be used over and over and over again. :D
Run the snippet to see a result
class JSONBuilder
{
constructor(contents=null)
{
if(!contents)
{
//Private objecy hash that isn't publicly accessible
var objectHash = {};
//Get stashed item
this.GetItem = function(key)
{
if(!key) throw new Error("Null or Underfined Key Passed.");
let value = objectHash[key];
if(!value) throw new Error(`Key : ${key} Not Found in JSON objectHash`);
return value;
}
//Set an item in the objecy hash
this.SetItem = function(key, value)
{
if(!key) throw new Error("Null or Underfined Key Passed.");
if(!value) throw new Error("Null or Underfined Key Not Found.");
objectHash[key] = value;
}
//Remove item from the hash
this.DeleteItem = function(key)
{
if(!key) throw new Error("Null or Underfined Key Passed.");
if(!objectHash[key]) throw new Error(`Key : ${key} Not Found in JSON objectHash`);
objectHash.DeleteItem(key);
}
//Turn items into a JSON object
this.Build = function()
{
return JSON.stringify(objectHash);
}
}
else
{
//If a string is passed as a paremeter, reconstruct from that
try
{
objectHash = JSON.parse(contents);
}
catch(Err)
{
console.log("Parsing of JSON Content Failed.");
throw Err;
}
}
}
}
class Item
{
constructor(id, source, content, title)
{
this.Id = id;
this.Source = source;
this.Content = content;
this.Title = title;
}
}
let builder = new JSONBuilder();
let itemContainer = [];
itemContainer.push(new Item(1, 'flicker', 'stuff', 'item1'));
itemContainer.push(new Item(2, 'flicker', 'morestuff', 'item2'));
builder.SetItem('items', itemContainer);
console.log(builder.Build());
The javascript below is supposed to be some sort of autocomplete. I am using bootstrap typeahead.
When I type items in my input field, I am able to see suggestions, the problem is I am not able to select them and populate the input field.
Any idea what may be wrong with it?
<script type="text/javascript">
$('#typeahead').typeahead({
source: function (query, process) {
objects = [];
map = {};
return $.get('live_search.php?filter=relation', { query: query }, function (data) {
console.log(data);
var data = $.parseJSON(data);
return process(data);
});
$.each(data, function(i, object) {
map[object.name] = object;
objects.push(object.name);
});
process(objects);
},
updater: function(item) {
$('#getSelection').val(map[item].name);
$('#getValue').val(map[item].name);
return item;
}
});
</script>
It looks like your source method returns immediately from the $.get call and never enters the $.each iteration. Move the iteration code into the get request callback block.
Also you're references to objects and map are in the scope of the source method, but you reference them in the updater method
var typeahead = {
objects: [],
map: {},
};
$("#typeahead").typeahead({
source: function(query, process) {
return $.get("live_search.php?filter=relation", { query: query }, function(
data
) {
var data = $.parseJSON(data);
$.each(data, function(i, object) {
typeahead.map[object.name] = object;
typeahead.objects.push(object.name);
});
return process(data);
});
},
updater: function(item) {
$("#getSelection").val(typeahead.map[item].name);
$("#getValue").val(typeahead.map[item].name);
return item;
},
});
Hello I am generating the following array as result from the search:
[{"id":"292","clientname":"John Mick"},{"id":"293","clientname":"John Tom"}]
this is what it is returned from MySQL Query, but in my Twitter typeahead configuration nothing is showing. Instead of to show 2 results.
Here is my typeahead code:
<script type="text/javascript">
$(function() {
// Instantiate the Bloodhound suggestion engine
var curr = {};
var suggestions = new Bloodhound({
datumTokenizer: function(datum) {
return Bloodhound.tokenizers.whitespace(datum.value);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: 'includes/livesearch.php?key=%QUERY',
wildcard: '%QUERY',
filter: function (clientname) {
curr[clientname.clientname] = clientname.id;
// Map the remote source JSON array to a JavaScript object array
return $.map(clientname, function (clientname, index) {
return index === "clientname" ? {
value: clientname
} : null;
});
}
}
});
// Initialize the Bloodhound suggestion engine
suggestions.initialize();
// Instantiate the Typeahead UI
$(".typeahead").typeahead({
hint: true,
minLength: 1
}, {
limit: 7,
displayKey: 'value',
source: suggestions.ttAdapter(),
})
.on("typeahead:selected", function (e, datum) {
$("form [name=clientid]").val(curr[datum.value]); // set value here
curr = {};
var value = document.getElementById("clientid").value;
if (value == 292) {
$("#alltask").hide();
$("#singletask").show();
} else {
$("#singletask").hide();
$("#alltask").show();
}
});
})
</script>
Any help to resolve this difficult situation is welcome. Thank you!
I am working on an app that uses Select2 (version 3.5.1). The HTML to setup this drop down / autocomplete field looks like this:
<input id="mySelect" class="form-control" type="hidden">
The form-control class in this snippet comes from Bootstrap. I am initializing this field from JavaScript using the following:
function getItemFormat(item) {
var format = '<div>' + item.ItemName + '</div>';
return format;
}
$(function() {
$('#mySelect').select2({
minimumInputLength: 5,
placeholder: 'Search for an item',
allowClear: true,
ajax: {
url: '/api/getItems',
dataType: 'json',
quietMillis: 250,
data: function (term, page) {
return {
query: term
};
},
results: function (data, page) {
return { results: data, id: 'ItemId', text: 'ItemText' };
}
},
formatResult: getItemFormat,
dropdownCssClass: "bigdrop",
escapeMarkup: function (m) { return m; }
});
});
When my select field loads, it successfully renders. Once I type at least the fifth character, it successfully pulls items from the server and lists them as options. However, if I try to select one of them, nothing happens. The drop-down popup stays open. Nothing gets put in the actual field. There are no errors in the JavaScript console. Its like I didn't click anything.
In addition, I noticed that nothing is highlighted when I put my mouse over an item or attempt to navigate the list of options with the arrow keys.
What am I doing wrong?
What is happening:
By default, results of the object you are returning in ajax.results should be an array in this structure [{id:1,text:"a"},{id:2,text:"b"}, ...].
results: function (data, page) {
var array = data.results; //depends on your JSON
return { results: array };
}
In Select2.js it actually states:
* #param options.results a function(remoteData, pageNumber, query) that converts data returned form the remote request to the format expected by Select2.
* The expected format is an object containing the following keys:
* results array of objects that will be used as choices
* more (optional) boolean indicating whether there are more results available
* Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
Reading the source code, we can see that ajax.results is called on AJAX success:
success: function (data) {
// TODO - replace query.page with query so users have access to term, page, etc.
// added query as third paramter to keep backwards compatibility
var results = options.results(data, query.page, query);
query.callback(results);
}
So ajax.results is really just a function for you to format your data into the appropriate structure ( e.g. [{id:a,text:"a"},{id:b,text:"b"}, ...]) before the data is passed to query.callback:
callback: this.bind(function (data) {
// ignore a response if the select2 has been closed before it was received
if (!self.opened()) return;
self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
self.postprocessResults(data, false, false);
if (data.more===true) {
more.detach().appendTo(results).html(self.opts.escapeMarkup(evaluate(self.opts.formatLoadMore, self.opts.element, page+1)));
window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
} else {
more.remove();
}
self.positionDropdown();
self.resultsPage = page;
self.context = data.context;
this.opts.element.trigger({ type: "select2-loaded", items: data });
})});
And what query.callback eventually does is to set the logic up properly so that everything works fine when you choose one of the items and trigger .selectChoice.
selectChoice: function (choice) {
var selected = this.container.find(".select2-search-choice-focus");
if (selected.length && choice && choice[0] == selected[0]) {
} else {
if (selected.length) {
this.opts.element.trigger("choice-deselected", selected);
}
selected.removeClass("select2-search-choice-focus");
if (choice && choice.length) {
this.close();
choice.addClass("select2-search-choice-focus");
this.opts.element.trigger("choice-selected", choice);
}
}
}
So if there is some misconfiguration (e.g. results is not in the correct structure) that causes the class .select2-search-choice-focus not to be added to the DOM element before .selectChoice is called, this is what happens:
The drop-down popup stays open. Nothing gets put in the actual field. There are no errors in the JavaScript console. Its like I didn't click anything.
Solutions
There are many solutions to this. One of them is, of course, do some array keys manipulation in ajax.results.
results: function (data, page) {
//data = { results:[{ItemId:1,ItemText:"a"},{ItemId:2,ItemText:"b"}] };
var array = data.results;
var i = 0;
while(i < array.length){
array[i]["id"] = array[i]['ItemId'];
array[i]["text"] = array[i]['ItemText'];
delete array[i]["ItemId"];
delete array[i]["ItemText"];
i++;
}
return { results: array };
}
But you may ask: why must the id be "id" and the text be "text" in the array?
[{id:1,text:"a"},{id:2,text:"b"}]
Can the array be in this structure instead?
[{ItemId:1,ItemText:"a"},{ItemId:2,ItemText:"b"}]
The answer is yes. You just need to overwrite the id and text functions with your own functions.
Here are the original functions for .selecte2 in Select2.js:
id: function (e) { return e == undefined ? null : e.id; },
text: function (e) {
if (e && this.data && this.data.text) {
if ($.isFunction(this.data.text)) {
return this.data.text(e);
} else {
return e[this.data.text];
}
} else {
return e.text;
}
},
To overwrite them, just add your own functions inside the object you are passing to .selecte2:
$('#mySelect').select2({
id: function (item) { return item.ItemId },
text: function (item) { return item.ItemText }
......
});
Updates
What else is happening :
However, the text of the selected item does not appear in the field after the list closes.
This means .selectChoice has been successfully executed. Now the problem lies in .updateSelection. In the source code:
updateSelection: function (data) {
var container=this.selection.find(".select2-chosen"), formatted, cssClass;
this.selection.data("select2-data", data);
container.empty();
if (data !== null) {
formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup);
}
if (formatted !== undefined) {
container.append(formatted);
}
cssClass=this.opts.formatSelectionCssClass(data, container);
if (cssClass !== undefined) {
container.addClass(cssClass);
}
this.selection.removeClass("select2-default");
if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
this.container.addClass("select2-allowclear");
}
}
From here we can see that, before the corresponding string of text is placed into the input, it would call formatSelection.
formatSelection: function (data, container, escapeMarkup) {
return data ? escapeMarkup(this.text(data)) : undefined;
},
Update: Solution
Previously I thought this.text(data) can be overwritten by having text: funcion(item){ ... } in the parameters, but sadly it doesn't work that way.
Therefore to render the text properly in the field, you should overwrite formatSelection by doing
$('#mySelect').select2({
id: function (item) { return item.ItemId },
formatSelection: function (item) { return item.ItemText }
//......
});
instead of trying to overwrite text (which should supposedly have the same effect but this way of overwriting is not yet supported/implemented in the library)
$('#mySelect').select2({
id: function (item) { return item.ItemId },
text: function (item) { return item.ItemText } //this will not work.
//......
});
The issue you are facing is that select2 wants all your results to have an id property. If they don't you need to initialise with an id function which returns the id from each result.
It will not allow you to select a result unless you satisfy one of these. So in the case of your example :
function getItemFormat(item) {
var format = '<div>' + item.ItemName + '</div>';
return format;
}
$(function() {
$('#mySelect').select2({
minimumInputLength: 5,
placeholder: 'Search for an item',
allowClear: true,
id: function(item) { return item.ItemId; }, /* <-- ADDED FUNCTION */
ajax: {
url: '/api/getItems',
dataType: 'json',
quietMillis: 250,
data: function (term, page) {
return {
query: term
};
},
results: function (data, page) {
return { results: data, id: 'ItemId', text: 'ItemText' };
}
},
formatResult: getItemFormat,
dropdownCssClass: "bigdrop",
escapeMarkup: function (m) { return m; }
});
});
You need to provide an ID that returns from your API like #itsmejodie said.
The other problem is that you have to provide select2 formatResult and formatSelection functions, once you have it loaded from Ajax but you can't put html on that. e.g.:
function format (item) {
return item.name;
}
$(function() {
$('#mySelect').select2({
minimumInputLength: 2,
placeholder: 'Search for an item',
allowClear: true,
ajax: {
url: '/api/getItems',
dataType: 'jsonp',
quietMillis: 250,
data: function (term, page) {
return {
query: term
};
},
results: function (data, page) {
return { results: data };
}
},
formatResult: format,
formatSelection: format
});
});
For version 4 of Select2 use
processResults: function (data) {
instead of
results: function (data) {