ROR: Like button, no refresh - javascript

I want to make my like button change on click without refresh.
Model: like.rb
class Like < ApplicationRecord
belongs_to :tweet
belongs_to :user
end
Controller
class LikesController < ApplicationController
before_action :find_tweet
before_action :find_like, only: [:destroy]
def create
if already_liked?
flash[:notice] = "You can't like more than once"
else
#tweet.likes.create(user_id: current_user.id)
end
redirect_to tweets_path
end
def destroy
if !already_liked?
flash[:notice] = 'Cannot unlike'
else
#like.destroy
end
redirect_to tweets_path
end
private
def find_tweet
#tweet = Tweet.find(params[:tweet_id])
end
def find_like
#like = #tweet.likes.find(params[:id])
end
def already_liked?
Like.where(user_id: current_user.id, tweet_id:
params[:tweet_id]).exists?
end
end
In my view...
tweets/_tweets_index.html.erb
<% pre_like = tweet.likes.find { |like| like.user_id == current_user.id} %>
<% if pre_like %>
<%= link_to '<i class="far fa-thumbs-up"></i>'.html_safe, tweet_like_path(tweet, pre_like), data: { remote: true, type: :json, method: :delete }, :class => "like-btn btn btn-primary btn-sm rounded-pill" %>
<% else %>
<%= link_to '<i class="fas fa-thumbs-up"></i>'.html_safe, tweet_likes_path(tweet), data: { remote: true, type: :json, method: :post }, :class => "like-btn btn btn-primary btn-sm rounded-pill" %>
<% end %>
<%= tweet.likes.count %>
Routes
resources :tweets do
resources :comments
resources :likes
member do
post :retweet
end
end
How can this be done? My like button works fine but i'm trying to get a no refresh on the button click. I have tried javascript and changed the controller accordingly but it says I don't have a destroy method? I'm at my wits end so have just gone back to the beginning like above.
How can I get this to work?
ty

Check out ActionCable. It supports Websockets which simply means that two endpoints communicate through a channel. You first create a connection between the server and the client. Then when the like button is hit the corresponding action is called. However, you have to alter the code in the action so that it doesn't render the page again, but broadcast a message to the channel mentioned previously. The client receives this message and by using Javascript it updates the counter of the button.
You should first generate a channel by using:
rails generate channel likes
Some files will be generated. Find the file
app/channels/likes_channel.rb
uncomment the line in the subscribed method and change the name of the channel to what you want.
Then, in your controller action add the line:
ActionCable.server.broadcast("name_of_your_channel", message)
This way you broadcast the message to the channel.
Finally, pick the message in the file:
app/javascript/channels/likes_channel.js
at the function received(data). The variable data hold the messages that were transmitted to the channel you subscribed.
This is just a very simple example and you might need to do more work. Please, read the documentation or some online tutorial.

Related

Rails AJAX request to controller method that then calls javascript function

I have looked at dozens of stack overflow posts and haven't found a solution that works which is why I'm reaching out for an otherwise well documented use case.
I have a button that that should do this.
When clicked, call a custom controller method that updates the model and does other things
Call a javascript function to update the page without reloading it (ajax)
Right now, the only way I am able to call a custom controller method is via this way which feels really hacky. I have stripped this down to as simple as possible.
routes.rb
match 'admin/:id/toggleAdmin' => 'admin#toggleAdmin', via: [:patch, :put], as: :toggleAdmin
list.html.erb
<td><%= link_to "Toggle Admin", toggleAdmin_path(id: user.id), method: :patch %></td>
admin_controller.rb
class AdminController < ApplicationController
def toggleAdmin
idToToggle = User.find(params[:id]).id
if idToToggle == current_user.id
redirect_to admin_list_path, danger: "You tried to make yourself a normal user! Don't do that!"
else
User.find(params[:id]).updateToAdmin()
redirect_to admin_list_path, info: "The user with an ID of #{idToToggle} has had their admin attribute toggled!"
end
end
end
What I would like to do instead of reloading the page when an admin is toggled is to just use some javascript to rewrite that part of the dom.
What is a better way to go about this?
Here are just a few of the various resources I have already tried.
How to call a controller's method from a view?
Can we call a Controller's method from a view (as we call from helper ideally)?
Call controller method with rails-Ajax?
https://www.reddit.com/r/rails/comments/7iyp6g/want_a_button_to_call_a_method_apparently_this_is/
How do I call a JavaScript function from an html.erb
Thanks for the help.
--- Edit for more information.
Using a clean rails app I now have the ability to call a controller method more cleanly but I am not getting an ajax request to go through that updates the page to show that the action was completed. (I am expecting a boolean value change and a flash). Here is the following relevant code:
users.js
$("#edit-form").html("<%= j render partial: 'form', locals: { user: #user } %>")
_form.html.erb
<%= form_for user do |form| %>
<%= user.admin %>
<% end %>
<%= link_to "Toggle Admin", toggle_admin_user_path(user), method: :put, remote: true %>
edit.html.erb
<h1>Editing User</h1>
<div id="edit-form">
<%= render partial: 'form', locals: { user: #user } %>
</div>
<%= link_to 'Show', #user %> |
<%= link_to 'Back', users_path %>
users_controller.rb Note that I have only included the toggle_admin and a couple other methods as the rest are just scaffolding.
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy, :toggle_admin]
def toggle_admin
if 1 == 1
logger.info "This is from info"
u = User.find(params[:id])
u.admin = !(u.admin)
u.save
respond_to do |format|
format.js { flash[:info] = "The user with an ID of #{#user.id} has had their admin attribute toggled!" }
end
else
redirect_to admin_list_path, danger: "You tried to make yourself a normal user! Don't do that!"
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
#user = User.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.fetch(:user, {})
end
end
The routing
First thing, if you need a nested custom route inside a resource you can define it as a member of this resource:
resources :users do
put 'toggle_admin', on: :member
end
which returns:
toggle_admin PUT /users/:id/toggle(.:format) users#toggle
Second, your method names should be in snake_case : def toggle_admin
See "Instance Methods" section
The controller
If you just want to update a user to admin (guessing by this method here: User.find(params[:id]).updateToAdmin()), you can define the custom toggle_adminmethod inside the UsersController.
Your custom method for triggering ajax request should respond to the js format and point to the corresponding js view (toggle_admin.js.erb)
As a member of the resource User, you can add it to the before_action callback to return the right user when you call the method.
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy, :toggle_admin]
#...
def toggle_admin
if #some condition
# do some logic here
respond_to do |format|
format.js { flash[:info] = "The user with an ID of #{#user.id} has had their admin attribute toggled!" }
end
else
redirect_to admin_list_path, danger: "You tried to make yourself a normal user! Don't do that!"
end
end
end
The view
In rails views, when you want to use ajax to update a view without reload, you need to use partial. It's this partial which will be refreshed inside the .js.erb file on a specific DOM element.
Let's make an example for the user#edit view.
First you need to render the view inside a partial form for example, and wrap it into a div with specific id:
edit.html.erb
<div id="edit-form">
<%= render partial: 'form', locals: { user: #user } %>
</div>
The partial:
_form.html.erb
<%= form_for user do |form| %>
# the form
<% end %>
<%= link_to "Toggle Admin", toggle_admin_user_path(user), method: :put, remote: true %>
# don't forget the remote: true for the ajax call
Finally the .js.erb for the custom action (assuming you have jquery):
toggle_admin.js.erb
$("#edit-form").html("<%= j render partial: 'form', locals: { user: #user } %>")
And voilĂ  ! when click the link, you refresh the edit view with new informations from the controller without reloading the view.
Of course, this is an example for a classic resource's edit view but you can adapt it for any case. Just keep the good naming between routes, controllers, controller methods and views (AdminController, list view etc...)
Edit
To handle the flash messages, you need to add inside your layout > application.html.erb :
<body>
<% flash.each do |key, value| %>
<%= content_tag :div, value, class: "classname" %>
<% end %>
<%= yield %>
</body>

Convert working Ruby on Rails form to AJAX

I have a working Ruby on Rails form that currently posts my results and redirects the user to another view and posts the results. The flow is:
views/tools/index.html.erb -> views/tools/ping.html.erb
Since I have it working now, I'd like to convert it to AJAX and keep the user on the views/tools/index.html.erb view, getting rid of the redirect to enhance the user experience. However, I'm unsure of how to proceed based on the way that my Tools controller is currently setup, and my incredibly lacking knowledge of AJAX.
So, here's what I currently have:
views/tools/index.html.erb (added 'remote: true' to form)
<h1> Tools </h1>
<h3> Ping </h3>
<%= form_tag ping_tool_path(1), method: "post", remote: true do %>
<%= text_field_tag :ip, params[:ip] %>
<%= submit_tag "Ping", name: nil %>
<% end %>
<!-- this is where I'd like to put the results via AJAX -->
<div id="output"></div>
controllers/tools_controller.rb
class ToolsController < ApplicationController
def index
end
def ping
ping_host(params[:ip])
save_host(params[:ip])
# Adds based on recommendations
respond_to do |format|
format.html { redirect_to tools_path }
format.js
end
end
protected
def ping_host(host)
f = IO.popen("ping -c 3 #{host}")
#output = f.readlines
tool_type = "ping"
tool = Tool.find_by(tool_type: tool_type)
tool.increment(:tool_hit_count, by = 1)
tool.save
#results = "<pre>#{#output.join}</pre>".html_safe
end
def save_host(host)
host = Host.find_or_create_by(host_ip: host)
host.increment(:host_hitcount, by = 1)
host.save
end
end
views/tools/ping.html.erb
<%= #results %>
views/tools/ping.js.erb (New file based on suggestion)
$("#output").html("<%= #results %>");
routes.rb
Rails.application.routes.draw do
root 'tools#index'
resources :tools do
member do
post 'ping'
end
end
end
This is what I see on the Network tab in Google Chrome after submitting the form:
So, what I know at this point is that I'll need to add remote: true to my form in views/tools/index.html.erb, and this is where I get lost.
It seems that I have an issue ATM with the fact that I've abstracted the form to use my ping method in the Tools controller, whereas all of the tutorials (and railscasts) I've gone through are doing AJAX on CRUD methods and a given model, not something like what I've build here so far. Please help me understand AJAX!
You're on the right track, now you need to modify the def ping action with a respond_to block.
def ping
ping_host(params[:ip])
save_host(params[:ip])
respond_to do |format|
format.html { redirect_to tools_path } ## if you still want to have html
format.js
end
end
and create a file called view/tools/ping.js.erb where you have javascript that will be run and returned asynchronously
$("#output").html("<%= j #results %>");
The <%= %> block will be evaluated first and replaced with the output of that ruby code. Then this will be inserted into the #output div.

Rails 4: Turbolinks redirect with flash

Lets say I have a standard controller create action:
def create
#object = Object.new(object_params)
if #object.save
redirect_to #object, flash: {success: "Created object successfully."}
else
render :new
end
end
Now if I change the redirect to use turbolinks:
redirect_to #object, turbolinks: true, flash: {success: "Created object successfully."}
the server returns a text/javascript response, and my browser is displaying it as a white page with just this line of text on it:
Turbolinks.visit('http:/localhost:3000/objects/1');
How do I make it so my browser actually executes that code instead of displaying it as text?
Also, how can I get the flash success message to appear?
This is not a direct answer to your question. But I would suggest using unobtrusive JS for this. You'd want to add remote: true in your form first.
def create
#object = Object.new(object_params)
if #object.save
flash.now.success = 'Created object successfully.'
else
flash.now.alert = #object.errors.full_messages.to_sentence
end
render :js
end
Create a file create.js.erb containing only the following:
$('#flash').replaceWith("<%= j render partial: 'layouts/flash' %>");
This is what render :js will be rendered back to the browser, thereby executing a script that will replace your flash container (#flash or whatever id you used) with the new flash messages
Furthermore make sure that you have a partial view file app/layouts/_flash.html.erb which contains your flash HTML code.
Update:
If not using Unobtrusive JS. You can render the partial file like the following:
def create
#object = Object.new(object_params)
if #object.save
flash.now.success = 'Created object successfully.'
else
flash.now.alert = #object.errors.full_messages.to_sentence
end
render partial: 'layouts/flash'
end
Then in your HTML, you embed something like the following:
$('#myForm').ajaxForm({
url : '<%= url_for objects_path %>',
type: 'post',
dataType : 'json',
success : function (response) {
$('#flash').replaceWith(response);
}
});
P.S. You might also be interested with Wiselinks which actually does this already (partial view loading; you specify the target container to be replaced). Because currently turbolinks do not have support yet with partial loading, it always loads the whole body.
This is how I ended up solving this using Turbolinks and materialize toasts for a great flash UX:
In my controller:
format.js {
flash[:toast] = 'Created record successfully.'
render :create
}
In create.js.erb:
Turbolinks.visit("<%= success_path %>");
In my _flash.html.erb partial:
<% flash.each do |type, message| %>
<% if type == "toast" %>
<script id="toast">
$(function() {
Materialize.toast('<%= message %>', 3000);
});
</script>
<% elsif type == "success" %>
...
<% elsif type == "alert" %>
...
<% end %>
<% end %>

Pass select value to controller without refreshing the page (AJAX, RAILS, JS)

I'm a newbie using AJAX and now I'm trying to work on a group of select boxes so that when the user selects a value from the first, the second updates its values. I'm trying to use AJAX, JQuery and ROR, but so far, I can't fetch the value from the first box and pass it to the controller in order to filter the results of the second.
This is my code for index.html.erb
<%= form_tag(posts_path, remote: true, :method => 'get', class: "forma") do %>
<%= select_tag("box", options_from_collection_for_select(#posts, :content, :content), :prompt => "Select") %>
<p>
<%= select_tag("nbox", options_from_collection_for_select(#listposts, :content, :id), :prompt => "Select") %>
<p>
<%= submit_tag "Show", class: "btn btn-info" %>
<% end %>
This is my index.js.erb
$(document).ready(function() {
$(".btn").on("click", function() {
$('.table').fadeToggle();
})
$('#box').change(function(){
$.ajax({
type: 'POST',
url: '/jess/rails/dummy/app/controllers/posts_controller',
data: {'filter' : $(this).val() },
success: function(data){
}
})
$('#nbox').fadeIn();
$('.btn').fadeIn();
})
})
This is my controller
class PostsController < ApplicationController
respond_to :html, :js
def new
#post = Post.new
end
def create
#posts = Post.all
#post = Post.new(post_params)
if #post.save
flash.now[:notice] = "YEAH"
respond_to do |format|
format.html {redirect_to #post.content}
format.js
end
else
render "/"
end
end
def show
#post = Post.find(params[:id])
end
def index
#posts = Post.all
#listposts = Post.where('content LIKE ?', params[:box])
end
def next
#listposts = Post.where('content LIKE ?', params[:box])
end
private
def post_params
params.fetch(:post, {}).permit(:content)
end
end
I think I'm doing it wrong with the AJAX part in the js.erb but I've changed it so many times I don't know anymore. Your help would be so very much appreciated!!
It looks like there are a couple of small problems.
First, it looks like the URL you are using may be wrong. The URL for the AJAX request would be the same format as the URL if you were to view that page in your browser.
With what you provided I can't tell exactly what it should be, but I can give you an example. Assuming you access your index page by just "http://example.com/index" (or something similar), you would also call the function you want as something based on it's route. If the route was "update", then it would be something like "http://example.com/update".
Basically, don't use the folder path but use the web path instead.
Second, you don't do anything your success callback in your AJAX request. With AJAX, you send and receive info from the server, then you have to use JavaScript to update whatever you need updated manually. So, you'll need to have your update action return some information which you can then use to change the state of your second set of boxes.

Rails Ajax search not doing anything -- 404 error

Trying to figure out Ajax search in a Rails 3 app, following this post, which itself borrows from a Railscast. Currently, when I submit, nothing happens, and if I go into the Chrome console, I see this error:
GET http://localhost:3000/search?utf8=%E2%9C%93&search=&_=1361491523102 404 (Not Found)
posts.js line 5 seems to be culpable, but that just seems to be the jQuery call, so I'm not sure what I'm doing wrong.
My relevant code:
posts.js.coffee
jQuery ->
# Ajax search on submit
$('.search_form').submit( ->
$.get(this.action, $(this).serialize(), null, 'script')
false
)
posts_controller.rb
def show
#search = Post.search(params[:search])
respond_to do |format|
format.html # show.html.erb
format.json { redirect_to #post }
end
end
def search
#search = Post.search(params[:search])
render json: { results: #search }
end
post.rb
def self.search(search)
if search
where('name LIKE ?', "%#{search}%")
else
scoped
end
end
show.html.erb
<%= form_tag search_path, method: "get", class: "search_form form-inline",
style: "display:inline-block;" do %>
<%= text_field_tag :search, params[:search], placeholder: "Search:", id: "search_field" %>
<%= submit_tag("Search", name: nil, class: "btn search", remote: true) %>
<% end %>
# Testing for #search stops it from pulling a "no method '.first' for nil NilClass" error.
<div id="list"><%= render partial: 'list', locals: { search: #search } if #search%></div>
list.html.erb
<ul>
<% #search.each do |p| %>
<li>(<%= p.created_at.strftime("%b %d, %Y") %>)</span></li>
<% end %>
</ul>
Any idea what's going wrong here?
EDIT -- Adding relevant lines of routes file:
resources :posts
# ...
match '/search', to: 'posts#search'
I already had that '/search' route there. Is that right -- in which case, is the problem somewhere else? Does the rest of the code look goodish?
EDIT -- added search method to PostsController, per Damien's rec. New things of interest are a) the way I call the partial, the new search action, and that fact that then I tried to replace the contents of the show method in Post.rb with "Post.all", it still didn't show anything. No errors thrown, though.
"No search action! Didn't realize I needed one. What should be in it?
When I tried an earlier version of this without AJAX, I had a search
action that was basically a replica of show (+ #search etc), with all
the same variables, otherwise it wouldn't work, because it rendered
the show page. Is that the right idea? It seemed very unDRY"
Using AJAX, there is no need to render the entire show page. Just return the filtered results in desired format:
def search
#search = Post.search(params[:search])
render json: { results: #search }
end
Add format based returns if necessary.

Categories

Resources