Rails javascript polling - javascript

I'm trying to implement javascript polling in my app but I'm running into a few problems. I'm pretty much following along with this railscasts. My problem is in trying to prepending any new data found. It prepends all of the data old and new and if there isn't any new data found it just prepends all of the old data. My other problem is that my setTimeout is only being called once, even after I try to keep it polling like they show in railscast. Below is my code. What am I doing wrong here?
polling.js
var InboxPoller;
InboxPoller = {
poll: function() {
return setTimeout(this.request, 5000);
},
request: function() {
return $.getScript($('.inbox_wrap').data('url'), {
after: function() {
$('.conversation').last().data('id')
}
});
}
};
$(function() {
if ($('.inbox_wrap').length > 0) {
return InboxPoller.poll();
}
});
polling.js.erb
$(".inbox_wrap").prepend("<%= escape_javascript(render #conversations, :locals => {:conversation => #conversation}) %>");
InboxPoller.poll();
conversations_controller.rb
class ConversationsController < ApplicationController
before_filter :authenticate_member!
helper_method :mailbox, :conversation
def index
#messages_count = current_member.mailbox.inbox({:read => false}).count
#conversations = current_member.mailbox.inbox.order('created_at desc').page(params[:page]).per_page(15)
end
def polling
#conversations = current_member.mailbox.inbox.where('conversation_id > ?', params[:after].to_i)
end
def show
#receipts = conversation.receipts_for(current_member).order('created_at desc').page(params[:page]).per_page(20)
render :action => :show
#receipts.mark_as_read
end
def create
recipient_emails = conversation_params(:recipients).split(',').take(14)
recipients = Member.where(user_name: recipient_emails).all
#conversation = current_member.send_message(recipients, *conversation_params(:body, :subject)).conversation
respond_to do |format|
format.html { redirect_to conversation_path(conversation) }
format.js
end
end
def reply
#receipts = conversation.receipts_for(current_member).order('created_at desc').page(params[:page]).per_page(20)
#receipt = current_member.reply_to_conversation(conversation, *message_params(:body, :subject))
respond_to do |format|
format.html { conversation_path(conversation) }
format.js
end
end
private
def mailbox
#mailbox ||= current_member.mailbox
end
def conversation
#conversation ||= mailbox.conversations.find(params[:id])
end
def conversation_params(*keys)
fetch_params(:conversation, *keys)
end
def message_params(*keys)
fetch_params(:message, *keys)
end
def fetch_params(key, *subkeys)
params[key].instance_eval do
case subkeys.size
when 0 then self
when 1 then self[subkeys.first]
else subkeys.map{|k| self[k] }
end
end
end
def check_current_subject_in_conversation
if !conversation.is_participant?(current_member)
redirect_to conversations_path
end
end
end
index.html.erb
<%= content_tag :div, class: "inbox_wrap", data: {url: polling_conversations_url} do %>
<%= render partial: "conversations/conversation", :collection => #conversations, :as => :conversation %>
<% end %>
_conversation.html.erb
<div id="conv_<%= conversation.id %>_<%= current_member.id %>" class="conversation" data-id="<%= conversation.id %>">
<div class="conv_body">
<%= conversation.last_message.body %>
</div>
<div class="conv_time">
<%= conversation.updated_at.localtime.strftime("%a, %m/%e %I:%M%P") %>
</div>
</div>

Javascript polling is extremely inefficient - basically sending requests every few seconds to your server to listen for "updates". Even then, in many cases, the updates will be entire files / batches of data with no succinctness
If we ever have to do something like this, we always look at using one of the more efficient technologies, specifically SSE's or Websockets
--
SSE's
Have you considered using Server Sent Events?
These are an HTML5 technology which work very similarly to the Javascript polling - sending requests every few seconds. The difference is the underlying way these work -- to listen to its own "channel" (mime type text/event-stream -- allowing you to be really specific with the data you send)
You can call it like this:
#app/assets/javascript/application.js
var source = new EventSource("your/controller/endpoint");
source.onmessage = function(event) {
console.log(event.data);
};
This will allow you to create an endpoint for the "event listener":
#config/routes.rb
resources :controller do
collection do
get :event_updates #-> domain.com/controller/event_updates
end
end
You can send the updates using the ActionController::Live::SSE class:
#app/controllers/your_controller.rb
Class YourController < ApplicationController
include ActionController::Live
def event_updates
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 300, event: "event-name")
sse.write({ name: 'John'})
sse.write({ name: 'John'}, id: 10)
sse.write({ name: 'John'}, id: 10, event: "other-event")
sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
ensure
sse.close
end
end
--
Websockets
The preferred way to do this is to use websockets
Websockets are much more efficient than SSE's or standard JS polling, as they keep a connection open perpetually. This means you can send / receive any of the updates you require without having to send constant updates to the server
The problem with Websockets is the setup process - it's very difficult to run a WebSocket connection on your own app server, hence why many people don't do it.
If you're interested in Websockets, you may wish to look into using Pusher - a third party websocket provider who have Ruby integration. We use this a lot - it's a very effective way to provide "real time" updates in your application, and no I'm not affiliated with them

Are you sure that polling is happening only once? Did you check in the console to make sure of that?
To me it looks like it might be happening, because I see issues in your javascript that prepends the content. What you have is this:
$(".inbox_wrap").prepend("<%= escape_javascript(render #conversations, :locals => {:conversation => #conversation}) %>");
This will actually insert the new content before inbox_wrap div. I think your intention is to prepend to the conversations list. For that, you should change it to this (Reference jQuery Docs: http://api.jquery.com/prepend/):
$(".conversation").prepend("<%= escape_javascript(render #conversations, :locals => {:conversation => #conversation}) %>");
The next thing is that I am assuming you want to prepend the new conversations on top of the list, which means two things.
The controller will return new conversations ordered by date in descending order.
Assuming above is correct, you would need to get the first conversation's id in your InboxPoller to pass to the controller as after parameter.
$('.conversation').first().data('id')
One More Thing
Another thing is that you can use the native Javascript function setInterval instead of setTimeout. setInterval executes a given function periodically, as opposed to setTimeout which does it only once. This way you won't have to initiate your InboxPoller after every call to the back-end, in .js.erb file.
Update
Again, looking at jQuery Documentation, it looks like $.getScript() does not pass any parameters back to the server. Instead use $.get as below:
InboxPoller = {
poll: function() {
return setInterval(this.request, 5000);
},
request: function() {
$.get($('.inbox_wrap').data('url'), {
after: function() {
return $('.conversation').first().data('id');
}
});
}
};
Also, I think you need to add .order('created_at desc') in polling method in your controller.

Related

Rails Controller - refresh page same as if I hit the refresh on my browser

I have inherited a Rails code base that I do not fully understand. We have a requirement to, when the user hits Submit, render ON THAT PAGE the set of validation failures the user put into the form. I cannot redirect to any other page - we must remain on the page which contains the form upon which they put the invalid input.
Here is my method
def tpr_bulk_update
updated, date_died = update_tprs #it returns 0,0 when there are validation fails
if updated == 0 && date_died == 0
flash.now[:notice] = 'A bunch of errors occurred'
#Here I need to refresh the page. I do not especially want to redirect.
#I want this to perform exactly the same as me hitting the refresh button on my browser.
#The initial form loaded via a very complicated codebase that I do not understand exactly.
#I do have available to me the params from the initial call - but it seems to me hitting refresh on
#the browser implicitly handles repassing to this method with the SAME PARAMS I CAME IN WITH....
#AND it shows my flash.now. So then, I want to refresh the page the same mechanism the browser uses,
#because this is what demonstrably meets my requirement
elsif !date_died
redirect_to tprs_index_vod_assets_path
else
flash[:notice] = "One or more TPR assets were not given valid future dates, so those invalid dates did not save".html_safe
redirect_to tprs_index_vod_assets_path
end
end
The issue is I see no way to do this. Perhaps the browser invoking refresh uses javascript that is impossible to inline in my rails controller?
redirect_to :back
fails on account that the set of params I came in with are not populated - it explodes.
respond_to do |format|
format.js {render inline: "location.reload();" }
end
My method does not output javascript, and neither will it ever output javascript - that is a requirement for the system.
I need whatever is equivalent to the refresh operation my browser (Chrome) performs when I press "Refresh". I want that to happen right after I set my flash.now message. How can I capture what Chrome/what hitting refresh actually does? And how can I perform it within my controller?
This is exactly what AJAX was designed for. By sending an asyncronous request you can send data to the server without reloading the page and update the page with the response. In Rails you can use Server Side Concerns to simply replace a chunk of contents with a rendered view.
Since you don't actually have an example of the form or controller this is a simplefied example that demonstrates the concept:
class ThingsController < ApplicationController
def new
#thing = Thing.new
end
def create
#thing = Thing.new(thing_params)
respond_to do |format|
if #thing.save
format.js
else
format.js { render :new }
end
end
end
end
end
# things/_form.html.erb
# remote: true is the default - its just added for extra clarity here
<%= form_with(model: #thing, remote: true, id: 'my-special-form') do |form| %>
<% if #thing.errors %>
# .. display the errors
<% end %>
# ...
<% end %>
# things/new.html.erb
<%= render partial: 'form' %>
// things/new.js.erb
// Since we want to extract the children of the form element
// we use DOMParser to create a fragment
const node = new DOMParser().parseFromString(
"<%= j render(partial: 'form') %>", // rails renders the template
"text/html"
);
// Find form on page and replace its contents
document.getElementById('my-special-form').innerHTML = node.querySelector('form').innerHTML;
// #todo set flash message
// things/create.js.erb
window.location = "<%= thing_path(#thing) %>";
The way that this works is that Rails UJS listens for submit events on any element with the data-remote attribute. Instead of the normal submit it will send an XHR request with the Content-Type: application/javascript header.
After rails is finished rendering and reponding with your js.erb view it is sent back to the client and Rails UJS takes the response and evals it by popping it into a script tag.

The right place for the Js to render a partial in Rails

My plan was to update a partial via a klick-event where I fetch some data via ajax.
So i opened my assets/javascripts directory and put the code in the js file of the module I planed to call it on. I managed to fetch the data and to call some "html() or text()" on the div. But as soon as I tried to call
.html("<%= escape_javascript( render( :partial => 'job') ) %>")
my controller told me unknown method "render". So I red in many other Threats that the assets directory was not build for static content.
But now comes the question where to put it if not in assets?
I tried to put it in the view/model folder (with the same name as the aktion 'show'). But it wouldn't load. Yes I told the Controller in the show action to respond_to format.js.
So where should I put the js-File and how to call it? Or is there even a method to let it in the assets-directory and beeing able to call render?
Thanks for your answer! Sadly I do exactly that but I doesnt work. So here is my Code:
/app/views/events/show.js.erb.coffee:
$(document).ready ->
url = window.location
url = String(url).split('/')
event_id = url[$.inArray( "events" , url) + 1]
$('.position').click ->
position_id = $(this).attr("id")
$.ajax '/events/'+event_id+'/eventpositions/'+position_id+'/eventjobs'
dataType: 'json'
success: (result)->
$( '#'+position_id ).find('.jobs').html("<%= escape_javascript( render( :partial => 'job') ) %>")
alert result[1].job_id
/app/views/events/show.html.haml:
%p#notice{:xmlns => "http://www.w3.org/1999/html"}= notice
%p
%b Name:
= #event.name
\ 
%b Plz:
= #event.plz
%p
%b Personal:
- #eventpositions.each do |p|
.position{:id => "#{p.id}"}
%b
Position: #{Position.find(p.position_id).name} Anzahl: #{p.quantity} Aufträge: #{p.eventjobs.all.count}
.jobs
= link_to 'Neuer Auftrag', new_event_eventposition_eventjob_path(#event.id,p.id)
%br
%br
= link_to 'Neue Position', new_event_eventposition_path(#event.id)
= link_to 'Edit', edit_event_path(#event)
|
\#{link_to 'Back', events_path}
/app/views/events/_job.html.haml:
.jobs
- eventjobs.each do |j|
User: #{User.find(Job.find(j.job_id).user_id).email}
%br
/app/controllers/events_controller.rb:
def show
#agency = Agency.find(current_agent.agency_id)
#event = #agency.events.find(params[:id])
#eventpositions = #event.eventpositions.all
respond_to do |format|
format.html # show.html.erb
format.json { render json: #event }
format.js
end
end
So the Code does work without Javascript. All renders and is fine. But my main Problem now is that it wont even react to a click event if I place it in the app/views/events Folder. As soon as I place it in the assets directory I works like a charm, but wont render.
So what am I missing? Why does my js Code does not get loaded?
Thanks a lot for your help!
Generally for this kind of thing, I'll name it the same as a particular action, such as show.js.erb, and then use the format.js as you have in the controller. If you're using coffeescript, it will need to be named show.js.coffee.erb. If this does not work, can you post the code of your view where this onclick event is setup?
Edit for using the show javascript code
Because you're using the show action, should just be able to do this:
= link_to "Show", #event, :remote => true, :format => :js
Adjust the event variable there as you need to, but it should work

Query rails database, and update highcharts using Javascript

I have a page which shows a highchar. I'd like to use Javascript to fetch several pieces of information for a specific user. I want to fetch from my database and place it on the highcharts graoh. I have set up a JSfiddle which shows static data. But
What I am trying to do is:
Make javascript call a Rails action with parameters.
Query the rails database(projectHours table).
Rails returns the response.
Javascript updates highcharts and maps the information stored in the projectHours table.
So my question is What if I want to use information from my db?
Two of the following models
Effort.rb
class Effort < ActiveRecord::Base
belongs_to :project_task
belongs_to :user
end
Users.rb
class User < ActiveRecord::Base
has_many :projects
has_many :efforts
A further note, I think that in my efforts controller I may need to add some form of render action
rails reponds_to do |f| f.json
{ render :json => some_hash
}
end
You can use jQuery's get function in order to send a GET request to your rails app like so
$.get("/efforts/24", { name: "John", time: "2pm" } );
And in your controller you could do something like
def show
#effort = Effort.find(params[:id])
# or find by some of the name of time params
respond_to do |f|
format.json { render :json => #effort }
end
end
Depending on how you want to render the results you can either let javascript handle the ajax:success by adding in to the original jQuery get
$.get("/efforts/24", { name: "John", time: "2pm" } ).success(function(data) {
alert(data)
// Or do whatever you want :)
});
Or you can change the respond_to to
respond_to do |f|
format.html { render :nothing => true }
format.js
end
And create a show.js.erb in your app/views/efforts directorty. In this file you can use the responded object
$('body').html("<h1><%= escape_javaScript(#effort.title) %></h1>").append("
<%=escape_javaScript(#effort.content) %>");
Some good tutorials
Rails and jQuery
jQuery GET

Ajax requests clears the session in Rails 3.0.5 [duplicate]

I have an action triggered by an AJAX request generated by Ajax.InPlaceEditor or InPlaceCollectionEditor like this:
new Ajax.InPlaceCollectionEditor('agent_email', 'inspections/<%= #inspection.id %>/update_field',
{
collection: [<% #agents.each do |agent| %>
'<%= agent.email %>',
<% end %>],
okText: 'Update',
cancelText: 'Never mind',
savingText: 'Updating...'
});
At the other end, the action contains this:
def update_field
--some code here--
if success
puts "stored change"
render :text => result
else
puts "did note change store"
render :text => inspection.errors.to_json, :status => 500
end
end
Once any of the render methods are reached, the session expires, and next time the user send a request, Devise sends them to the logon on page.
Even though I am exempting update_field from authentication (before_filter :authenticate_user!, :except => :update_field), the session is still getting reset.
I have looked at the answer at a very similar question at Devise session immediately expiring on .js call [AJAX], but it is not solving my particular problem.
Any ideas?
I got this to work by getting the code from http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails (prototype-snippet.js):
/*
* Registers a callback which copies the csrf token into the
* X-CSRF-Token header with each ajax request. Necessary to
* work with rails applications which have fixed
* CVE-2011-0447
*/
Ajax.Responders.register({
onCreate: function(request) {
var csrf_meta_tag = $$('meta[name=csrf-token]')[0];
if (csrf_meta_tag) {
var header = 'X-CSRF-Token',
token = csrf_meta_tag.readAttribute('content');
if (!request.options.requestHeaders) {
request.options.requestHeaders = {};
}
request.options.requestHeaders[header] = token;
}
}
});
... within a Javascript block in my application.html.erb:
<script type="text/javascript">
(... the code from above)
</script>
Also don't forget to add:
<%= csrf_meta_tag %>
in the same file towards the top (if not already there).
The document "CSRF Protection Bypass in Ruby on Rails" explains why this works.

AJAXy action accessible via "normal" request

Greetings,
I have an application which contains a calendar as alternative index view for Courses (Project has_many Courses). There I have two arrows for navigating to the next/previous month, it works via AJAX. Now, that's my action for updating the calendar:
def update_calendar
#project = Project.find params[:id]
#date = Date.parse(params[:date]).beginning_of_month
#courses = #project.courses.all(:conditions => {:date => (#date - 1.month)..(#date + 1.month)})
respond_to do |format|
# format.html { redirect_to :action => 'index' }
format.js { render :partial => 'calendar', :locals => {:calendar_date => #date, :courses => #courses} }
end
end
The important part is format.js { ... }. I thought it should just answer js/AJAX requests, but it doesn't. It renders the partial when I hit the URL http://localhost:3000/projects/1/update_calendar?date=2010-08-01 for example. I don't want that behaviour, I just want it to answer correctly when it's coming in via AJAX. Do I need to use request.xhr?? And what is format.js { ... } supposed to do than?
Best regards
Tobias
You need to tell it what you want to happen when someone goes to that action with an html request, ie you need to put your format.html block back in. If you don't have the braces it will do the default behaviour which is to show the update_calendar view. You probably want to redirect to a different action though.

Categories