I'm writing a Rails app and need to plug in this little bit of jQuery code, but I don't really know how to get it to work. Here's my controller code:
class ChatroomController < ApplicationController
def send_data
#role = Role.find_by_id(session[:role_id])
render :juggernaut do |page|
page.insert_html :bottom, 'chat_data', "<b>#{#role.name}:</b> #{h params[:chat_input]}<br>"
end
render :nothing => true
end
end
and view code:
<h2>Chat</h2>
<html>
<head>
<%= javascript_include_tag :defaults, :juggernaut %>
<%= juggernaut %>
</head>
<body>
<div id='chat_data', class="chatbox">
</div>
<br>
<%= form_remote_tag(
:url => { :action => :send_data },
:complete => "$('chat_input').value = ''" ) %>
<%= text_area_tag( 'chat_input', '', { :rows => 3, :cols => 70, :id => 'chat_input'} ) %>
<%= submit_tag "Send" %>
</form>
</body>
</html>
Now, I need to make the chatroom always scroll down to the bottom when any user sends a new message. But also, when the current user has manually scrolled up, disable this sort of behaviour. I found some jQuery code here: Scrolling Overflowed DIVs with JavaScript
Now I don't know to get it to work. I pasted into application.js:
$("#chat_data").each( function()
{
var scrollHeight = Math.max(this.scrollHeight, this.clientHeight);
this.scrollTop = scrollHeight - this.clientHeight;
});
I've also added <%= javascript_include_tag 'jquery', 'application' %> to the head of my view.
But when my chatroom log fills up, the scrollbar appears but does not automatically move to the bottom as new messages come through.
The problem seems to be that the code you've inserted is only run once, at the beginning of the script.
I don't know a lot about jquery so this is just a general solution.
function sub(data) {
$('#chat_data').each( function () {
var s_top = this.scrollHeight - this.clientHeight;
var scl = this.scrollTop == s_top;
this.innerHTML += '<br/>' + data;
if ( scl ) this.scrollTop = s_top + this.clientHeight;
})
};
problem is that now when you receive new data from the server you must add it into the #chat_data by calling sub("text that goes into chat window").
you'll have to replace
page.insert_html ....
with something that sends the rjs to the client, like:
page.call( :sub, data)
hope it helps.
Related
Rails & Javascript beginner here,
On a training project, I made flash messages disappear after few seconds using JQuery. A visitor would send AJAX request to add a product to his cart, then a flash partial 'Added to cart' appears and automatically fades out after few seconds.
# application.html.erb
<div id='flash' class='flex-column'>
<%= render partial: 'shared/flash' %>
</div>
# shared/_flash.html.erb
<% flash.each do |key, value| %>
<%= display_flash(key, value) %>
<%= javascript_pack_tag 'custom/flash' %>
# this works, but injects the script each times the partial is rendered
<% end %>
# helpers/application_helper.rb
def display_flash(key, value)
def display_flash(key, value)
div_class = [flash_class(key), 'text-center fade show alert-dismissible'].join(' ')
content_tag(:div, class: div_class) do
content_tag(:p, value) +
button_tag(class: 'ml-auto close', 'data-dismiss': 'alert', type: 'button') do
content_tag(:span, '×'.html_safe, 'aria-hidden': 'true') +
content_tag(:span, 'Close', class: 'sr-only')
end
end
end
end
// app/javascript/packs/custom/flash.js
function closeFlash(){
let lastAlert = $('#flash .alert:last')
function fadeFlash() {
lastAlert.animate( {opacity: 0}, 2000, function() {
$(this).hide('slow', function() {
$(this).remove()
});
});
};
setTimeout(fadeFlash, 2000)
};
closeFlash();
The issue with this is that it pollutes my DOM with unnecessary <script> tags:
This could be fixed, but is there a suitable way to execute one specific javascript function after rendering a (AJAX) partial ?
In my case, executing closeFlash() located in packs/custom/flash.js each time a partial is rendered.
Thanks for your help and your time
EDIT Solution:
From Amit Patel answer and this post
# app/views/shared/_flash.html.erb
<% flash.each do |key, value| %>
<%= display_flash(key, value) %>
<script>
$(function() {
closeFlash();
});
</script>
<% end %>
// app/javascript/packs/custom/flash.js
window.closeFlash = function() {
let lastAlert = $('#flash .alert:last')
function fadeFlash() {
lastAlert.animate( {opacity: 0}, 2000, function() {
$(this).hide('slow', function() {
$(this).remove()
});
});
};
setTimeout(fadeFlash, 2000)
};
It doesn't inject the whole function but (I believe) the minimal javascript code to call it.
Move <%= javascript_pack_tag 'custom/flash' %> to your layout or your application.js` so that it is available across the app.
modify your template like
application.html.erb
<div id='flash' class='flex-column'>
<%= render partial: 'shared/flash' %>
<script>
$(function(){
closeFlash();
});
</script>
</div>
A more recent approach would be to set a Stimulus controller.
Not sure if I am asking this question wrong, but I can't seem to find exactly the issue I am faced with.
I have a very basic rails 6 app for a task list. Tasks are either complete or incomplete and the change/update of this is to be done via javascript (the html side works just fine).
Here is my form partial _task.html.erb:
<%= form_with model: task, html: { class: "edit_task", id: "edit_task_#{task.id}" } do |f| %>
<%= f.check_box :complete %>
<%= f.submit "Update" %>
<%= f.label :complete, task.name %>
<%= link_to "(remove)", task, method: :delete, data: {confirm: "Are you sure?"}, remote: true %>
<% end %>
Here is the javascript to submit the form, tasks.js
function submit_update(){
const checkboxes = document.querySelectorAll('.edit_task input[type=checkbox]');
const submitbutton = document.querySelectorAll('.edit_task input[type=submit]');
submitbutton.forEach(button => button.remove());
checkboxes.forEach(checkbox => {
checkbox.addEventListener('click', () => checkbox.parentElement.submit());
});
}
window.addEventListener('turbolinks:load', event => {
submit_update();
document.addEventListener('task_added', event => submit_update());
});
This part works just fine, but once submitted and based on this section of the controller
def update
#task.update!(task_params)
respond_to do |format|
format.html { redirect_to root_url, notice: 'Task successfully updated' }
format.js
end
end
My understanding is together this should launch update.js.erb, which currently looks like
unction update_task() {
const current_task = document.querySelector('#edit_task_<%= #task.id %>');
const complete_tasks = document.querySelector('#complete_tasks');
const incomplete_tasks = document.querySelector('#incomplete_tasks');
<% if #task.complete? %>
complete_tasks.insertAdjacentHTML('beforeend',current_task.innerHTML);
<% else %>
incomplete_tasks.insertAdjacentHTML('beforeend',current_task.innerHTML);
<% end %>
}
update_task();
I have tried changing the above to a single line using an alert call and it still never gets called.
If someone could let me know why update.js.erb is not being called, it would be much appreciated :)
If any additional information is required, please let me know?
EDIT:
On further testing, I have found that if I submit the update via the click of the button, ie remove the submission via javascript, that the update.js.erb is actioned correctly.
So it would seem the focus needs to be on the tasks.js file and how that submits?
What is weird is that when that is included, after the submit the HTML format runs just fine, just not the js format??
After a number of days of trying to get the correct search words I have found the solution and happy to submit for others to find :)
The magic can be found here
The change for my example is as follows --
checkboxes.forEach(checkbox => {
checkbox.addEventListener('click', () => checkbox.parentElement.submit());
});
// becomes
checkboxes.forEach(checkbox => {
checkbox.addEventListener('click', event => {
checkbox.parentElement.dispatchEvent(new Event('submit', {bubbles: true}));
});
});
I have since edited the update.js.erb (does the same thing just a bit cleaner), but it also
needed an additional entry for newly added items to trigger having the entry change positions:
$ cat app/views/tasks/update.js.erb
function update_task() {
const current_task = document.querySelector('#edit_task_<%= #task.id %>');
const to_task = document.querySelector('#<%= #task.complete? ? "complete_tasks" : "incomplete_tasks" %>');
// Remove from previous list
current_task.remove();
// Append to the end of new list
to_task.insertAdjacentHTML('beforeend', '<%= j render(#task) %>');
// Advise tasks.js that new entry needs listener added
to_task.dispatchEvent(new Event('task_added', { bubbles: true }));
}
update_task();
Hope others find this useful :)
I've noticed how Akira Matsuda have added helper in Kaminari gem and made ti a lot simple for us to use Load more button.
I've read the document and figured simply adding
<%= link_to_next_page #items, 'Next Page' %>
should make the ajax load button work, but id didn't.
So I googled few other articles for some help and I wrote
few extra things...
Here are what I have right now.
I have these files
root.html.slim
ul#works
= render :partial => "works"
work.html.slim
- #works.each do | work |
= link_to work
li
span.thumb
= work_image_of work
h4 = link_to work.title, work, thumb:true
p.pull-left
small
= work.collaborators.count.to_s
| collaborators
= link_to_next_page #works, 'Next Page', id: 'view_more'
index.controller
def root
#works = Work.page(params[:page]).per(9)
render :layout => 'application'
end
also added in application.js
$('#works').append("<%= escape_javascript(render 'works', object: #works) %>");
$("#view_more").replaceWith("<%= escape_javascript(
link_to_next_page(#works, 'more', remote: true, id: 'view_more')
) %>");
and this in action helper,
def link_to_next_page(scope, name, options = {}, &block)
param_name = options.delete(:param_name) || Kaminari.config.param_name
link_to_unless scope.last_page?, name, {param_name => (scope.current_page + 1)}, options.merge(:rel => 'next') do
block.call if block
end
end
I have jquery included in my js files.
What am I missing?
Please help me out!
Thank you for your time!!
Basically I already have a piece of coffeescript that animates a drop down menu:
menu_in = -> $('.cart_pulldown').slideDown(250)
menu_out = -> $('.cart_pulldown').slideU(150)
$('#store_menu').hoverIntent(over: menu_in, out: menu_out, timeout: 150)
And I want to tie this to the add-to-cart-button action so that the menu slideDown/slideUp sequence happens when a user adds to their cart, heres that js code:
function set_product_page_variant_state() {
var varel = $('div#product-social-links');
var cart_link = $("#add-to-cart-button");
if(varel && cart_link) {
if(variant_id = varel.attr('data-variant-id')) {
$.post('/flash_sales/get_state.json', {'variant_ids[]': [variant_id]}, function(data) {
var state = data[variant_id];
if(state) {
if(state=='on_hold') {
cart_link.text("On Hold").show();
} else if(state=='in_my_cart') {
// TODO: this is funking ugly and slow to load, this whole thing needs a good old fashion refactorin'.
cart_link.text("In My Cart")
.hide()
.after("<a href='/cart' class='action-button add_to_cart' id='#add-to-cart-button'>In My Cart</a>")
.remove()
} else if(state=='available') {
cart_link.removeAttr('disabled').show();
} else if(state=='sold_out') {
cart_link.text("Sold Out").show()
} else {
// something went wrong, enable the button
cart_link.removeAttr('disabled').show()
}
} else { cart_link.removeAttr('disabled').show() }
});
} else { cart_link.removeAttr('disabled').show() }
}
}
and just to be thorough, here is the associated html:
<div id="cart-form">
<%= form_for :order, :url => spree.populate_orders_url do |f| %>
<div id="inside-product-cart-form" itemprop="offers" itemscope itemtype="http://schema.org/Offer">
<% if #product.price %>
<div>
<div class="add-to-cart">
<%= hidden_field_tag "variants[#{#variant.id}]", 1 %>
<%= button_tag "Add to Cart", :class => "hidden action-button add_to_cart", :type => :submit, :disabled => true, :id => "add-to-cart-button" %>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
Any advice is greatly appreciated, thanks in advance!
You can use jQuery delegate events in your Coffeescript file. For example, to show the menu for 500ms before triggering menu_out:
$(document).on 'click', '#add-to-cart-button', (event) ->
menu_in()
setTimeout 500, menu_out
Since CoffeeScript puts your code in a closure you need to manually attach global variables to the window, like window.menu_in = -> ...
I have a form element which is being pulled in on request with ajax. I am then trying to perform an ajax request on the inserted text box to find a location as it is typed. The code works on the first textbox but simply fails when the second one is inserted. I've tried to get the script to reload itself when the ajax has completed but it still won't work. Help would be much appreciated.
Form.html.erb - Sets up the rails nested form and pulls in partial
<%= nested_form_for(#post, :html=> {:multipart => true, :class=> "new_blog_post", :id=> "new_blog_post"}) do |f| %>
...
<fieldset>
<%= render :partial => 'search_locations', :locals => { :f => f } %>
</fieldset>
<p><%= f.link_to_add "Add a location", :locations %></p>
...
<% end %>
partial.html.erb - Pulled in on page load and then when 'Add a location' button is pressed
<fieldset>
<%= f.fields_for :locations do |m| %>
<%= m.text_field :name ,:class=>"localename", :placeholder=> "Name of the location", :autocomplete => "off" %>
<%= m.text_field :longitude, :class => "long" %>
<%= m.text_field :latitude, :class => "lat" %>
<div class="latlong">
<p class="help-block">Enter the name of the town or city visited in this blog entry.</p>
</div>
<%= m.link_to_remove "Remove this location" %>
<% end %>
</fieldset>
Javascript (placed at bottom of form)
<script type="text/javascript">
function locationchecker() {
// Rails to multiply the script 20 times
<% (0..20).each do |i| %>
// when the #search field changes
$(".localename:eq(<%=i%>)").keyup(function() {
// get the value of searchfield
var location<%=i%> = $(".localename:eq(<%=i%>)").val();
//Take the value of the textbox and pull it from geocoder
$.get('/locations/location?location='+location<%=i%>,this.value, function(searchone<%=i%>) {
$(".latlong:eq(<%=i%>)").html(searchone<%=i%>);
})
// Upon complete run the script again
.complete(function(searchone<%=i%>) { locationchecker });
});
<%end%>
}
// load script on doc ready
$(document).ready(locationchecker);
</script>
Help would be great!
Thanks in advance,
James
you should use the .on()(jQuery api doc) method to attach your keyup event handler, like so :
$('#myForm').on('keyup','.localename',function() {
// stuff here
var location = $(this).val(); // this = element on which the event was triggered
});