Webpage Look
So I have a Todo List and contains Todo Items. And for each incomplete task(item), there is a button next to it called "Mark Item as Done". (See button_to method) Whenever I click on that button, it should go into that item and mark it as done. However, I'm struggling to implement AJAX into this project and I need help. I'm new to rails and ajax, so I have no clue what I'm doing... The alert message in the update.js.erb is to test if it's reaching there.
Am I supposed to create a partial file called _todoitems.html.erb or _todolists.html.erb? And what else am I missing and what else do I need to do?
Here are the relevant files of what I've done so far...
routes.rb
todolist_todoitems GET /todolists/:todolist_id/todoitems(.:format) todoitems#index
POST /todolists/:todolist_id/todoitems(.:format) todoitems#create
new_todolist_todoitem GET /todolists/:todolist_id/todoitems/new(.:format) todoitems#new
edit_todolist_todoitem GET /todolists/:todolist_id/todoitems/:id/edit(.:format) todoitems#edit
todolist_todoitem GET /todolists/:todolist_id/todoitems/:id(.:format) todoitems#show
PATCH /todolists/:todolist_id/todoitems/:id(.:format) todoitems#update
PUT /todolists/:todolist_id/todoitems/:id(.:format) todoitems#update
DELETE /todolists/:todolist_id/todoitems/:id(.:format) todoitems#destroy
todolists GET /todolists(.:format) todolists#index
POST /todolists(.:format) todolists#create
new_todolist GET /todolists/new(.:format) todolists#new
edit_todolist GET /todolists/:id/edit(.:format) todolists#edit
todolist GET /todolists/:id(.:format) todolists#show
PATCH /todolists/:id(.:format) todolists#update
PUT /todolists/:id(.:format) todolists#update
DELETE /todolists/:id(.:format) todolists#destroy
root GET / todolists#index
todolists/_form.html.erb
<%= form_for(#todolist, remote: true) do |f| %>
todolists_controller.rb
# PATCH/PUT /todolists/1
# PATCH/PUT /todolists/1.json
def update
respond_to do |format|
if #todolist.update(todolist_params)
format.html { redirect_to #todolist, notice: 'Todolist was successfully updated.' }
format.json { render :show, status: :ok, location: #todolist }
format.js
else
format.html { render :edit }
format.json { render json: #todolist.errors, status: :unprocessable_entity }
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_todolist
#todolist = current_user.todolists.find(params[:id])
end
todolists/show.html.erb
<!-- paginate_items is basically the current user's items -->
<% #paginate_items.each do |item| %>
<div class="list">
<% if item.due_date > Date.today %>
<% if item.done? %>
<a class="complete">
<%= item.due_date %>
</a>
<a class="linkResults">
<%= link_to "#{item.task_title}", [#todolist, item], style: "font-weight: bold;" %><br/> <br/>
</a>
<% else %>
<form class="oneLine">
<a class="notDue">
<%= item.due_date %>
</a>
<a class="linkResults">
<%= link_to "#{item.task_title}", [#todolist, item], style: "font-weight: bold;" %>
<%= button_to "Mark Item as Done", edit_todolist_todoitem_path(#todolist, item), remote: true, id: "done_item_true" %><br/> <br/>
</a>
</form>
<% end %>
todolists/update.js.erb
alert("TEST TEST TEST");
Add a custom route for the ajax request in routes.rb. If you have resources for items make it something like:
resources :items do
collection do
post 'check_it_off'
end
end
Add a corollary controller action in your items controller and update the state when that action is called:
def check_it_off
item_id = params[:item_id]
item = Item.find(item_id)
# whatever you are updating, just an example
item.status = done
item.save
render json: {data: "Success"}
end
Attach an event to marking the item off and call your ajax request:
$(document).on('click', '.some-class', function(e){
e.preventDefault();
var itemId = $('#item_id')
$.ajax({
type: 'POST',
url: '/items/check_it_off'
data: itemId
}).done(function(response){
console.log(response)
})
})
In your view, give every item an id that relates to their actual id by saying something like: id="<%= item.id %>"
That should do it. That's basically a full ajax post request.
Add some javascript to handle the response on the form and the update the dom on the success callback.
$(function(){
$("#form_id").on("ajax:success", function(e, data, status, xhr) {
// update the dom here, e.g.
$("#stuff").append('<img src="check.png"/>');
}
).on("ajax:error", function(e, xhr, status, error) {
console.log(e, xhr, status, error);
}
)
});
Related
I'm trying to build an app that allows users to share quotes by artists about other artists. For instance, a quote by Bob Dylan about John Lennon. As such, my Artist model is set up in a way that allows an artist to be both the Speaker and Topic on a Quote, and each Quote belongs_to each Artist as the Speaker or Topic.
I'm having trouble getting a Rails error message to display inside a Bootstrap modal when using Selectize to trigger the modal. I got the modal working by following this demo.
The modal is used to create a new Artist from the quotes/new form, but I can't get the error messages for the Artist model to display in the Bootstrap modal or on the quotes/new page. When I try to create something that triggers an error message (such as validates_uniqueness) in the modal, it just closes the modal and doesn't display the error message. Everything else is working as expected.
What am I missing to connect the Ajax request to the view?
Here's the relevant section of my form:
<%= f.label :speaker, 'Who said it?' %>
<%= f.collection_select :speaker_id, #speakers, :id, :name,
{prompt: 'Select an artist'}, {class: 'form-control selectize-speaker'} %>
Full source for quotes/form.html.erb
Here's the relevant code in my controller:
class ArtistsController < ApplicationController
def create
#artist = current_user.artists.build(artist_params)
authorize #artist
respond_to do |format|
if #artist.save
if request.referer.include?("artists")
flash[:success] = "Artist was successfully created."
format.html { redirect_to #artist }
else
format.json { render json: #artist }
end
else
format.html { render :new }
format.json { render json: #artist.errors.full_messages }
end
end
end
end
Full source for artists_controller.rb
Relevant javascript code:
$(document).on('turbolinks:load', function() {
var selectizeCallback = null;
// Selectize Speaker
$('.speaker-modal').on('hide.bs.modal', function(e) {
if (selectizeCallback != null) {
selectizeCallback();
selecitzeCallback = null;
}
$('#new_speaker').trigger('reset');
});
$('#new_speaker').on('submit', function(e) {
e.preventDefault();
$.ajax({
method: 'POST',
url: $(this).attr('action'),
data: $(this).serialize(),
success: function(response) {
selectizeCallback({value: response.id, text: response.name});
selectizeCallback = null;
$('.speaker-modal').modal('toggle');
}
});
});
$('.selectize-speaker').selectize({
create: function(input, callback) {
selectizeCallback = callback;
$('.speaker-modal').modal();
$('#speaker_name').val(input);
}
}); // end selectize speaker
}); // end document on
Full source for quotes.js.
And my error message partial, shared/_error_messages.html.erb:
<% if object.errors.any? %>
<div id='error_explanation'>
<div class='alert alert-danger'>
<button type='button' class='close' data-dismiss='alert'>×</button>
<p><strong>The form contains
<%= pluralize(object.errors.count, 'error') %>.</strong></p>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
</div>
<% end %>
Additional source files:
models/quote.rb
models/artist.rb
controllers/quotes_controller.rb
Both a successful and unsuccessful save are returning a 200 response, which means that your success callback will be called:
success: function(response) {
selectizeCallback({value: response.id, text: response.name});
selectizeCallback = null;
$('.speaker-modal').modal('toggle');
}
This always toggles the modal, therefore closing it.
If you ensure that the response is a 4xx on validation error, then you can define an error callback which populates your errors list and does not close the modal.
So instead of:
format.json { render json: #artist.errors.full_messages }
Use something like:
format.json { render json: #artist.errors.full_messages, status: :bad_request }
Then, pass an error callback to your AJAX call:
error: function(response) {
// somehow populate your errors list
// display the errors list
}
This won't work right now, though, because the errors container won't exist: you only render it under this condition:
object.errors.any?
Which, on initial load, will always evaluate false. What you can instead do is always render the errors container, default it to some hidden class if there aren't any errors, and in your error callback, remove the hidden class after it's populated.
I have implemented follow/unfollow functionality and would like to add AJAX call to it, but I am stuck.
My partial _follow_button.html.erb for follow/unfollow which is rendered on Users->index, looks like:
<% if current_user.id != user.id %>
<% if !current_user.following?(user) %>
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
<div><%= hidden_field_tag :followed_id, user.id %></div>
<span class="follow"><%= f.submit "Follow User", class: "btn btn-primary btn-sm" %></span>
<% end %>
<% else %>
<%= form_for(current_user.active_relationships.find_by(followed_id: user.id),
html: { method: :delete }, remote: true) do |f| %>
<span class="unfollow"><%= f.submit "Unfollow User", class: "btn btn-secondary btn-sm" %></span>
<% end %>
<% end %>
<% end %>
Then my controller for relationships looks like:
class RelationshipsController < ApplicationController
respond_to :js, :json, :html
def create
user = User.find(params[:followed_id])
#follow = current_user.follow(user)
end
def destroy
user = Relationship.find(params[:id]).followed
#unfollow = current_user.unfollow(user)
end
end
My view on user profile looks like:
<div class="col-5" style="margin-left: -5px;">
<%= render '/components/follow_button', :user => User.find_by_username(params[:id]) %>
</div>
My routes.rb have the following routes defined:
resources :users do
member do
get :following, :followers
end
end
resources :relationships, only: [:create, :destroy]
My Views folder structure has subfolders Users and Relationships. Both of them have separate controllers, and I have tried adding simple alert function 'alert("Works");' to the create.js.erb in both of those subfolders to try and match them with the controller, but none don't seem to work. This is my first Rails project, and I do not quite understand what the issue could be. Any suggestions?
Calling the partial follow/unfollow
<% if current_user.id != user.id %>
<%= render partial: 'follow_links', locals: { user: user }
<% end %>
Partial follow_links.
<% show_follow_link = current_user.following?(user) ? 'hidden' : '' %>
<% show_unfollow_link = current_user.following?(user) ? '' : 'hidden' %>
<!-- links to follow/unfollow have data-attributes that include the path to make the ajax post and the user to follow, that is used to find the link to show after the ajax call. You should use the path to the controller that will create or destroy the relationship -->
<%= link_to 'Follow', '#', { class: 'follow-user btn-success #{show_follow_link}', "data-url": follow_user_path(user.id), "data-followee": user.id } %>
<%= link_to 'Unfollow', '#', { class: 'unfollow-user btn-danger #{show_unfollow_link}', "data-url": unfollow_user_path(user.id), "data-followee": user.id } %>
Javascript for the partial. Ajax post to follow/unfollow
$('.follow-user').on("click",function() {
follow_unfollow($(this), "follow")
});
$('.unfollow-user').on("click",function() {
follow_unfollow($(this), "unfollow")
});
function follow_unfollow(target, what_to_do)
url = target.attr('data-url')
followee = target.attr('data-followee')
if (what_to_do == "follow") {
other_button = $('.unfollow-user[data-followee="'+followee+'"]')
} else {
other_button = $('.follow-user[data-followee="'+followee+'"]')
}
$.ajax( {
url: url,
type: 'post',
success: function() {
// Hide this link
target.addClass('hidden');
// Show the other link
other_button.removeClass('hidden');
},
error: function(ret) {
alert(ret.responseJSON.error);
}
});
};
Changes in your controller.
class RelationshipsController < ApplicationController
def create
user = User.find(params[:followed_id])
#follow = current_user.follow(user)
respond_to do |format|
if #follow.valid?
format.html
format.json: { render json: #follow }
return
else
format.html
format.json: { render json: { :error => 'Follow failed', :status_code :not_found } }
end
end
end
def destroy
user = Relationship.find(params[:id]).followed
#unfollow = current_user.unfollow(user)
respond_to do |format|
if #unfollow.valid?
format.html
format.json: { render json: #unfollow }
else
format.html
format.json: { render json: { :error => 'Unfollow failed', :status_code :not_found } }
end
end
end
end
An advice
An advice, also regarding your last question: I would recommend - instead of posting questions about debugging code on StackOverflow - create a good debugging environment for yourself.
Byebug or Binding pry is a good place to start, but before you can use those properly you need to understand the code you are using. I would recommend reading Working with Javascript in depth! - it really helped me getting the hang of it and understanding the dataflow of Rails and ajax.
This would, i think, break the unbreakable Stackoverflow-loop, that i myself were tied to for a long time:
loop do
puts "Try code"
sleep 1000
puts "Arrhh! an error!"
sleep 1000
puts "Posting on Stackoverflow"
sleep 1000
puts "Waiting for answer"
sleep 1000
end
I hope you figure it out!
I am following section 4 (Server Side Concerns) to set up ajax on a page. I've copied the tutorial text completely (replacing the model names with my own) and it creates and saves my "Participants" record, but does not automatically refresh the ajax partial.
This is the error I get...which looks like it's referrring to my create.js.erb
ActionView::Template::Error ('nil' is not an ActiveModel-compatible object. It must implement :to_partial_path.):
1: $("<%= escape_javascript(render #participant) %>").appendTo("#participants");
2: // $('#participants').html("<%= j (render #participants) %>");
app/views/participants/create.js.erb:2:in `_app_views_participants_create_js_erb___1675277149181037111_70181034249880'
Here's my code
class ParticipantsController < ApplicationController
def new
#participant = Participant.new
#participants = #participants.recently_updated
end
def create
#participant = Participant.new(participant_params)
respond_to do |format|
if #participant.save
format.html { redirect_to #participant, notice: 'Helper Invited!' }
format.js {}
format.json { render json: #participant, status: :created, location: #participant }
else
format.html { render action: "new" }
format.json { render json: #participant.errors, status: :unprocessable_entity }
end
end
end
_form.html.erb
<ul id="participants">
<%= render #participants %>
</ul>
<%= form_for(#participant, remote: true) do |f| %>
<%= f.label :email %><br>
<%= f.email_field :email %>
<%= f.submit 'SUBMIT' %>
<script>
$(document).ready(function() {
return $("#new_participant").on("ajax:success", function(e, data, status, xhr) {
return $("#new_participant").append(xhr.responseText);
}).on("ajax:error", function(e, xhr, status, error) {
return $("#new_participant").append("<p>Oops. Please Try again.</p>");
});
});
</script>
<script>
$(function() {
return $("a[data-remote]").on("ajax:success", function(e, data, status, xhr) {
return alert("The helper has been removed and notified.");
});
});
</script>
_participant.html.erb
<li >
<%= participant.email %> <%= link_to participant, remote: true, method: :delete, data: { confirm: 'Are you sure?' } do %>REMOVE<% end %>
</li>
create.js.erb
$("<%= escape_javascript(render #participant) %>").appendTo("#participants");
destroy.js.erb
$('#participants').html("<%= j (render #participants) %>");
It's on line 2 of your create.js.erb file, it's the missing #participants not the #participant.
You've commented the line out in JS, but the ERB is still going to be processed by Rails, so it's still trying to do the render #participants
Update
For future... it's the last line of that error that's the key:
app/views/participants/create.js.erb:2
See the 2 at the end, that's telling you which line the error happened on, and so that's where you need to focus when looking for the problem.
I am trying to create a to-do list app in Rails which consists of many to-do lists and each list has as many to-do items. When I try to destroy a completed to-do item with Ajax, I have to refresh the page before it disappears. I'm new to Rails and Javascript, so any thoughts would be appreciated.
Here's my Items destroy Javascript file:
<% if #item.destroyed? %>
$('#item-' +<%= #item.id %>).hide();
<% else %>
$('#item-' +<%= #item.id %>).prepend("<div class='alert alert-danger'><%= flash[:error] %></div>");
<% end %>
The Lists#show view that calls the item partial:
%h1= #title
.row
.col-md-6
= render 'items/item', locals: {items: #items}
.row
.col-md-6
= render 'items/form', locals: {list: #list, item: #item}
= link_to "Edit", edit_list_path(#list), class: 'btn btn-success'
= link_to "Delete List", #list, method: :delete, class: 'btn btn-danger', data: { confirm: 'Are you sure you want to delete this to-do list?' }
- if #lists.nil?
= link_to "New To-do List", new_list_path, class: 'btn btn-success'
The Item partial:
- #items.each do |item|
= item.name
= link_to "", [item.list, item], method: :delete, class: 'glyphicon glyphicon-ok', remote: true
%br/
Items Controller:
class ItemsController < ApplicationController
respond_to :html, :js
def create
#list = List.find(params[:list_id])
#item = #list.items.new(item_params)
#item.user_id = current_user.id
if #item.save
redirect_to #list, notice: 'Your new To-Do Item was saved!'
else
redirect_to #list, notice: 'You forgot to enter a To-Do item. Please try again.'
end
end
def destroy
#list = current_user.lists.find(params[:list_id])
#item = #list.items.find(params[:id])
#title = #list.title
if #item.destroy
flash[:notice] = "\"#{#item.name}\" was deleted successfully."
else
flash[:error] = "There was an error deleting the list."
end
respond_with(#item) do |format|
format.html {render [#list]}
end
end
def item_params
params.require(:item).permit(:name)
end
end
Your app sends a delete request when the link created by = link_to "", [item.list, item], method: :delete, class: 'glyphicon glyphicon-ok', remote: true is clicked, but does not change the view. You need to add an event listener that catches that ajax delete request is succeeded and hides the item element in the view.
Here's a rough code to achieve that. I didn't test the code but I think you can get the idea.
item partial:
.item-container
- #items.each do |item|
.item
= item.name
= link_to "", [item.list, item], method: :delete, class: 'glyphicon glyphicon-ok', remote: true
JS:
$(document).ready(function() {
$('.item-container').on('ajax:success', function() {
$(this).closest('.item').hide();
});
};
References:
http://api.jquery.com/on/
http://tech.thereq.com/post/17243732577/rails-3-using-link-to-remote-true-with-jquery
http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to-label-Options
By the way, where do you place the piece of JavaScript code you posted? I doubt it is ever executed.
Not terribly familiar with HAML, but it looks like you are not adding an ID in your view. Your destroy.js.erb is looking for a selector like id=item-1 in order to hide it. Therefore, your item partial needs to be populated with an id in order to find it later. In ERB, I'd do:
<ul>
<% #items.each do |item| %>
<li id="item-" + item.id><%= item.name %> </li>
<% end %>
</ul>
Something like that :)
I send certain data from a view to my controller. The controller action checks to see if the user has enough money, and if so, allows them to buy a tool of a certain price.
Otherwise, it doesnt update their tool.
Either way, I want to send a JSON response back to the view, to display.
How do I display these messages?
Here is my controller:
# Updates dollars and tool id when a user goes shopping in 'store'...
def update_tool
#user = User.find(params[:user_id])
#tool = Tool.find(params[:toolId])
price = #tool.price
# subtract tool price from users dollars
if #user.dollars <= price
respond_to do |format|
msg = { :status => "error", :message => "You do not have enough money!", :html => "<b>NO MONEY!</b>" }
format.json { render :json => msg }
end
else
#user.dollars = #user.dollars - price
# re-assign users tool_id reference ID
#user.tool_id = #tool.id
#store to database
#user.save
#sends a confirmation back to store
respond_to do |format|
msg = { :status => "success", :message => "You purchased a tool!", :html => "..." }
format.json { render :json => msg }
end
end
end
I want to take these status responses and use them to trigger events in my view,
something like this:
success: function(){
window.alert(':message');
},
error: function(){
window.alert(':message');
}
I'm just uncertain how to access the content of the json response message.
UPDATE:
Heres my AJAX request, with my success or failure functions:
function buyTool() {
$.ajax({
type: 'PATCH',
headers: {
'X-CSRF-Token': '<%= form_authenticity_token.to_s %>'
},
url: '<%= update_tool_path %>',
dataType: "JSON",
async: true,
data: {
'user_id' : <%= #user.id %>,
'toolId' : toolId
},
success: function(){
window.alert(":json");
},
error: function(){
window.alert(":json");
}
});
};
Its not working though-- My alert windows just actually displays the text ":json".
Do I need to pass the anon error: function that data?
My preference for this is to use flashes that are triggered by ajax. To do this use the following.
Add the following to your ApplicationController.rb
after_filter :flash_to_headers
#....
private
def flash_to_headers
return unless request.xhr?
response.headers['X-Message'] = flash_message
response.headers["X-Message-Type"] = flash_type.to_s
flash.discard # don't want the flash to appear when you reload page
end
def flash_message
[:error, :warning, :notice, :success].each do |type|
return flash[type] unless flash[type].blank?
end
end
def flash_type
[:error, :warning, :notice, :success].each do |type|
return type unless flash[type].blank?
end
end
And then add a flashes.js.coffee file with the following (This uses bootstrap styled flashes so just change the classes to something with your own styling)
show_ajax_message = (msg, type) ->
if (type == "error")
$("#flash-message").html "<div id='flash-#{type}' class='alert alert-danger'>#{msg}</div>"
else if (type == "success")
$("#flash-message").html "<div id='flash-#{type}' class='alert alert-success'>#{msg}</div>"
else if (type == "notice")
$("#flash-message").html "<div id='flash-#{type}' class='alert alert-info'>#{msg}</div>"
else if (type == "warning")
$("#flash-message").html "<div id='flash-#{type}' class='alert alert-warning'>#{msg}</div>"
$("#flash-#{type}").delay(5000).slideUp 'slow'
$(document).ajaxComplete (event, request) ->
msg = request.getResponseHeader("X-Message")
type = request.getResponseHeader("X-Message-Type")
show_ajax_message msg, type
Finally add somewhere for the flashes to render
# views/shared/_flashes.html.erb
<!-- Id is used for ajax flashes -->
<div id="flash-message">
<% if flash[:notice] %>
<div class="alert alert-success">
<%= flash[:notice] %>
</div>
<% elsif flash[:error] %>
<div class="alert alert-danger">
<%= flash[:error] %>
</div>
<% elsif flash[:alert] %>
<div class="alert alert-info">
<%= flash[:alert] %>
</div>
<% end %>
<% flash.discard %>
</div>
and render it from your layouts/application.html.erb
<%= render 'shared/flashes' %>
After this you can trigger flash messages as you would normally in rails and they will appear.