Searchkick + Bloodhound + Typeahead for autocomplete - ruby-on-rails

I am trying to implement a simple autocomplete feature for a single attribute.
Model:
searchkick text_start: [:name],autocomplete: ['name']
After reindexing the behaviour on the Rails console is ok.
2.2.0-p0 :002 >Doctor.search("a", autocomplete: true).map(&:name)
gives the output-
=> ["a", "aa", "aaa", "aaaa"]
After this i added the Autocomplete action to the controller and a new route to the routes.rb file.
Controller:
def autocomplete
console.log("In auto")
render json: Doctor.search(params[:query], autocomplete: false, limit: 10).map(&:name)
end
Routes:
resources :doctors do
collection do
get :autocomplete
end
end
At this point if i simply test the following URL:
http://localhost:3000/doctors/autocomplete?query="a"
Then i get the expected result in the browser:
["a", "aa", "aaa", "aaaa"]
Now adding a search box.
_header.html.erb:
<%= form_tag doctors_path, method: :get do %>
<div class="form-group">
<%= text_field_tag :query, params[:query], class: 'form-control typeahead', autocomplete: "off" %>
<%= submit_tag 'Search', class: 'btn btn-primary' %>
</div>
<% end %>
And finally the Javascript:
var ready;
ready = function() {
var numbers = new Bloodhound({
remote: {url: "/doctors/autocomplete?query=%QUERY"},
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.name); },
queryTokenizer: Bloodhound.tokenizers.whitespace
});
// initialize the bloodhound suggestion engine
var promise = numbers.initialize();
promise
.done(function() { console.log('success!'); })
.fail(function() { console.log('err!'); });
// instantiate the typeahead UI
$( '.typeahead').typeahead(null, {
displayKey: 'name',
source: numbers.ttAdapter()
});
}
$(document).ready(ready);
$(document).on('page:load', ready);
And this is the script tag used:
<script type="text/javascript" src="https://twitter.github.io/typeahead.js/releases/latest/typeahead.bundle.js"></script>
There is no response shown by the search box on typing anything,also there is no error shown on the console of Google Chrome.

You need to return hash in response of your autocomplete action:
def autocomplete
render json: Doctor.search(params[:query], autocomplete: true, limit: 10).map {|doctor| {name: doctor.name, value: doctor.id}}
end

Related

how to process form_with using GET request as XHR

I'm working on a Rails 6 app, and want to update page view based on a dropdown value selected using XHR. Dropdown must use GET method coz I am calling index action.
I am using form_with which by default uses remote: true.
I am not using local: true.
I tried onchange: "Rails.fire(this.form, 'submit')" - this does send XHR request and receives a response but does not update view.
I tried onchange: "this.form.submit();" - this does a full page reload not utilizing XHR.
Code from app/views/users/index.html.erb
<%= form_with url: station_users_path(station_id: Current.user.home_station), method: :get do |form| %>
<%= form.select :user_status, options_for_select( { "Active users" => "unlocked", "Inactive users" => "locked"}, #user_status ), {}, { :onchange => "Rails.fire(this.form, 'submit')" } %>
<% end %>
Code from app/controllers/users_controller.rb
def index
#user_status = params[:user_status] || "unlocked"
#users = #station.users.send(#user_status) || []
#user_status == "unlocked" ? seperate_managers_from_users : #managers = []
end
In onchange just write one get_station_users() function. Inside that you can use ajax calling.
In forms
<%= form_with url: station_users_path(station_id: Current.user.home_station), id: “form_id”, method: :get do |form| %>
<%= form.select :user_status, options_for_select( { "Active users" => "unlocked", "Inactive users" => "locked"}, #user_status ), {}, { :onchange => "get_station_users()" } %>
<% end %>
Add Script
function get_station_users(){
$.ajax({
type: "GET",
url: "/station_users",
data: { $('#form_id').serialize(), },
dataType: 'script'
});
}
Your response will be as JS. So you can use index.js.erb

Ajax call on a <select> in a formtastic form

I need in my app to select from a list of provider a provider, and then, via ajax, I could see below a list of categories, that belongs to a specific provider. I have a form in activeadmin:
<%= semantic_form_for [:admin, #game], builder: ActiveAdmin::FormBuilder do |f| %>
<%= f.semantic_errors :state %>
<%= f.inputs do %>
<%= f.input :categorization_id, label: 'Provider', as: :select,
collection: Provider.all.map { |provider| ["#{provider.name}", provider.id] },
input_html: { class: (:provider_select), 'data-url': category_select_path(provider: 4) } %>
<%= f.input :categorization_id, label: 'Category',input_html: { class: ('category_dropdown') }, as: :select,
collection: Category.all.map { |category| ["#{category.name}", category.id]}%>
...
<% end %>
<%= f.actions %>
<% end %>
In activeadmin controller I have:
controller do
def ajax_call
#provider = Provider.find(params[:provider])
#categories = #provider.categories
respond_to do |format|
format.json { render json: #categories }
end
end
end
JS:
$(document).on('ready page:load', function () {
$('.select.input.optional').last().addClass('hidden_row');
$('#game_categorization_id').change(function () {
var id_value = this.value;
$('.hidden_row').removeClass('hidden_row');
$.ajax({
type: 'GET',
url: '/admin/games/category_select'
// data: id_value
})
});
});
And the routes: match '/admin/games/category_select' => 'admin/games#ajax_call', via: :get, as: 'category_select'
I don't have an idea, how can I pass providers id from collection into url. Currently, I have there category_select_path(provider: 4), but actually it has to be smth. like this - category_select_path(provider: provider.id) In the browser, in a Network tab of devtools I can see my category_select, but there is an error: Couldn't find Game with 'id'=category_select. I can't figure out from where does it come. Any suggestions? Thanks.
The problem was in routes: match '/admin/games/category_select' => 'admin/games#ajax_call', via: :get, as: 'category_select'. It is reserved by a show action of Activeadmin controller. So I changed my routes to: get '/admin/select_category' => 'admin/games#get_providers_categories', as: 'select_category', and added to ajax call data: {provider: provider}, so I could fire in params id of provider:
$.ajax({
type: 'GET',
url: '/admin/select_category',
data: {
provider: provider
},
success: (function (data) {
$('#select_category').children('option').remove();
$('#select_category').prepend('<option value=""></option>');
$.each(data.categories, function () {
$('#select_category').append('<option value="' + this.id + '">' + this.name + '</option>')
})
})
})

Searchkick in another controller

I am a newbie in Ruby on rails, please help me solve my problem.
I already use gem searchkick, and in console I can get the result.
this is my code
route.rb
resources :m_customers
resource :patient, except: [:index, :show] do
collection do
get 'autocomplete'
end
end
root :to => 'home#index'
get 'patient/PatientList', to:'patient#PatientList'
get 'patient/PatientList/autocomplete', to:'patient#autocomplete'
get 'patient/PatientHistory/:kode/:histid', to:'patient#PatientHistory', as: 'patient/PatientHistory'
get 'patient/PatientSub/:id', to:'patient#PatientSub', as: 'patient/PatientSub'
get 'patient/PatientObj/:objid', to:'patient#PatientObj', as: 'patient/PatientObj'
get 'patient/PatientAsses/:assid', to:'patient#PatientAsses', as: 'patient/PatientAsses'
get 'patient/PatientPlan/:planid', to:'patient#PatientPlan', as: 'patient/PatientPlan'
m_customer.rb => model
class MCustomer < ActiveRecord::Base
self.primary_key = :Cust_ID
searchkick match: :word_start, searchable: [:Cust_Name, :Cust_ID]
MCustomer.search "tipping poi"
end
patient_controller.rb
class PatientController < ApplicationController
require 'will_paginate/array'
def PatientList
#date_now = Time.now
#patients = TRegistration.paginate(:page => params[:page], :per_page => 7).where(:Reg_status => 'N')
#patient2s = TRegistration.paginate(:page => params[:page], :per_page => 7).where(:Reg_status => 'P')
if params[:query].present?
#MCustomers = MCustomer.search(params[:query])
else
#MCustomers = []
end
end
def autocomplete
render json: MCustomer.search(params[:query], autocomplete:true, limit: 10).map do |customer| {name: customer.Cust_Name, value: customer.Cust_ID}
end
end
end
when I go to the page below, I can find the result from searchkick :
//localhost:3000/patient/PatientList/autocomplete?query=s
but when I inserted serch input, nothing could be shown
PatientList.html.erb
<div class="cust-search">
<%= form_tag patient_PatientList_path, method: :get do %>
<div class="form-group">
<%= text_field_tag :query, params[:query], class: 'form-control twitter-typeahead' %>
<%= submit_tag 'Search', class: 'btn btn-default' %>
</div>
<% end %>
</div>
java script patient.js
var ready;
ready = function() {
var engine = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.name); },
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: { url: '../patient/PatientList/autocomplete?query=%QUERY' }
});
// initialize the bloodhound suggestion engine
var promise = engine.initialize();
promise
.done(function() { console.log('success!'); })
.fail(function() { console.log('err!'); });
// instantiate the typeahead UI
$( '.twitter-typeahead').typeahead(null, {
displayKey: 'Cust_Name',
source: engine.ttAdapter()
});
}
$(document).ready(ready);
$(document).on('page:load', ready);
please help. thanks anyway.
This is what your code should look like if you want a working autocomplete (only the relevant parts)
m_customer.rb (you should really consider naming it just customer)
class MCustomer < ActiveRecord::Base
searchkick autocomplete: ['Cust_Name']
end
you should use the autocomplete option and provide the column you want to autocomplete, e.g: cust_name
patient_controller.rb ( you should really consider naming it customer_controller )
class PatientController < ApplicationController
def autocomplete
render json: MCustomer.search(params[:query], autocomplete:true, limit: 10).map do |customer| { name: customer.Cust_Name }
end
end
end
here you are formatting your json response
patient.js (read this carefully and see the modifications)
var engine = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.name); },
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: { url: '/patient/autocomplete?query=%QUERY' }
});
var promise = engine.initialize();
$( '.twitter-typeahead').typeahead(null, {
name: "customer",
displayKey: "name",
limit: 20,
source: engine.ttAdapter()
});
}
I think this is where you made your mistake, you should provide the right key so the method will read your json as it should and retrieve the names you want.
you should check out this wonderful guide, you can read and just copy-paste with the modifications that suit your code:
https://rubyplus.com/articles/4031-Autocomplete-using-Typeahead-and-Searchkick-in-Rails-5
hope it helps anyone

Rails collection select when collection is way big

I have a form partial inside which I select associated users through a multiple: true collection select:
= f.collection_select(:user_ids, User.all, :id, :email, {selected: #limit_group.user_ids, include_blank: true}, {multiple: true, "data-placeholder" => "Add users to group"})
But how can I do this more efficiently to avoid big load times when the database has like thousands of users?
You'll be better using something called AutoComplete / LiveSearch with a text box (like Pardeep Saini mentioned).
We've done this before:
You could achieve this relatively simply:
= f.text_field :user_ids, placeholder: "Search for users"
You'd then have to use javascript:
#app/assets/javascripts/application.js
$(document).on("keyup", "input[type=text]#user_ids", function(){
$.getJSON("users/search", {name: $(this).val()}).done(function(json){
var users = [];
$.each(json.users, function(user) {
users.push("" + user.name + "");
});
$(".search").html(users).show();
});
});
$(document).on("click", ".search a", function(e) {
e.preventDefault();
// add hidden field with user name to form
});
You'd have to back it up with the relevant controller action:
#config/routes.rb
resources :users do
get :search, on: :collection
end
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def search
#users = User.where("name LIKE ?", "%" + params[:name] + "%")
respond_to do |format|
format.json (render json: #users.to_json)
end
end
end
The above code should be refactored.
--
To get this working with multiple values would be a little bit more involved. It could be done, but you'd have to do it like the tags setup in StackOverflow...
The way they do that is to basically use a similar principle to the above (each tag will be a returned piece of data from the search).
Here's the actual code we used in the cosmetics example above:
#app/assets/javascripts/extra/jquery.livesearch.js
(function($) {
$.searchbox = {}
$.extend(true, $.searchbox, {
settings: {
url: 'search',
param: 'search',
dom_id: '#livesearch',
minChars: 2,
loading_css: '#livesearch_loading',
del_id: '#livesearch_del'
},
loading: function() {
$($.searchbox.settings.loading_css).show()
},
idle: function() {
$($.searchbox.settings.loading_css).hide()
},
start: function() {
$.searchbox.loading()
$(document).trigger('before.searchbox')
},
stop: function() {
$.searchbox.idle()
$(document).trigger('after.searchbox')
},
kill: function() {
$($.searchbox.settings.dom_id).fadeOut(50)
$($.searchbox.settings.dom_id).html('')
$($.searchbox.settings.del_id).fadeOut(100)
},
reset: function() {
$($.searchbox.settings.dom_id).html('')
$($.searchbox.settings.dom_id).fadeOut(50)
$('#SearchSearch').val('')
$($.searchbox.settings.del_id).fadeOut(100)
},
process: function(terms) {
if(/\S/.test(terms)) {
$.ajax({
type: 'GET',
url: $.searchbox.settings.url,
data: {search: terms.trim()},
complete: function(data) {
$($.searchbox.settings.del_id).fadeIn(50)
$($.searchbox.settings.dom_id).html(data.responseText)
if (!$($.searchbox.settings.dom_id).is(':empty')) {
$($.searchbox.settings.dom_id).fadeIn(100)
}
$.searchbox.stop();
}
});
return false;
}else{
$.searchbox.kill();
}
}
});
$.fn.searchbox = function(config) {
var settings = $.extend(true, $.searchbox.settings, config || {})
$(document).trigger('init.searchbox')
$.searchbox.idle()
return this.each(function() {
var $input = $(this)
$input
.keyup(function() {
if ($input.val() != this.previousValue) {
if(/\S/.test($input.val().trim()) && $input.val().trim().length > $.searchbox.settings.minChars){
$.searchbox.start()
$.searchbox.process($input.val())
}else{
$.searchbox.kill()
}
this.previousValue = $input.val()
}
})
})
}
})(jQuery);
... and ...
#app/assets/javascripts/application.js
$(document).ready( function() {
var base_url = window.location.protocol + "//" + window.location.host;
$('#SearchSearch').searchbox({
url: base_url + '/search/',
param: 'search',
dom_id: '#livesearch',
loading_css: '#livesearch_loading'
})
});
$(document).on('click', '#livesearch_del', function() { $.searchbox.reset(); })
$(document).on('submit', '#SearchForm', function() { $.searchbox.kill(); });
$(document).on('click', '.livesearch_results tr', function() { window.location = $('a:first', this).attr('href'); });
The routes & controller:
#config/routes.rb
match 'search(/:search)', :to => 'products#search', :as => :search, via: [:get, :post]
#app/models/product.rb
class Product < ActiveRecord::Base
def self.search(search)
where("name LIKE ? OR description LIKE ?", "%#{search}%", "%#{search}%").take(5)
end
end
#app/controllers/product_controller.rb
class ProductsController < ApplicationController
def search
#products = Product.search params[:search]
respond_to do |format|
format.js { render :partial => "elements/livesearch", :locals => {:search => #products, :query => params[:search]} }
format.html {
render :index
}
end
end
end
The views:
#app/views/elements/_livesearch.html.erb
<div class="livesearch_container">
<table class="livesearch_results">
<% unless search.blank? %>
<% search.each_with_index do |item,i| %>
<% pos ||= '' %>
<% if (i == 0) then pos = 'first' end %>
<% if (i == search.size - 1) then pos += ' last' end %>
<tr data-link="<%= "/#{item.slug}" %>" class="<%= "#{pos}" %>">
<td class="image">
<% model = item.images.first || item.images.build %>
<%= image_tag(model.image.url(:thumb), :title => item.name, data: {"placement" => "left"}, :height => "85") %><br/>
</td>
<td class="information">
<%= link_to image_tag(item.brand.images.first.image.url(:thumb), :width => "55", :title => "View #{item.brand.name}"), "/#{item.brand.slug}", :class => "brand" if defined?(item.brand.images.first) %>
<div class="name"><%= link_to item.name, "/#{item.slug}" %></div>
</td>
<td class="price">
<%= number_to_currency(item.price, unit: "£") %>
</td>
</tr>
<% end %>
<tr class="results"><td colspan="3"><%= link_to "See all #{search.size} results here »", search_path(query) %></td></tr>
<% else %>
<tr class="results"><td colspan="3"><%= link_to 'No results found', search_path(query) %></td></tr>
<% end %>
</table>
</div>
I also made a gist here: https://gist.github.com/richpeck/2310ff3ab1ffcd6a9138

Typeahead not sending requests to server

I followed this tutorial
https://shellycloud.com/blog/2013/10/adding-search-and-autocomplete-to-a-rails-app-with-elasticsearch
and when i type in the text field, i don't see any ajax requests being sent to the server.
(I added the typeahead.js to my assets)
What am i missing?
Here is my code
view:
.col-xs-12.col-sm-12.col-md-12.col-lg-6.col-lg-offset-3
%form#search_form{action: "/drugs/search", method: "get", text: "search", role:"search"}
.input-group
%input.form-control{type: "text", placeholder: "Search", name: "query", id: "drug_search", autocomplete: "off"}
%span.input-group-btn
%button.btn.btn-default{type: "submit"} Search
.panel-group#drug-list
= render #drugs
= will_paginate #drugs, renderer: BootstrapPagination::Rails
controller:
def autocomplete
render json: Drug.search(params[:query], autocomplete: true, limit: 6).map(&:generic_name)
end
js
$ ->
$("#drug_search").typeahead
name: "drug"
remote: "/drugs/autocomplete?query=%QUERY"
There was a slight API change when v0.10.0 was released. Try this:
var drugSource = new Bloodhound({
datumTokenizer: function(d) { return Bloodhound.tokenizers.whitespace(d.num); },
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: '/drugs/autocomplete?query=%QUERY'
});
drugSource.initialize();
$('#drug_search').typeahead(null, {
name: 'drug',
source: drugSource.ttAdapter()
});
For more details about what changed with the v0.10.0 release, you can refer to the migration guide.

Resources