Rails / typeahead.js on huge database - javascript

I am using typeahead.js to fill in a belongs_to field on a Rails form. When you create a Post, you pick the associated Book. There are thousands of records. The app is crashing on that page if I let typeahead.js look at Book.all, but if I limit to a couple hundred with Book.featured, it seems to work. How can I give the typeahead access to all the data, though?
/api/books.json returns all the records, not sure if that's helpful?
Here is how I have it set up:
var substringMatcher = function(strs) {
return function findMatches(q, cb) {
cb(strs);
};
};
books = <%= Book.featured.to_json.html_safe %>
var bookFinder = function(books) {
$('#post_book_id').val(null)
// $('#post_book_name').val(null)
return function findMatches(q, cb) {
matches = [];
$.each(books, function(i, book){
if(book.title.indexOf(q) !== -1){
matches.push(book)
}
})
cb(matches)
}
}
$('#post_book_name').typeahead({
hint: true,
highlight: true,
minLength: 2
},
{
name: 'books',
displayKey: 'title',
source: bookFinder(books)
});
$('#post_book_name').on('typeahead:select', function(ev, suggestion) {
$('#post_book_id').val(suggestion.id)
});

Related

Multiple Remote Sources with Twitter Typeahead.js

I'm using the Twitter typeahead.js library for typeahead search with a single data source /api/domains.
I'd now like to add two more sources using other API endpoints:
/api/ips
/api/hostnames
How would I accomplish this? Do I need to use the BloodHound engine for this functionality?
Existing Code
<script>
$('#header').on('click', '.tt-selectable', function() {
window.location.href = "/explore/" + $(this).attr('data-domain');
});
var substringMatcher = function(strs) {
return function findMatches(q, cb) {
var matches, substringRegex;
// an array that will be populated with substring matches
matches = [];
// regex used to determine if a string contains the substring `q`
substrRegex = new RegExp(q, 'i');
// iterate through the pool of strings and for any string that
// contains the substring `q`, add it to the `matches` array
$.each(strs, function(i, str) {
if (substrRegex.test(str)) {
matches.push(str);
}
});
cb(matches);
};
};
$.getJSON('/api/domains')
.done(function( json ) {
populateSuggestions(json.domains);
})
.fail(function( jqxhr, textStatus, error ) {
var err = textStatus + ", " + error;
console.log( "Request Failed: " + err );
});
const populateSuggestions = function(data) {
$('.typeahead').typeahead({
hint: true,
highlight: true,
minLength: 1
}, {
displayKey: 'name',
limit: 10,
source: substringMatcher(data),
templates: {
suggestion: function (data) {
return "<div data-domain=" + data + ">" + data + "</div>"
}
}
});
}
</script>
Existing API response
{
domains: [
"a.com",
"b.com",
"c.com",
"d.com" ]
}
Do you want the suggestions differentiated by the source? I would expect so, since one source is domains, one is IP addresses (I presume?) and the other is hostnames. The first two I would expect someone to be typing in very different entries and getting back very different data.
Anyway, that's frequently how I use typeahead - similar searches but with multiple sources - and yes I use Bloodhound to set up each source. Then for each bloodhound source I have a separate JSON object in the typeahead setup. Like this:
$('#searchbox .typeahead').typeahead({
hint: true,
highlight: true,
autoselect: true,
minLength: 2
},
{
name: 'other',
display: 'addr',
source: otherBH,
templates: {
header: "<span class='typeahead-header'>Static</span>"
}
},
{
name: 'lookupSvc',
display: 'lookupSvc',
source: lookupServiceBH,
templates: {
header: "<span class='typeahead-header'>AJAX</span>"
}
}
...

AutoComplete with Key-Value in grid, showing key when focus

I am using paramquery grid component in which I am trying to use autocomplete.
Column Model for branch:
{ title: "Branch", dataIndx: "branchId", width: 150,
filter: { type: "select",
condition: 'equal',
prepend: { '': '--All--' },
listeners: ['change'],
valueIndx: "branchId",
labelIndx: "branchName",
options: branchList,
},
editor: {
type: "textbox",
init: autoCompleteEditor
//type: function (ui) { return dropdowneditor(this, ui); }
},
render: function (ui) {
for (var i = 0; i < branchList.length; i++) {
var option = branchList[i];
if (option.branchId == ui.rowData.branchId) {
return option.branchName;
}
}
}
}
autoCompleteEditorMethod:
var autoCompleteEditor = function (ui) {
var $inp = ui.$cell.find("input");
//initialize the editor
$inp.autocomplete({
source: function(request, response) {
var rows = imAutocompleteJSONParse(branchList);// this method converting my JSON object into Value and label format.
return response(rows);
},
selectItem: { on: true }, //custom option
highlightText: { on: true }, //custom option
minLength: 0,
select: function(event, ui) {
event.preventDefault();
$(this).val(ui.item.label);
},
focus: function(event, ui) {
event.preventDefault();
$("#search").val(ui.item.label);
}
}).focus(function () {
//open the autocomplete upon focus
$(this).autocomplete("search", "");
});
}
I get branch id into my grid and I have branchList JSON which have branch id & branch Name. Inside grid my render function showing branchName on UI.
But when I click on searchable dropdown I'm getting branch id.
Below snapshot may explain my issue properly.
Summary of issue: I am getting branch id in Grid. With help of render method I am able to show branch name on grid. but when I click on textbox I getting branch id.
http://jsfiddle.net/v4zx8tjc/4/
Like blackmiaool suggests in his comment, this question would be easier to answer with a live demo using something like JSFiddle.
Based on what I can see in your question, which isn't that much, there are a few areas I would take a second look at.
The Source function in JQuery.autoComplete. Where is branchList coming from? I don't see it declared anywhere and why are you not using the 'request' param?
Not sure what your custom properties are doing but it might be a good idea to verify those are not interfering with the results.
Edit 1: Looking back at the code you posted I think I see where your branchList variable is coming from. It would be very helpful to see your imAutocompleteJSONParse() method because I believe that may be where things are breaking down.

Algolia template if no hits are returned

I've implemented my own Algolia PoC based of https://www.algolia.com/doc/search/auto-complete and I'm now struggling with a specific use case: how can I handle a search which does not return any hits?
Here is my code:
I've been able to identify and detect when/where no hits are returned, but I can't do anything beside just using a console.log(). I tried to get a custom return_msg but I can't call the function.
I also tried to do some tweak under suggestion: function(suggestion) but this function is never called if no hits are returned.
I also did not found any documentation about this "Templates" section on https://github.com/algolia/autocomplete.js
$('#q').autocomplete({ hint: false }, [
{
source: function(q, cb) {
index.search(q,
{ hitsPerPage: 10 },
function(error, content) {
if (error) {
cb([]);
return;
}
if (content.nbHits == 0)
{ return_msg = '<h5> Sorry, no result </h5>';
// DO something here
console.log(return_msg);
// console.log return "Sorry, no result"
}
cb(content.hits, content);
});
},
displayKey: 'game',
templates: {
suggestion: function(suggestion) {
return_msg = '<h5> '+ suggestion.MY_ATTRIBUTE + '</h5>'
return return_msg;
}
}
}
]).on('autocomplete:selected', function(event, suggestion, dataset) {
window.location = (suggestion.url);
});
Any pointers would be greatly appreciated =)
Using the templates option of your dataset you can specify the template to use when there are no results:
source: autocomplete.sources.hits(indexObj, { hitsPerPage: 2 }),
templates: {
suggestion: // ...
header: // ...
footer: // ...
empty: function(options) {
return '<div>My empty message</div>';
}
}
Full documentation here.

How to reference another collection on insert?

I'm trying to figure how to take an Image (a file using CollectionFS) and insert the Image's Id into my Items imageId field:
lib/collections/items.js
Items = new Mongo.Collection("items");
Items.attachSchema(new SimpleSchema({
name: {
type: String,
label: "Name",
},
userId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoform: {
type: "hidden",
label: false
},
autoValue: function () { return Meteor.userId() },
},
image: {
type: String,
optional: true,
autoform: {
label: false,
afFieldInput: {
type: "fileUpload",
collection: "Images",
label: 'Select Photo',
}
}
},
imageId: {
type: String
}
}));
lib/collections/images.js
if (Meteor.isServer) {
var imageStore = new FS.Store.S3("images", {
accessKeyId: Meteor.settings.AWSAccessKeyId,
secretAccessKey: Meteor.settings.AWSSecretAccessKey,
bucket: Meteor.settings.AWSBucket,
});
Images = new FS.Collection("Images", {
stores: [imageStore],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
}
// On the client just create a generic FS Store as don't have
// access (or want access) to S3 settings on client
if (Meteor.isClient) {
var imageStore = new FS.Store.S3("images");
Images = new FS.Collection("Images", {
stores: [imageStore],
filter: {
allow: {
contentTypes: ['image/*']
},
}
});
}
Right now my allow rules are:
server/allows.js
Items.allow({
insert: function(userId, doc){return doc && doc.userId === userId;},
update: function(userId, doc){ return doc && doc.userId === userId;},
remove: function(userId, doc) { return doc && doc.userId === userId;},
})
Images.allow({
insert: function(userId, doc) { return true; },
update: function(userId,doc) { return true; },
remove: function(userId,doc) { return true; },
download: function(userId, doc) {return true;},
});
I'm using Autoform so my form looks like this:
client/item_form.html
<template name="insertItemForm">
{{#autoForm collection="Items" id="insertItemForm" type="insert"}}
{{> afQuickField name="name" autocomplete="off"}}
{{> afQuickField name="image" id="imageFile"}}
<button type="submit">Continue</button>
{{/autoForm}}
</template>
Right now when I select browse and select an image it will be in the database and I want to take that _id it has and place it in the Item that is created afterwards, but how do I fetch that particular image? I figured this is a good way to reference an image.
UPDATE 1
Find out the ID is actually located hidden after a file is selected:
<input type="hidden" class="js-value" data-schema-key="image" value="ma633fFpKHYewCRm8">
So I'm trying to get ma633fFpKHYewCRm8 to be placed as a String in the ImageId.
UPDATE 2
Maybe one way is to use FS.File Reference?
I have solved the same problem rather simpler, after file is inserted, I just call a method that does the related collection update:
client.html
<template name="hello">
<p>upload file for first texture: <input id="myFileInput1" type="file"> </p>
</template>
lib.js
var textureStore = new FS.Store.GridFS("textures");
TextureFiles = new FS.Collection("textures", {
stores: [textureStore]
});
Textures = new Mongo.Collection("textures");
client.js
Template.hello.events({
'change #myFileInput1': function(event, template) {
uploadTextureToDb('first',event);
}
});
function uploadTextureToDb(name, event) {
FS.Utility.eachFile(event, function(file) {
TextureFiles.insert(file, function (err, fileObj) {
// Inserted new doc with ID fileObj._id, and kicked off the data upload using HTTP
console.log('inserted');
console.log(fileObj);
//after file itself is inserted, we also update Texture object with reference to this file
Meteor.call('updateTexture',name,fileObj._id);
});
});
}
server.js
Meteor.methods({
updateTexture: function(textureName, fileId) {
Textures.upsert(
{
name:textureName
},
{
$set: {
file: fileId,
updatedAt: Date.now()
}
});
}
});
as you are using autoForm and simpleSchema, it might not be so easy, but I suggest to you to forget about autoForm and simpleSchema at first and try to make it work with simple html and default collections.
After everything works, you can go back to setting up those, but beware that there might be more issues when it comes to CollectionFS, especially when it comes to styling generated by autoForm.

Kendo UI Autocomplete using 2 words(first and last name)

I am currently trying to get a kendo ui autocomplete to show results even if a user enters first and second name. At the moment i currently have my code working if the autocomplete contains first or second name. I would like the autocomplete to still show results for e.g if 'Peter S' was entered it would still show a results for 'Peter Smith' and 'Peter Samsung'. If someone could have a look at my code and maybe point me in the correct direction that would be great. I have spent alot of time on this and i think i might be taking the wrong approach.
Code below:
$("#FeeEarnerEmailSend").kendoAutoComplete({
dataSource: new kendo.data.DataSource({
serverFiltering: true,
transport: {
read: "/" + prefix + "/api/Session/GetEmployees",
parameterMap: function () {
return { id: $("#FeeEarnerEmailSend").data("kendoAutoComplete").value() };
}
}
}),
dataTextField: 'FullName',
filter: "contains",
//placeholder: "Search...",
minLength: 3,
suggest: true,
select: function (e) {
var employeeAutoComplete = e.sender;
// this var is used in the Search button click event
selectedEmployeeDataItem = employeeAutoComplete.dataItem(e.item.index());
},
change: function () {
if ($.trim($("#FeeEarnerEmailSend").val()) == "") {
selectedEmployeeDataItem = null;
}
},
dataBound: function (e) {
selectedEmployeeDataItem = e.sender.dataItem(0);
}
});
This is my Csharp code, i think the issue may lie here within my linq which needs edited to achieve this?
[HttpGet]
[Route("api/Session/GetEmployees")]
public IHttpActionResult GetEmployees(string id)
{
logger.Trace("Get Employees - {0} - based on prefix", id);
try
{
DirectoryContext context = new DirectoryContext(new Uri(ConfigurationManager.AppSettings["DirectoryServiceUrl"]));
var result = from q in context.Employees
where q.Surname.Contains(id) || q.KnownAs.Contains(id)
orderby q.Surname, q.KnownAs
select new
{
NetworkId = q.NetworkID,
FullName = String.Format("{0} {1}", q.Surname.ToUpper(), q.KnownAs),
EmailAddress = q.EmailAddress
};
logger.Info("Returning Employees - {0}", id);
return Ok(result.ToList());
}
catch (Exception ex)
{
logger.Error(ex.Message, ex);
return InternalServerError(ex);
}
}

Categories