Refreshing multiple partials with polling in Rails - javascript

Let's say I have a list of statuses that might look like this:
ul#list
- #list_items.each do |item|
li.loading Item #{item.id} - Status: #{item.status}
li Item #{item.id} - Status: #{item.status}
li Item #{item.id} - Status: #{item.status}
li.loading Item #{item.id} - Status: #{item.status}
Which renders me:
Item 1 - Status: Loading
Item 2 - Status: Finished
Item 3 - Status: Finished
Item 4 - Status: Loading
What I would like to do is periodically poll for changes on individual list items and refresh them if the status has changed. So far I was able to get away with refreshing the whole list:
ul#list
== render 'status_list', list_items: #list_items
Coffee:
if $('.loading').length > 0
setInterval (=>
#refreshListPartial()
), 5000
Where #refreshListPartial is an AJAX function that hits the Rails Controller which then goes on to refresh the whole list partial:
$("#list").html("<%= escape_javascript(render partial: 'status_list', locals: { list_items: #list_items } ) %>");
But how would one go in order to check the state of individual list items on the page and refresh only them? I know that React would probably be a much easier solution for this task, but is it even possible to accomplish with Rails without jumping over a dozen hoops? Another thing that came to mind is ActionCable (I'm using Rails 5), but opening a perma-connection for this functionality seems to be an overkill, I'd rather prefer polling.
Update
Just thinking out loud. So to refresh multiple partials instead of one I'll need to arrive to this in my .js.erb file:
<%- #items.each do |item| %>
$("#list-item-<%= item.id %>").html("<%= escape_javascript(render partial: 'item', locals: { list_item: item } ) %>");
<% end %>
The view should now look like:
ul#list
#list_items.each do |item|
== render 'list_item', list_item: #list_item
So what's left is the ajax function that should get the ids' of the list items that are needed to be refreshed and send them to the controller as an array.

I ended up doing an extension of what I myself proposed in the question update.
Frontend code that checks whether some partials need to be refreshed based on their data-tags:
class JobRequestPartialReload
constructor: ->
checkForRunningJobs = ->
if $('.poll_for_changes').length > 0
#arr = []
$('.poll_for_changes').each (index, element) =>
#arr.push(element.closest('[data-id]').dataset.id)
sendDataToRails()
sendDataToRails = ->
$.ajax
url: "/jobs/refresh_list"
method: "POST"
data: {jobRequestList: #arr}
setInterval (=>
checkForRunningJobs()
), 10000
$(document).on 'turbolinks:load', new JobRequestPartialReload
Controller:
def refresh_list
ajax_data = params[:jobRequestList].map(&:to_i)
#job_requests = JobRequest.includes(...).where(id: ajax_data)
respond_to do |format|
format.js
end
end
Finally, the JS.erb file:
<% #job_requests.each do |job| %>
<% case job.state %>
<% when 'initiated' %>
// don't care
<% when 'active' %>
visibleStatus = $('#job-id-<%= job.id %> .status-list').text()
if (visibleStatus == 'initiated') {
$('#job-id-<%= job.id %>').html("<%= escape_javascript(render partial: 'job_requests/shared/job_request', locals: { job: job } ) %>");
<% else %>
// failed, completed, etc.
$('#job-id-<%= job.id %>').html("<%= escape_javascript(render partial: 'job_requests/shared/job_request', locals: { job: job } ) %>");
<% end %>
<% end %>
Answer update
I later added js code that checked whether certain partials were in the actual user viewport, and checked only them, at the rate of 5-10 seconds. This greatly reduced the number of queries each client was sending.

Related

Rails 7 how to open table inside a row of another table

In my Rails 7 (with bootstrap) app I need to create a table with all users transaction that has on each row an arrow that when clicked on, expands that row inside which is a new table (that's what I think it is - a new table). I think the attached design will better explain what I mean:
To achieved that I could try javascript with something like this https://jsfiddle.net/Wfxpu/180/ but I'm not sure if it's a modern approach. I'm wondering is it possible to a write something like this without any JS code using turbo maybe? or even pure HTML ?
Here is what I was trying to do:
# transaction controller
class TransactionsController < ApplicationController
def index
response = client.transactions.list(platform_id: current_user.platform_id, page: 1, per_page: 100)
#transactions = response.body['data']
end
private
def client
#client ||= TestAPI::Client.new
end
end
so then sample table would be:
# views/transactions/index
<table>
<% #transactions.each do |transaction| %>
<tr><%= transaction.amount %></tr>
<% end %>
</table>
You could try using a remote link_to as your "arrow" to invoke show_more action.
<%= link_to your_path, remote: true do %>
your image
<% end %>
In your controller you would need to respond to that with
def show_more
...
#data_to_show = some_database_call
respond_to do |format|
format.js
...
end
end
then in show_more.js.erb you can run javascript using the data that you just received.
var content = document.querySelector("#hidden-section-1");
content.innerHTML = ""
content.insertAdjacentHTML("beforeend",
"<%= j render('layouts/your_template', data: #data_to_show) %>");
where layouts/your_template would be a template with all the stuff you want to show(the "other" table you mentioned)
<h1> merchant: <%= data.merchant %> <h1>
...
<p> transaction_id: <%= data.transaction_id %> </p>
...

The jquery code is not executed after a renderJS

(Sorry for my english )
I realized 3 tabs that dynamically reload my activities, a GET request is made in order to display a specific list of activities (activity of a group / of the user / news)
However I have a jquery code that no longer runs when I click on one of these tabs,
With rails I return a collection to display the activities, a solution remains to put the jquery in this collection, but when I do this the jquery is executed for each activities loaded and is not that i want.
Activities :
<div class="activity-card-container" id="activity-cards">
<%= render partial: 'activities/activity', collection: #activities %>
</div>
$('.newness').on('mouseenter', function (event) {
event.preventDefault();
$(this).toggleClass('d-none');
let num = parseInt($.trim($('.body__activity_counter').html()));
$('.body__activity_counter').html(--num);
if (num == 0) {
$('.body__activity_counter').addClass('hover');
}
newnessId = $(this).data('id');
$.ajax({url: '/user_activities.json', method: 'POST', data: {user_activity: {activity_id: newnessId}}})
});
Partial Activity :
<div class="activity--js margin-card activity-card card card-body">
.......
<%= activity.title %>
<%= activity.content %>
.......
</div>
One of the Tab controller :
def fetch_activities
user = current_user
activities = []
user.groups.each do |group|
activities << Activity.where(group: group)
end
#all_activities = activities.flatten.sort_by(&:updated_at).reverse
respond_to do |format|
format.js
end
end
and his route :
get "/fetch_activities" => 'activities#fetch_activities', as: 'fetch_activities'
Thanks

How can I update an instance variable with each ajax request?

I have a long block of comments on a view of model Page. Instead of showing all the comments on page load, I'm trying to create a "view more" button that shows the next ten comments. The button sends an ajax request to the controller, which then renders this block using jquery:
_view_more.html.erb
<% comments.each_with_index do |comment, index|%>
<% if (( index > #start_number) && (index < #end_number) ) %>
<%= comment.text %>
<% end %>
Let's say I always want to show the next 10 comments. I would just set #start_number = #start_number + 10 and #end_number = #end_number + 10
in the controller, but instance variables get reset, so #start_number would be nil. How can I set a variable that increases by 10 upon every ajax request?
"view more" button
<%= link_to "view more", view_more_page_path, remote: true %>
pages_controller.rb
def view_more
#page = Page.find(params[:id])
respond_to do |format|
format.html { redirect_to root_path }
format.js
end
end
view_more
$("#comments-body").append("<%= escape_javascript(render 'view_more') %>");
I will use haml and coffee-script
When rendering comments you put an html5 data attribute with the id of the comment:
#view where to render comments
%div#comments-wrapper{:"data-pageid" => #page.id}
=render #comments
=link_to "View more", "#", id: "view-more-link"
The comment partial
#comments/_comment.html.haml
%p.single-comment{:"data-commentid" => comment.id}
=comment.body
application.coffee
$ ->
$("#view-more-link").on 'click', ->
last_comment_id = $("#comments-wrapper .single-comment").last().data('commentid')
page_id = $("#comments-wrapper").data("pageid")
$.ajax
url: "/comments/view_more"
dataType: "script"
data:
last_comment_id: last_comment_id
page_id: page_id
comments_controller
def view_more
#page = Page.find(params[:pageid])
if params[:last_comment_id]
#comments = #page.comments.where("comments.id > ?", params[:last_comment_id]).limit(10)
end
respond_to do |format|
format.js
end
end
comments/view_more.js.erb
$("#comments-wrapper").append("<%= escape_javascript(render #comments) %>");
Note: I don't how your routes were set up so I put the page.id as a data-attribute as well
I would use already implemented pagination gems kaminari or will_paginate. I'll create this example using will_paginate.
First of all, it's important to say that your approach is incorrect, because it loads all comments every view_more request. If you want to show 10 comments, makes sense select only they from database, right? The pagination gem will do it for you!
Let's to the code:
"view more" button
<%= link_to "view more", view_more_page_path, remote: true, id: 'view-more-btn' %>
pages_controller.rb
def view_more
#comments = Page.find(params[:id]).comments.paginate(page: params[:page], per_page: 10)
respond_to do |format|
format.html { redirect_to root_path }
format.js
end
end
_view_more.html.erb
<% #comments.each do |comment| %>
<%= comment.text %>
<% end %>
view_more.js.erb
$("#comments-wrapper").append("<%= escape_javascript(render 'view_more') %>");
# We need to update the 'view more' link with the next page number
$('#view-more-btn').attr('href', '<%= view_more_page_path((params[:page] || 0) + 1) %>')
is it not good to update an hidden variable before making ajax call with the count..?
var currentVal = parseInt($("[type=hidden]").val());
$("[type=hidden]").val( currentVal + 1 );
add the hidden field right at the begining of the comments section with default value as "0"
<input type="hidden" name="hiddenId" id="hiddenId" value="0">
Hope it will help
If you want a quick and dirty approach, you could save start_number and end_number inside a cookie.
But keeping track of what needs to be rendered next on client side would be a right thing to do.

Can't find the error in my dependent select drop down on Active Admin( Rails 3.2, Active Admin 1.0)

I'm trying to build a RoR app, with three models:
Games that can be classified in a Sector(called GameSector) and in a subsector (called GameSubsector)
A sector is made up of many subsectors.
a Subsector.
Here are my basic models relationships:
models/game.rb
belongs_to :game_sector, :foreign_key => 'game_sector_id', :counter_cache => true
belongs_to :game_subsector, :foreign_key => 'game_subsector_id',:counter_cache => true
I use Active Admin to input the Games, Sectors or subsectors information.
I have a very basic form when I create a game and I'd just like to make the second select drop down (game_subsector) adjust on the choice of the first select (gamesector) so that I don't the the whole (very long) list of game_subsectors but only those that belong to the game_sector I choose.
After dozens of tests and techniques tried but failing, I've finally used this dev's advice that appeared relevant to me: http://samuelmullen.com/2011/02/dynamic-dropdowns-with-rails-jquery-and-ajax/.
But it still does not work.
Here is the form on Active Admin which is located on admin/game.rb
ActiveAdmin.register Game do
menu :parent => "Campaigns", :priority => 1
controller do
with_role :admin_user
def game_subsectors_by_game_sector
if params[:id].present?
#game_subsectors = GameSector.find(params[:id]).game_subsectors
else
#game_subsectors = []
end
respond_to do |format|
format.js
end
end
end
form do |f|
f.inputs "Details" do
f.input :name
f.input :game_sector_id,
:label => "Select industry:",
:as => :select, :collection => GameSector.all(:order => :name),
:input_html => { :rel => "/game_sectors/game_subsectors_by_game_sector" }
f.input :game_subsector_id, :as => :select, :collection => GameSubsector.all(:order => :name)
f.actions
end
I feel the javascript is even maybe not fired.
The jquery I use is located on app/assets/javascript/admin/active_admin.js (I changed config so it loads this javascript when loading active admin pages)
jQuery.ajaxSetup({
'beforeSend': function(xhr) { xhr.setRequestHeader("Accept", "text/javascript"); }
});
$.fn.subSelectWithAjax = function() {
var that = this;
this.change(function() {
$.post(that.attr('rel'), {id: that.val()}, null, "script");
});
};
$("#game_game_sector_id").subSelectWithAjax(); //it it found in my view???
Finally I created a view as this expert adviced: in app/views/layout/ game_subsectors_by_game_sector.js.erb
$("#game_game_subsector_id").html('<%= options_for_select(#game_subsectors.map {|sc| [sc.name, sc.id]}).gsub(/n/, '') %>');
I'm not sure I have out it in the right place though...
What you need is:
Inspect with your web browser console your selects, and use a CSS selector to create a jQuery object for the sector select, something like:
$('#sector_select')
Append to this object a handler, so when it changes AJAX request is fired:
$('#sector_select').change(function(){
$.ajax('/subsectors/for_select', {sector_id: $(this).val()})
.done(function(response){ // 3. populate subsector select
$('#subsector_select').html(response);
});
});
See 3 in code, you need to inspect to get the right CSS selector. Be sure you are getting the expected response in the Network tab of your web browser inspector(if using Chrome).
You need a controller that answers in /subsectors/for_select, in the file app/controllers/subsectors_controller.rb:
class SubsectorsController < ApplicationController
def for_select
#subsectors = Subsector.where sector_id: params[:sector_id]
end
end
You need a view that returns the options to be populated app/views/subsectors/for_select.html.erb:
<% #subsectors.each do |ss| %>
<option value="<%= ss.id %>"><%= ss.name %></option>
<% end %>
You need a route:
get '/subsectors/for_select', to: 'subsectors#for_select'

Issue with Ajax Appending

I have a comment model that is paginated and I would like the comments to load more comments on the same page whenever the next button is clicked. I have somewhat of an idea of how to go about doing this but can anyone advise how to go about doing this. I have some code already.
For the comment section instead of render I think it may have to be looking for the micropost and its id to find the right comments to append but I am unsure about how to go about tying this all together.
Pagination JS
$(function() {
$("#CommentPagin a").live("click", function() {
$.getScript(this.href);
return false;
});
});
Show JS
$("#cc").append('<%= escape_javascript(render :partial => "users/comments" )%>');
Comment Section
<div id='comments'>
<% comments = micropost.comments.paginate(:per_page => 5, :page => params[:page]) %>
<div id="CommentPagin">
<span class="CommentArrowIcon"></span>
<%= will_paginate comments, :page_links => false , :class =>"pagination" %>
</div>
<%= render 'users/comments' %>
</div>
Comment Rendering Section
<div id="cc">
<% comments = micropost.comments.paginate(:per_page => 5, :page => params[:page]) %>
<%= render comments %>
</div>
User Controller
def show
#user = User.find(params[:id])
#school = School.find(params[:id])
#comment = Comment.find(params[:id])
#micropost = Micropost.new
#comment = Comment.new
#comment = #micropost.comments.build(params[:comment])
#microposts = #user.microposts.order('created_at DESC').paginate(:per_page => 10, :page => params[:page])
respond_to do |format|
format.html
format.js
end
end
I´m a bit rusty with rails so this is somewhat generic answer.
I would load the next n comments from a route / action that renders just your Comment Rendering Section as HTML
Just think of it as you where requesting assets from your own API and using them to update the page.
Pagination JS
/**
* jQuery 1.7+
* use .delegate() for older versions.
**/
$("#CommentPagin").on('click', 'a', function(e){
// Get data from server - make sure url has params for per_page and page.
$.get($(this).attr('href'), function(data){
// refresh client with data
$("#cc").append(data);
});
});

Categories

Resources