Typing indicator with action cable in rails- 7 - ruby-on-rails

I am unable to add typing indicator in my rails app with action cable I have created app in rails 7 and I user trubo stream tag and broadcast in it so I did't used channel for live chat , I tried to find tutorial and video but there is not any
I want to add typing indicator so I writtern js for the same on input it will called and it will go to controller
On input I am calling controller "rtm"
room controller
def rtm
#typing = "hhhhhhhhhhhhhhhhhhh"
# ActionCable.server.broadcast "typing_channel",{ message: "helloo"}
# #typings.broadcast_append_to "typing"
Turbo::StreamsChannel.broadcast_append_to "typing", target: 'typing', partial: 'rooms/typing', locals: { message: "#typing" }
end
here I have issue how can I broadcast the typing message to my room page
Room.rb
class Room < ApplicationRecord
scope :public_rooms, -> { where(is_private: false) }
has_many :messages
after_create_commit {broadcast_append_to "rooms"}
end
message.rb
class Message < ApplicationRecord
belongs_to :user
belongs_to :room
after_create_commit { broadcast_append_to self.room }
end
rooms/index
<script>
$(document).ready(function(){
var tmo = null;
$("#msg").on("input", function(){
$.ajax({
type: 'GET',
url: '/rooms/rtm',
data: {data: ''}
});
document.getElementById("typing").innerHTML = "Typing...";
if (tmo) {
clearTimeout(tmo);
}
tmo = setTimeout(function () {
$.ajax({
type: 'GET',
url: '/rooms/rmm',
data: {data: ''}
});
document.getElementById("typing").innerHTML = "";
}, 1000);
});
});
</script>
<div class="container">
<h5> Hi <%= current_user&.firstname %> </h5>
<%= debug(params) if Rails.env.development? %>
<br> <h4> Rooms </h4>
<%= render partial: 'layouts/new_room_form' %>
<%= turbo_stream_from "rooms" %>
<div id="rooms">
<%= render #rooms %>
</div>
</div>
<% if #single_room.present? %>
<%= link_to #single_room.name,#single_room, class: "btn btn-primary" %>
<%= turbo_stream_from #single_room %>
<div id="messages">
<%= render #messages %>
</div>
<%= render partial: 'layouts/new_message_form' %>
<%= #typing %>
<%= turbo_stream_from #typing %>
<div id="typing">
</div>
<%= render partial: 'rooms/typing' %>
<span id="typing"></span><br>
<% end %>

To get the typing indicator you need to use action cable and create a channel for it. You can use turbo stream to render the typing indicator. Example:
app/channels/typing_channel.rb
class TypingChannel < ApplicationCable::Channel
def subscribed
stream_from "typing_channel"
end
end
app/javascript/channels/typing_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("TypingChannel", {
connected() {
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
// Called when there's incoming data on the websocket for this channel
}
});
app/views/rooms/index.html.erb
<div id="typing">
<%= turbo_stream_from "typing_channel" %>
</div>
app/views/rooms/_typing.html.erb
<p><%= message %></p>
app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
def rtm
ActionCable.server.broadcast "typing_channel", { message: "helloo" }
end
end
app/javascript/controllers/rooms_controller.js
import { Controller } from "stimulus"
import consumer from "../channels/consumer"
export default class extends Controller {
static targets = [ "input" ]
connect() {
this.subscription = consumer.subscriptions.create("TypingChannel", {
received: (data) => {
this.renderTyping(data)
}
})
}
renderTyping(data) {
const typing = document.getElementById("typing")
typing.innerHTML = data.message
}
disconnect() {
this.subscription.unsubscribe()
}
}
Is not possible to use turbo stream with action cable. You need to use action cable to get the typing indicator. You can use turbo stream to render the typing indicator.

typing_channel.js
import consumer from "channels/consumer"
consumer.subscriptions.create("TypingChannel", {
connected() {
console.log("connected")
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
console.log(data)
const box = document.getElementById('typing');
if (box.textContent.includes(data.body)) {
} else {
$('#typing').append('<p>'+ data.body + data.message +'</p>');
}
}
});
I am using below js for indicator
<script>
$(document).ready(function(){
var tmo = null;
$("#chat").on("input", function(){
$.ajax({
type: 'GET',
url: '/rooms/rtm',
});
if (tmo) {
clearTimeout(tmo);
}
tmo = setTimeout(function () {
$.ajax({
type: 'GET',
url: '/rooms/rmm',
});
}, 1000);
});
});
</script>
in controller
ActionCable.server.broadcast "typing_channel", {message: 'Typing', body: "#{current_user.email}"}

Related

Error with Rails Sortable - Cannot read properties of undefined (reading 'dataset')

I followed this tutorial to implement drag and drop. https://www.driftingruby.com/episodes/drag-and-drop-with-hotwire
I am getting an error on drop that says Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'dataset'). Any thoughts on why this is happening?
Project model:
acts_as_list scope: :project
_project.html.erb
<div id="projects" data-controller="position">
<%= content_tag :div, data: { sgid: project.to_sgid_param, id: "#{dom_id project}" }, class: 'table-row' do %> <div><%= project.name %> </div>
<div><%= project.description %> </div>
<div><%= project.status %> </div>
<div><%= project.user.email %> </div>
<div class="flex justify-end">
<%= link_to "View", project_path(project) , data: {turbo: false} %>
<%= button_to "Delete", project, method: :delete, form: {data: {turbo_confirm: "Are you sure?"}} %> </div>
</div>
<% end %>
</div>
position_controller.js
import { Controller } from "#hotwired/stimulus";
import Sortable from "sortablejs"
import { put, get, post, patch, destroy } from "#rails/request.js"
export default class extends Controller {
connect() {
this.sortable = Sortable.create(this.element, {
animation: 150,
onEnd: this.updatePosition.bind(this)
})
}
async updatePosition(event) {
const response = await put('/projects', {
body: JSON.stringify({
sgid: event.project.dataset.sgid,
position: event.newIndex + 1
})
})
if (response.ok) {
console.log(event.project.dataset.sgid)
}
}
}
well event doesn't have a method project. You may have meant event.target. Anyway it looks as if the target of the event is probably the #projects element (but you need to check this in dev tools and see where the event is being captured).
Let's suppose that the event is being captured by the #projects element, well that element has an empty dataList. So wherever the capture is occurring, you need to put data: {sgid: project.to_sgid_param} on that element.
Finally, you should be able to populate the ajax request with the value sgid: event.target.dataset.sgid.

Rails | Ajax Response, from Controller

Got a partial view successfully interacting with my coffee script. Collection_Select change triggers script & resulting value is correct, Controller does hit def new successfully.
Only question remaining is how to access the results of the coffee script in the controller.
Partial View:
<% #modName = locals[:moduleName] %>
<% #id = locals[:id] %>
<%= form_with url: admin_command_path() do |f| %>
<%= collection_select(:refcode, :Code, Command.where(FLD: #modName), :Code, :Definition, options ={prompt: true}) %>
<br /><br />
<button class="btn_new">
<%= link_to "Execute", new_admin_command_path(mod: #modName, id: #id) %>
</button>
<% end %>
Coffee Script:
get_SubModule = ->
$('#refcode_Code').change (e) ->
com_value = $('#refcode_Code').val()
console.log 'COFFEEE IS LIFE', com_value
str = $('#refcode_Code :selected').text()
data: {sub_mod_str: com_value}
return
return
So now what.
ActiveAdmin.register Command do
def new
[need to access sub_mod_str here however possible]
end
I think when you Change value you need call Ajax and request controller
get_SubModule = ->
$('#refcode_Code').change (e) ->
com_value = $('#refcode_Code').val()
console.log 'COFFEEE IS LIFE', com_value
str = $('#refcode_Code :selected').text()
data: {sub_mod_str: com_value}
$.ajax(
type: 'POST'
url: your_url
data: data
dataType: 'json'
success: (data) =>
console.log(data)
error: (er) =>
console.log(er)
)
return
return
Controller
def new
byebug // check params
end

Ruby on Rails 6 Turbolinks Stripe form values not persisting on back

I am trying to build a form that allows a user to make a booking for a martial arts class they wish to attend. I have created a form that dynamically changes based on the selections the user makes, when I change any of the select options the form updates and when I submit the form it redirects to a Stripe checkout. The problem I have is after submitting the the form and I click the browsers back button or the back button provided on the Stripe checkout page the select options that have been updated have reverted back to the default options rather than the updated options. Can anyone help me correct this behaviour and get the correct form elements to persist?
Here is the code I am using to do this:
The view I am rendering the form in:
<% content_for :banner_title, #page_data['bannerTitle'] %>
<% content_for :head do %>
<meta name="turbolinks-cache-control" content="no-cache">
<% end %>
<div class="content container py-5">
<div class="row">
<div class="col-12 col-md-7 mx-auto">
<%= render "forms/booking", options: #options %>
</div>
</div>
</div>
The form I am using:
<%= form_for #booking, class: 'booking clearfix' do |f| %>
<div class="form-group">
<%= f.label(:class_name, "Select a class you wish to attend: ") %>
<%= f.select(:class_name, options_for_select(options[:class_names], #booking.class_name), {}, class: 'form-control' ) %>
</div>
<div class="form-group">
<%= f.label(:date, "Select a date:") %>
<%= f.select(:date, options_for_select( options[:dates], #booking.date ), {}, class: 'form-control' ) %>
</div>
<div class="form-group">
<%= f.label(:time, "Select a time: ")%>
<%= f.select(:time, options_for_select(options[:times], #booking.time), {}, class: 'form-control') %>
</div>
<div class="form-group">
<%= f.label(:attendees, "How many attending: ") %>
<%= f.select(:attendees, options_for_select(options[:attendees], #booking.attendees), {}, class: 'form-control' )%>
</div>
<%= f.submit 'Place Booking', class: 'btn btn-primary btn-lg text-light float-right', id: 'create-booking' %>
<% end %>
<%= javascript_pack_tag 'booking_form' %>
<script src="https://js.stripe.com/v3/"></script>
The model for the form (I'm not using ActiveRecord, i dont know if this makes any difference?):
class Booking
include ActiveModel::Model
MAX_ATTENDEES = 10
attr_accessor :time, :class_data, :attendees, :date, :class_name
def initialize(args={})
#time = args['time']
#class_name = args['class_name']
#class_data = args['class_data']
#date = args['date']
#attendees = args['attendees']
end
def day
#date.split(',').first
end
def available_dates
days_index_array = class_data['times'].keys.map {|k| day_index(k) }
days_within( days_index_array )
end
def available_times
if !date
class_data['times'][class_data['times'].keys.first.downcase]
else
class_data['times'][day.downcase]
end
end
def total_cost
#class_data['cost'].to_i * #attendees.to_i
end
def attending_string
ActionController::Base.helpers.pluralize(attendees, 'person')
end
private
def days_within(days, timeframe=1.month)
start_date = Date.tomorrow
end_date = start_date + timeframe
(start_date..end_date).to_a.select {|k| days.include?(k.wday) }
end
def day_index(day)
DateTime::DAYNAMES.index(day.to_s.capitalize)
end
end
And the controller I am calling the new action in:
class BookingsController < ApplicationController
include BookingsHelper
before_action :set_class_data
skip_before_action :set_page_data, except: :new
def new
set_booking
# store values to be passed to the form helper method options_for_select. Each value must be an array populated with arrays with the format [value, text]
#options = {
class_names: #class_data.map {|c| [ c['name'], c['name'] ]},
dates: #booking.available_dates.map {|d| [d.strftime('%A, %d %B'), d.strftime('%A, %d %B')] },
times: #booking.available_times.map {|t| [t,t]},
attendees: Booking::MAX_ATTENDEES.times.map {|i| [i+1, i+1]}
}
end
def create
end
def booking_form_data
booking_form_data = set_booking_form_data(params)
update_session_booking(booking_form_data)
render json: booking_form_data
end
private
def set_booking
if session[:current_booking]
pp "session exists"
#booking = Booking.new(session[:current_booking])
else
pp "session does not exist"
#booking = Booking.new
session[:current_booking] = #booking.instance_values
end
set_booking_class_data
end
def set_booking_class_data
!#booking.class_name ? #booking.class_data = #class_data.first.except('information') : #booking.class_data = #class_data.find {|cd| cd['name'] == #booking.class_name}.except('information')
end
def booking_params
params.permit(:class_name, :date, :time, :attendees, :update_type)
end
def update_session_booking(booking_form_data)
if params[:update_type] == 'class_name'
session[:current_booking]['class_name'] = params[:class_name]
session[:current_booking]['date'] = booking_form_data[:date_options].first
session[:current_booking]['time'] = booking_form_data[:time_options].first
elsif params[:update_type] == 'date'
session[:current_booking]['date'] = params[:date]
session[:current_booking]['time'] = booking_form_data[:time_options].first
elsif params[:update_type] == 'time'
session[:current_booking]['time'] = params['time']
elsif params[:update_type] == 'attendees'
session[:current_booking]['attendees'] = params[:attendees]
elsif params[:update_type] == 'load'
session[:current_booking] = booking_params.except(:update_type)
end
pp "Session Booking: #{session[:current_booking]}"
end
def set_booking_form_data(params)
booking_form_data = {}
selected_class = #class_data.find {|cd| cd['name'] == params[:class_name] }
# when the class_name select is changed
if params[:update_type] == 'class_name'
booking_form_data[:date_options] = days_within( selected_class['times'].keys.map {|k| day_index(k) } ).map {|d| d.strftime('%A, %d %B') }
booking_form_data[:time_options] = selected_class['times'][booking_form_data[:date_options].first.split(',')[0].downcase]
# when date select is changed
elsif params[:update_type] == 'date'
booking_form_data[:time_options] = selected_class['times'][params[:date].split(',')[0].downcase]
end
booking_form_data
end
end
And the javascript I am using to update the form:
getBookingFormData = (bodyData={}, successCallback=()=>{}) => {
$.ajax({
url: '/booking_form_data',
method: 'POST',
beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
data: bodyData,
success: successCallback
})
}
createOptions = (values) => {
let newOptions = [];
$.each(values, (index, value) => {
let newOption = $('<option></option>');
newOption.attr('value', value);
newOption.text(value);
newOptions.push(newOption)
})
return newOptions
}
appendOptions = (options, element) => {
$(element).empty();
$(element).append(options)
}
currentFormValues = () => {
return {
class_name: $('#booking_class_name').val(),
date: $('#booking_date').val(),
time: $('#booking_time').val(),
attendees: $('#booking_attendees').val()
}
}
$('select#booking_class_name').on('change', () => {
let bodyData = {
class_name: $('select#booking_class_name').val(),
update_type: 'class_name'
}
let successCallback = (res) => {
let dateOptions = createOptions(res.date_options);
let dateSelect = $('select#booking_date');
let timeOptions = createOptions(res.time_options);
let timeSelect = $('select#booking_time');
appendOptions(dateOptions, dateSelect);
appendOptions(timeOptions, timeSelect);
}
getBookingFormData(bodyData, successCallback)
});
$('select#booking_date').on('change', () => {
let bodyData = {
class_name: $('select#booking_class_name').val(),
date: $('select#booking_date').val(),
update_type: 'date'
};
let successCallback = (res) => {
let timeOptions = createOptions(res.time_options);
let timeSelect = $('select#booking_time');
appendOptions(timeOptions, timeSelect);
}
getBookingFormData(bodyData, successCallback)
});
$('select#booking_time').on('change', () => {
let bodyData = {
time: $('select#booking_time').val(),
update_type: 'time'
};
getBookingFormData(bodyData);
});
$('select#booking_attendees').on('change', () => {
let bodyData = {
attendees: $('select#booking_attendees').val(),
update_type: 'attendees'
};
getBookingFormData(bodyData);
});
$('#create-booking').on('click',(e) => {
e.preventDefault();
bookingDefault = false
const stripe = Stripe(process.env.STRIPE_PUBLIC);
let requestHeaders = new Headers({
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
'Content-Type': 'application/json'
})
fetch('/create_checkout_session', {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(currentFormValues())
})
.then((res) => { return res.json() })
.then((session) => { return stripe.redirectToCheckout({ sessionId: session.id }) })
.then((result) => {
if (result.error) { alert(result.error.message) }
})
.catch((error) => { console.error('Error: ', error) })
})
From what Ive read i think it may be a problem related to caching which makes me think this is an issue with turbolinks but I could be completely wrong. Ive tried adding meta tags that disable turbolinks or force it to reload the page but they did not seem to work.
Any input at all would be really appreciated as Ive been stuck on this for days. Let me know if you need any more information
This isn't so much Stripe-related as it is related you your form value management. If you want to keep these values around, you'll need to build that into your front application, somehow. There are lots of options for this:
Using local storage
Using query parameters, if not sensitive info
Using a cookie and a server session you can re-retrieve and hydrate the f.select options with a default value.

Rails 5 Subscribing to Action Cable channel from ajax request

I've been trying to show all conversation messages without refreshing page just with an ajax request. With this code i'm able to send message with channel but i'm not able to receive the message.
I would like to pass dynamically ID from frontend. I know it's possible to make that with a data parameter added into the body tag (like this).
I would like make the same but when i click on a conversation that change body data "conversation-id" and subscribe to the new channel id. I hope it's possible to make that with rails 5/jquery(ajax).
Here's my code :
conversation.coffee
App.conversation = App.cable.subscriptions.create channel: "ConversationChannel", id: document.querySelector('#conversation').dataset.conversationId,
connected: ->
$('#new_message').submit (e) ->
$this = $(this)
id = $this.find('#message_conversation_id');
body = $this.find('#message_body')
App.conversation.write id.val(), body.val()
body.val('')
e.preventDefault()
return false
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
$('.box-msg').append(data.data)
write: (id, body) ->
#perform 'write', {id: id, body: body}
conversation_channel.rb
class ConversationChannel < ApplicationCable::Channel
def subscribed
id = "[HOW CAN I PASS DYNAMICALLY ID]"
if id #on stream seulement si id existe
stream_from "conversation_#{id}"
end
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def write(data)
msg = Message.create(
conversation_id: data["id"],
body: data['body'],
user_id: 41,
read: 0
)
if msg.save
html = ApplicationController.render(partial: 'messages/message', locals: { message: msg})
end
ActionCable.server.broadcast "conversation_#{data["id"]}", data: html
end
end
conversations/index.html.erb
<script>
$(document).ready(function() {
$('.card-conversation').click(function() {
$("#conversation-content").html('');
var id = $(this).data("conversation");
$("body").attr("data-conversation-id", id);
$("#message_conversation_id").val(id); //input id
$.ajax({
url: "/conversations/"+id+"/messages",
cache: false,
success: function(html){
$("#conversation-content").append(html);
}
});
$("#conversation-content").fadeIn();
});
});
</script>
<div class="row h-100 no-margin">
<div class="col-md-4 h-100 messages-list" style="padding-right: 2px !important;">
<% #conversations.first(3).each do |conversation| %>
<div class="card card-conversation" data-conversation="<%=conversation.id %>">
<p>see conversation</p>
</div>
<% end %>
</div>
<div class="col-md-8 conversation no-padding" id="conversation">
<div id="conversation-content"></div>
<%= simple_form_for Message.new, url: "#" do |f| %>
<%= f.input :body, label: false, placeholder: "Add msg" %>
<%= f.input :conversation_id, label: false %>
<%= f.submit 'Go' %>
<% end %>
</div>
</div>

Passing Ajax Parameters to Rails Controller

I am trying to pass some parameters from my view via an AJAX call to a custom method fetch_info in my photos controller. My controller does not seem to be receiving the parameters. When I click on an image to initiate the AJAX call, I see the following in my terminal:
Processing by PhotosController#fetch_info as JSON
Parameters: {"id"=>"", "secret"=>""}
Completed 500 Internal Server Error in 267ms
FlickRaw::FailedResponse ('flickr.photos.getInfo' - Photo not found):
app/controllers/photos_controller.rb:38:in `fetch_info'
It looks like the fetch_info method is being called, but the parameters are empty. How should I be passing in my parameters through AJAX?
Here is my view. I also have my javascript in the view for the purpose of just getting this to work.
index.html.erb
<div class="body_container">
<div id="photos_container">
<% #photos_array.each do |p| %>
<%= link_to '#' do %>
<div class='each_photo_container', id='<%="#{p[:id]}"%>' >
<%= image_tag p[:s_url] %>
</div>
<% end %>
<!-- Load Modal onClick -->
<script type="text/javascript">
jQuery(function() {
$('#<%=p[:id]%>').click(function (e) {
//ajax call to fetch photo info
var fetch_id = '<%=p[:id]%>';
var fetch_secret = '<%=p[:secret]%>';
$.ajax({
type: 'GET',
url: '/photos/fetch_info',
dataType: 'json',
data: { 'id' : fetch_id.val(), 'secret' : fetch_secret.val() }
});
return false;
});
});
</script>
<% end %>
<div class="basic_modal">
</div>
</div>
</div>
Here is my photos_controller.rb:
def fetch_info
puts params[:id]
puts params[:secret]
info = flickr.photos.getInfo(:photo_id => params[:id], :secret=> params[:secret])
end
You can use this code:
$('##{p[:id]}').click(function (e) {
//ajax call to fetch photo info
var fetch_id = '#{p[:id]}';
var fetch_secret = '#{p[:secret]}';
$.ajax({
type: 'GET',
url: '/photos/fetch_info',
dataType: 'json',
data: { 'id' : fetch_id, 'secret' : fetch_secret }
});
return false;
})

Resources