Auto-complete suggestion within a simple_form - ruby-on-rails

I am building an application where i try to suggest "Similar topics" in rails, as the user put the title of his/her new story.
I have 2 problems:
The controller with the custom action does not work at all. it seems that the server simply retrieves the view. Without running any of the code in the action
To go around the issue of the controller, i created a service.rb with a function to retrieve the records based on the params[:title], but from here I do NOT know how to make small popup window with suggestions (and weblinks) as the user write the title of the topic.
I have done so far :
View
<div class="col-md-12">
<%= simple_form_for #message do |f| %>
<div style="font-size: xx-small; font-style: italic; color: #44B5EB">
<%= f.input :title, label: "#{t :Title}", placeholder: "#{t :Search}", id: "title" , data: {behavior: "autocomplete_message"}%>
<%= f.cktext_area :description, label: "#{t :Message_body}", :input_html => {:rows => 25} %>
<br> <br>
<%= f.submit "#{t :Create_story}", class: "btn btn-primary"%>
<% end %>
</div>
</div>
<script>
$("#title").addEventListener("turbolinks:load", function () {
$input = $("[data-behavior = 'autocomplete_message']");
var options = {
getValue: "name",
url: function (phrase) {
return "messages/search.json?title=" + phrase;
},
categories: [
{
listLocation: "qandas",
header: "<p class='Search_drop_separate'>Q&A </p>",
}
],
list: {
onChooseEvent: function(){
var url = $input.getSelectedItemData().url;
$input.val("");
Turbolinks.visit(url)
}
}
};
$input.easyAutocomplete(options)
});
</script>
Controller
class StorytController < ApplicationController
before_action :authenticate_user!
before_action :find_message, only: [:show, :edit, :update, :destroy]
respond_to :html, :js
...
def search
##qandasquestions = Qandasquestion.ransack(question_or_answer_cont: params[:q]).result(distinct: true)
respond_to do |format|
format.html {
#qandasquestions = #qandasquestions
redirect_to stories_search_path
}
format.json {
#qandasquestions = #qandasquestions.limit(5)
}
end
end
def full_name
"#{first_name} #{last_name}"
end
private
def force_json
request.format = :json
end
end
Search.jason.builder
json.qandas do
json.array!(#qandasquestions) do |qandasquestion|
json.name "#{qandasquestion.question}"
json.url qanda_path(qandasquestion.qanda_id)
end
end
routes:
get 'stories/search'
What I am looking to build is actually very similar to what we have on Stackoverflow on the principle.
Anybody did something similar and can help me please?

I don't mean to sidetrack you but if you have a couple minutes to check this out, have you seen select2? It works nice with Rails and there's also a gem to make it work nice with simple_form
https://github.com/lndl/select2_simple_form

Related

Create multiple records with one form submit

I have 3 models: User, Ingredient, and a map of which user has which ingredients - UserIngredient.
My current setup works for adding 1 ingredient at a time. What I want is to update the code so that users can enter a few ingredients and just click "submit" once rather than clicking it for each ingredient individually. I've looked into nested_resources but it seems like not the right place to use it.
what is the right way of doing this?
thank you
app/models/user.rb
class User < ApplicationRecord
...
has_many :user_ingredients, dependent: :destroy
has_many :ingredients, through: :user_ingredients
...
end
app/models/ingredient.rb
class Ingredient < ApplicationRecord
...
has_many :user_ingredients, dependent: :destroy
has_many :owners, through: :user_ingredients
...
end
app/models/user_ingredient.rb
class UserIngredient < ApplicationRecord
belongs_to :user
belongs_to :ingredient
validates :user, presence: true
validates :ingredient, presence: true
end
app/views/user_ingredients/new.html.erb
<div>
<%= turbo_frame_tag #user_ingredient do %>
<%= render "form", user_ingredient: #user_ingredient %>
<% end %>
</div>
app/views/user_ingredients/_form.html.erb
<div class="w-full mx-auto">
<%= form_for #user_ingredient do |f| %>
<div class="flex-row gap--md">
<%= f.select(
:ingredient_id,
options_from_collection_for_select(Ingredient.where(id: f.object.ingredient_id), :id, :name, :selected => f.object.ingredient_id),
{ prompt: 'Start typing to search' },
{ id: "drDwn_ingredient",
class: "w-full border border-black",
required: true,
data: {
controller: "selectIngredient",
selectIngredient_url_value: autocomplete_ingredients_path,
},
}) %>
<div class="flex-row gap--xxxs">
<label>
<input type="submit" class="add_cancel_ing gap--md" />
<%= inline_svg_tag "svg/circle-check.svg", class: "svg_add_ing" %>
</label>
<%= link_to user_ingredients_path do %>
<%= inline_svg_tag "svg/circle-xmark.svg", class: 'svg_cancel_ing' %>
<% end %>
</div>
</div>
<% end %>
</div>
app/controllers/user_ingredients_controller.rb
class UserIngredientsController < ApplicationController
before_action :authenticate_user!
before_action :set_user_ingredient, only: [:show, :destroy]
def index
#user_ingredients = current_user.user_ingredients
end
def new
#user_ingredient = UserIngredient.new
end
def create
#user_ingredient = UserIngredient.new(user_ingredient_params.merge(user: current_user))
if #user_ingredient.save
respond_to do |format|
format.html { redirect_to user_ingredients_path, notice: 'Ingredient was successfully added to your bar!' }
format.turbo_stream { flash.now[:notice] = 'Ingredient was successfully added to your bar!' }
end
else
render :new
end
end
def destroy
#user_ingredient.destroy
respond_to do |format|
format.html { redirect_to user_ingredients_path, notice: "Ingredient was removed!" }
format.turbo_stream { flash.now[:notice] = "Ingredient was removed!" }
end
end
private
...
def set_user_ingredient
#user_ingredient = current_user.user_ingredients.find(params[:id])
end
def user_ingredient_params
params.require(:user_ingredient).permit(:id, :ingredient_id)
end
end
app/javascript/controllers/selectIngredient_controller.js
import { Controller } from "#hotwired/stimulus";
import { get } from "#rails/request.js";
import TomSelect from "tom-select";
export default class extends Controller {
static values = { url: String };
multi_select_config = function () {
return {
plugins: ["remove_button", "no_active_items"],
valueField: "value",
load: (q, callback) => this.search(q, callback),
closeAfterSelect: true,
persist: false,
create: false,
};
};
async search(q, callback) {
const response = await get(this.urlValue, {
query: { q: q },
responseKind: "json",
});
if (response.ok) {
const list = await response.json;
callback(list);
} else {
console.log("Error in select_ctrl: ");
console.log(response);
callback();
}
}
connect() {
new TomSelect(this.element, this.multi_select_config());
}
}
You should use accepts_nested_attributes_for method for User
And try to create related records via User.
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Or you can try to make custom action for accepting custom form with multiple records at once. But first option will be more predictable and easier for supporting.
For views you can use cocoon gem. It's pretty old, but it still works good.
Or you can inspire by it and make your custom solution)
https://github.com/nathanvda/cocoon

Using js.erb in rails

In Rails 5 app with devise, I need to use a new.js.erb file to update select tag in my registrations view and controller. I cant seem to figure out why my new.js.erb file isn't working.
I've tried to use respond_to in controller as below,
registrations-controller.rb
def new
super
#cities = CS.get(:us,params[:state])
respond_to do |format|
format.js { render '/new.js.erb' }# layout: false }
format.html
end
end
new.html.erb
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), :remote => true) do |f| %>
<div class="signup-input-container">
<div class="field">
<%= f.text_field :firstname, autofocus: true, autocomplete: "firstname", placeholder: "First name", class: "signup-input-container--input" %>
</div>
<div class="field">
<%= f.select :state, options_for_select(CS.states(:us).map { |code, name| [name, code] }),{:prompt => "State"}, {:class => "signup-input-container--input", :id => "state-picker"} %>
</div>
<div class="field">
<%= f.select :city, options_for_select([]),{}, {:class => "signup-input-container--input", :id => "city-picker"} %>
</div>
</div>
<% end %>
new.js.erb
var city = document.getElementById("city-picker");
while (city.firstChild) city.removeChild(city.firstChild);
var placeholder = document.createElement("option");
placeholder.text = "Choose a city";
placeholder.value = "";
city.appendChild(placeholder);
<% #cities.each do |c| %>
city.options[city.options.length] = new Option('<%= c %>');
<% end %>
main.js
var state = document.getElementById("state-picker");
state.addEventListener("change", function() {
$.ajax({
url: "/states?state=" + state.value,
type: "GET"
})
})
I'm expecting this to create select tag options with my array of cities in my controller. Does anyone know how to get this to work?
To solve this you should just setup a separate controller where you can fetch the data from asynchronously and alternatively there are also several free API's which can be used for geographical lookup such as Googles Geocoding API and Geonames.
To setup a separate controller you can do it by:
# /config/routes.rb
get '/states/:state_id/cities', to: 'cities#index'
# /app/controllers/cities_controller.rb
class CitiesController < ApplicationController
# GET
def index
#cities = CS.get(:us, params[:state_id])
respond_to do |f|
f.json { render json: #cities }
end
end
end
I would skip using a .js.erb template altogether and just return JSON data which you can use directly in your JS or with one of the many existing autocomplete solutions. .js.erb only makes sense for extensive HTML templating (like for example rendering an entire form) where you want to reuse your server side templates - it greatly increases the complexity and generally makes a mess of your javascript which is not worth it just to output a list of option tags.
// If you are using jQuery you might as well setup a delegated
// handler that works with turbolinks,
$(document).on('change', '#state-picker', function(){
$.getJSON("/states/" + $(this).value() + "/cities", function(data){
// using a fragment avoids updating the DOM for every iteration.
var $frag = $('<select>');
$.each(data, function(city){
$frag.append$('<option>' + data + '</option>');
});
$('#city-picker').empty()
.append($('frag').children('option'));
});
});

Checkbox in index view live updating

I'm currently learning rails and working on what I'm sure is everyone's first rails app, a simple todo list. I need to implement a checkbox next to the items to indicate whether they are complete or not. Each item has a boolean attribute called "completed" in their model. I have found a couple checkbox questions while searching but none explain the syntax very easily in the context of the index view.
Also, I really want the checkbox to work without a submit button. I know I could accomplish something like this using AngularJS's ng-model but I don't think it would be practical to implement angular for such a small thing and I don't know how angular works with rails.
If anyone could give me a pointer in the right direction, it would be greatly appreciated. Here's my index.html.erb for reference.
<h1>
To Do List
</h1>
<table>
<tr>
<% #todo_items.each do |item| %>
<!-- Checkbox here -->
<tc style="<%= 'text-decoration: line-through' if item.completed %>">
<%= link_to item.title, item %>
</tc>
<tc>
<%= item.description %>
</tc>
<tc>
<%= link_to "Edit", edit_todo_item_path(item) %>
</tc>
<tc>
<%= link_to "Delete",item, data:{:confirm => "Are you sure you want to delete this item?"}, :method => :delete %>
</tc>
<hr/>
<% end %>
</tr>
</table>
<p>
<%= link_to "Add Item", new_todo_item_path %>
</p>
This is my way, I don't know this way is right direction or not but this works for me (also different case but same of concept).
views for checkbox
You could put an id item or something into attribute of checkbox for find an object in controller if you send data to controller for get record of object, and you could define if attribute completed of record is true or false:
<%= check_box_tag :completed_item, 1, item.completed? ? true : false, { class: 'name-of-class', data: { id: item.id} } %>
controller
You need two action call set_completed and remove_completed, and also you don't need templates for them, just use format as json:
before_action :set_item, only [:set_completed, :remove_completed, :other_action]
def set_completed
#item.set_completed!
respond_to do |format|
format.json { render :json => { :success => true } }
end
end
def remove_completed
#item.remove_completed!
respond_to do |format|
format.json { render :json => { :success => true } }
end
end
private
def set_item
#item = Item.find params[:id]
end
Model
For set_completed! and remove_completed! you could define in your model
def set_default!
self.update_attributes(:completed => true)
end
def remove_default!
self.update_attributes(:completed => false)
end
routes
resources :address do
collection do
post 'set_completed'
post 'remove_completed'
end
end
Also, you need help JavaScript for handle send request from view to controller event click of checkbox:
jQuery
$(".completed_item").click(function(){
var check = $(this).is(":checked");
if (check == true){
set_completed($(this).attr('data-id'));
} else{
remove_completed($(this).attr('data-id'));
}
});
function set_completed(data_id) {
$.ajax({
type: 'POST',
url: "/items/set_completed",
data: { id: data_id},
dataType: 'json',
success: function(response){
if(response){
}else{
alert('error');
}
}
})
}
function remove_compelted(data_id) {
$.ajax({
type: 'POST',
url: "/items/set_completed",
data: { id: data_id},
dataType: 'json',
success: function(response){
if(response){
}else{
alert('error');
}
}
})
}

Using AJAX with two select boxes in Rails 4

I have a view containing two select boxes: company and employee. Both have a blank option and when a company is selected, it populates the employees based on the selected company. This works just fine. My issue is that when I submit a form that fails validation (as expected) and I select a company once more once the 'new' view renders again in extensions#create, my 'get' AJAX call has changed from /servers/1/extensions/get_company_employees (correct) to /servers/1/get_company_employees (incorrect) and is returning 404 Not found. Why is this happening and what should I do to remedy this? All relevant code is listed below
routes.config
resources :servers do
scope module: 'servers' do
resources :extensions, shallow: true
end
end
# Ajax call
get 'servers/:id/extensions/get_company_employees', to: 'servers/extensions#get_company_employees', as: 'get_company_employees'
app/controllers/servers/extensions_controller.rb
class Servers::ExtensionsController < ApplicationController
def get_company_employees
#server = Server.find(params[:id])
#extension = #server.extensions.build
#path = [#server, #extension]
#companies = Company.all
#employees = Employee.where("company_id = ?", params[:company_id])
respond_to do |format|
format.js
end
end
def new
#server = Server.find(params[:server_id])
#extension = #server.extensions.build
#path = [#server, #extension]
#companies = Company.all
#employees = Employee.none
end
def create
#server = Server.find(params[:server_id])
#extension = #server.extensions.build(extension_params)
#extension.password = "pass"
if #extension.save
flash[:success] = "Successfully created extension"
redirect_to #extension
else
#path = [#server, #extension]
#companies = Company.all
#employees = Employee.none
flash.now[:error] = "Failed to create extension"
render "new"
end
end
private
def extension_params
params.require(:extension).permit(:value, :password, :employee_id, :server_id, :phone_id)
end
end
app/views/servers/extensions/_form.html.erb
<%= form_for(#path) do |f| %>
<p>
<%= label_tag(:company) %>
<%= select_tag "company", options_from_collection_for_select(#companies, "id", "name"), include_blank: "Select a company" %>
</p>
<p>
<%= f.label(:employee) %>
<%= f.collection_select :employee_id, #employees, :id, :full_name, include_blank: "Select an employee" %>
</p>
<p>
<%= f.submit "Submit" %>
</p>
<% end %>
app/views/servers/extensions/get_company_employees.js.coffee
$("#extension_employee_id").empty()
.append("<option>Select an employee</option>")
.append("<%= j options_from_collection_for_select(#employees, :id, :full_name) %>")
app/assets/javascripts/servers/extensions.coffee
$ ->
$(document).on 'page:load', '#company', (evt) ->
$.ajax 'get_company_employees',
type: 'GET'
dataType: 'script'
data: {
company_id: $("#company option:selected").val()
}
$(document).on 'change', '#company', (evt) ->
$.ajax 'get_company_employees',
type: 'GET'
dataType: 'script'
data: {
company_id: $("#company option:selected").val()
}
Its because you have now specified complete URL in ajax call
It should be something like this in both cases.
$.ajax "/servers/"+ id +"/extensions/get_company_employees',
type: 'GET'
dataType: 'script'
data: {
company_id: $("#company option:selected").val()
}
// store and fetch id attribute from page in any of the dom element
Ideally you should write a function for your ajax call which can be called wherever required and code redundancy can be reduced.

Filter field based on previous field selection

How can I use simple_form to filter a field, based on a previous fields value?
For instance, I have an Opportunities form, with two fields, Company and Contact.
Company Field:
<div class="form-group">
<%= f.association :company, collection: Company.all.order(:account), prompt: "", :label_method => :account, :value_method => :id %>
</div>
Contact Field:
<div class="form-group">
<%= f.association :contact, collection: Contact.all.order(:first_name), prompt: "", :label_method => lambda { |contact| "#{contact.first_name} #{contact.last_name}" }, :value_method => :id %>
</div>
Here is what I want to do: If I select a company called "Deviant" from the Company field above, I want the Contact field to only display those contacts associated with the company called "Deviant".
I am trying something like this, but can't get it to work:
<div class="form-group">
<%= f.association :contact, collection: Contact.where("company_id = ?", params[:id]), prompt: "", :label_method => lambda { |contact| "#{contact.first_name} #{contact.last_name}" }, :value_method => :id %>
</div>
I don't know how to reference the value in the Company field.
How can I do this?
Thanks.
Update
Anyone? Surely this must be possible. This is a key functionality in any form. I would hope I don't need jQuery or something.
I think the best approach is to use ajax requests to update your contacts collection dinamically whenever the company's selected value is changed.
First you'll need an action in your contacts controller:
app/controllers/contacts_controller.rb
class ContactsController < ApplicationController
def contacts_list
if params[:company_id]
#contacts = Contact.where(company_id: params[:company_id])
else
#contacts = Contact.all
end
respond_with(#contacts) do |format|
format.json { render :json => #contacts.to_json(:only => [:id, :first_name, :last_name]) }
end
end
end
Add this to your routes:
config/routes.rb
post 'contacts_list' => "contacts#contacts_list", as: :contacts_list
Then use the coffeescript code bellow to populate your contacts' collection:
app/assets/javasctipts/companies.js.coffee
$(document).ready ->
if $("#opportunity_company_id")
populate_contacts()
$("#opportunity_company_id").change ->
populate_contacts()
populate_contacts = ->
$contacts_select = $("select#opportunity_contact_id")
$contacts_select.attr "disabled", "disabled"
company_id = $("select#opportunity_company_id").val()
if company_id is ""
$contacts_select.html "<option value=\"\">(select the company first)</option>"
else
$contacts_select.html "<option value=\"\">(loading contacts...)</option>"
data = {company_id: company_id}
data[window._auth_token_name] = window._auth_token
$.ajax "/contacts_list",
type: "post"
dataType: "json"
data: data
success: (contacts) ->
_html = '<option value="">Select the contact:</option>'
_html += '<option value="'+contact.id+'">'+contact.first_name + ' ' + contact.last_name + '</option>' for contact in contacts
$contacts_select.html _html
$contacts_select.removeAttr "disabled"
error: ->
alert 'Error trying to load contacts.'
Finally, inside your html's head tag:
<% if protect_against_forgery? %>
<script>
window._auth_token_name = "<%= request_forgery_protection_token %>";
window._auth_token = "<%= form_authenticity_token %>";
</script>
<% end %>
Hope it helps...
update:
Add the following line to your ApplicationController (app/controllers/application_controller.rb):
respond_to :html, :xml, :json, :js

Resources