rails remove element in real time - javascript

Got a link that removes a message from the current_users screen:
= link_to '[x]', msg, method: :delete, remote: true, class: "del-link"
It triggers this coffeescript-funktion:
delete_message = () ->
$('.del-link').on 'ajax:success', (event) ->
event.preventDefault()
$(this).closest('.message').remove()
return
and this rails-method:
def destroy
#msg = Msg.find(params[:id])
#msg.destroy
respond_to do |format|
format.html { redirect_to chat_path }
format.json { head :no_content }
format.js { render :layout => false }
end
end
But how would it be done, that the message is removed on every users screen, e.g. using ActionCable?
The coffeescript for the chat:
App.proom = App.cable.subscriptions.create "ProomChannel",
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) ->
unless data.msg.blank?
$('#messages-table').append data.msg
scroll_bottom()
$(document).on "turbolinks:load", ->
submit_msg()
scroll_bottom()
delete_message()
the channel.rb
class ProomChannel < ApplicationCable::Channel
def subscribed
stream_from "proom_channel"
end
def unsubscribed
end
end
Approach
So I added this to the destroy-action of the controller
if msg.destroy
ActionCable.server.broadcast 'proom_channel', msg: #msg
end
Also I added an ID to every message-div-element containing the id of the message-record to find and remove it from the DOM with the following line
$("msg_#{msg.id}").remove()
But now I don't know where to put it.

I am not able to give you right now the complete answer, but I will update my answer later if you do not find a solution:
Like when you create a message with action cable, after saving the message in the DB, you trigger an ActionCable.server.broadcast 'messages' that will execute some JS included for example in the User Browser asset pipeline file app/assets/javascripts/channels/messages.js
This is my message controller, once I save the message, I start the Server.broadcast process. It will trigger the process in my messages_channel.rb that will update all clients that are subscribed to that channel.
def create
message = Message.new(message_params)
message.user = current_user 
chatroom = message.chatroom
if message.save
ActionCable.server.broadcast 'messages',
message: message.content,
user: message.user.name,
chatroom_id: message.chatroom_id,
lastuser: chatroom.messages.last(2)[0].user.name
head :ok
end
end
Image and text is from the following article of Sophie DeBenedetto
We add our new subscription to our consumer with
App.cable.subscriptions.create. We give this function an argument of
the name of the channel to which we want to subscribe,
MessagesChannel.
When this subscriptions.create function is invoked, it will invoke the
MessagesChannel#subscribed method, which is in fact a callback method.
MessagesChannel#subscribed streams from our messages broadcast,
sending along any new messages as JSON to the client-side subscription
function.
Then, the received function is invoked, with an argument of this new
message JSON. The received function in turn calls a helper function
that we have defined, renderMessage, which simply appends new messages
to the DOM, using the $("#messages") jQuery selector, which can be
found on the chatroom show page.
The channel will call the messages.js function received, that will append the div to the DOM, you will need to call an alternative action in messages.js.
App.messages = App.cable.subscriptions.create('MessagesChannel', {
received: function(data) {
// code executed - the parameters will be in data.chatroom_id and
// data.user, data.message etc...
}
});
So you will need to call the channel from your action messages#destroy, have in messages.js a specific function to remove the div from the DOM.
I don't know if you will need to create a specific server side channel or you can just edit your present messages_channel.rb to include a specific function.. You need to read the guides and figure this out.. I may try this in the future, but right now I can't
Or an easy alternative, just write some js in messages.js to solve this and have the div removed when required.. for example the messages#destroy action can pass a parameter and if that parameters is present, you will remove the message, instead of adding it
https://github.com/rails/rails/tree/master/actioncable#channel-example-1-user-appearances

Related

How to keep a connection with a channel that uses paramaters passed from client to server for ActionCable?

I am not getting my head around actioncable.I want to allow users to subscribe to a PostChannel which looks like this,
class PostNotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from params[:post_id]
end
def unsubscribed
stop_all_streams
end
end
Then I have the js
/javascripts/channels/post_notifications.js
$(document).on('turbolinks:load', function() {
$("a.post_subscribe").click(function subscribeToPost() {
var post_id = $(this).parents("li").attr("class").split(" ")[0]; // Get the post id off the page
App.post_notifications = App.cable.subscriptions.create({channel: "PostNotificationsChannel", post_id: post_id}, {
connected: function() {},
disconnected: function() {},
received: function(data) {}
});
});
});
So you click on the .post_subscribe anchor tag and it gets the value of that posts HTML class which I have on the page, then use that in the params hash of the subscription creation. This is all fine and dandy and the user who clicks this will get updates when some action happens on that post. But if you refresh the page then that connection cannot reconnect that consumer with that post as there is no parameters in the hash. So, I am wondering how do I get out of this.
Also, even on the rails guides page it shows this here So they are passing params over but how do you keep the connection when the page reloads?
So what I understand from your question is, you want the users to be a follower of that post when a user clicks on "subscribe" link. For this purpose, you will have to store this information in the database by having an association between User and Post as a Follower.
Then whenever an activity happens on that post, broadcast a notification to the notifications stream of all the followers of that specific post.
Create a NotificationsChannel to which each user will be subscribed to whenever he logs in to the site, so for example a User with id 1, logs in, he will be subscribed to a notifications stream which will be unique to each user e.g. notify_user_1_channel
class NotificationsChannel < ApplicationCable::Channel
def subscribed
if params[:recepient].present?
stream_from "notify_#{params[:recepient]}_channel"
end
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
In your script for channel,
App.chat = App.cable.subscriptions.create {
channel: "NotificationsChannel"
recepient: $('#recepient').data("recepient")
}
And for giving the recepient as a parameter, in view code somewhere, preferably in footer,
<div id="recepient" data-recepient="User_<%= current_user.id %>"></div>
And, for broadcasting the notification to a stream, in after_create_commit of notification model,
ActionCable.server.broadcast "notify_#{notification.recepient_type}_#{notification.recepient_id}_channel"

How to trigger rails post method from faye.ru file (using faye)

so i am using faye pub-sub in my application, publish is happening from different application, in my faye.js i have written ajax post method for rails. now if 5 pages of my application is opened in browser, faye.js is loaded 5 times and post method is called 5 times. if not a single page is opened, post method wont work even once. however i am receiving published data in faye server. so is there a way of calling rails post method in faye.ru file when i use callback method.
This is my faye.ru
require 'faye'
require File.expand_path('../config/initializers/faye_token.rb', __FILE__)
Faye::WebSocket.load_adapter('thin')
Faye.logger = Logger.new(STDOUT)
class ServerAuth
def incoming(message, callback)
if message['channel'] !~ %r{^/meta/}
if message['ext']['auth_token'] != ENV['FAYE_TOKEN']
message['error'] = 'Invalid authentication token'
end
end
callback.call(message)
end
# IMPORTANT: clear out the auth token so it is not leaked to the client
def outgoing(message, callback)
if message['ext'] && message['ext']['auth_token']
message['ext'] = {}
end
callback.call(message)
end
end
$bayeux = Faye::RackAdapter.new(:mount => '/faye', :timeout => 25)
$bayeux.add_extension(ServerAuth.new)
run $bayeux
and my faye.js
$(function() {
var faye = new Faye.Client('http://localhost:9292/faye');
faye.subscribe("/functional", function(data) {
ajax_call(data);
});
});
You can use uri and http in your faye.ru
require "uri"
require "net/http"
params = {"param1":"param2"}
x = Net::HTTP.post_form(URI.parse('http://your-post url'), params)
puts x.body

Implementing notification/alert popup on job completion in Ruby on Rails

I am implementing background processing jobs in Rails using 'Sidekiq' gem which are run when a user clicks on a button. Since the jobs are run asynchronously, rails replies back instantly.
I want to add in a functionality or a callback to trigger a JavaScript popup to display that the job processing has finished.
Controller Snippet:
def exec_job
JobWorker.perform_async(#job_id,#user_id)
respond_to do |wants|
wants.html { }
wants.js { render 'summary.js.haml' }
end
end
Edit 1:
I am storing the 'user_id' to keep a track of the user who triggered the job. So that I can relate the popup to this user.
Edit 2:
The 'perform' method of Sidekiq does some database manipulation(Update mostly) and log creation, which takes time.
Currently, the user gets to know about the status when he refreshes the page later.
Edit 3(Solution Attempt 1):
I tried implementing 'Faye Push Notification' by using subscribe after successful login of user(with channel as user_id).
On the server side, when the job completes execution, I created another client to publish a message to the same channel (Using faye reference documents).
It is working fine on my desktop, i.e., I can see an alert popup prompting that the job has completed. But when I try testing using another machine on my local network, the alert is not prompted.
Client Side Script:
(function() {
var faye = new Faye.Client('http://Server_IP:9292/faye');
var public_subscription = faye.subscribe("/users/#{current_user.id}", function(data) {
alert(data);
});
});
Server Side Code:
EM.run {
client = Faye::Client.new('http://localhost:9292/faye')
publication = client.publish("/users/#{user.id}", 'Execution completed!')
publication.callback do
logger.info("Message sent to channel '/users/#{user.id}'")
end
publication.errback do |error|
logger.info('There was a problem: ' + error.message)
end
}
Rails 4 introduced the ActionController::Live module. This allows for pushing SSEs to the browser. If you want to implement this based on a database update you will need to look into configuring Postgresql to LISTEN and NOTIFY.
class MyController < ActionController::Base
include ActionController::Live
def stream
response.headers['Content-Type'] = 'text/event-stream'
100.times {
response.stream.write "hello world\n"
sleep 1
}
ensure
response.stream.close
end
end
Here is a good article on it: http://ngauthier.com/2013/02/rails-4-sse-notify-listen.html
Thanks fmendez.
I looked at the suggestions given by other users and finally, I have implemented a 'Faye' based push notification by:
Subscribing the user on successful login to a channel created using 'user_id'
Replying to this channel from server side after job completion (By fetching user_id)
For better understanding(so that it may be helpful for others), check the edit to the question.

Ember.js: Save record to Ember.Data, wait for response before displaying

I'm building an app which allows users to post to Twitter. When they click the submit button we close the posting form. We create a Message object which is saved to the data store and sent to the server. The server creates a Post object, then submits a request to Twitter. The server then updates the Post object, replies back to the UI with the updated information.
That part is already working. But I need to know if it's NOT working so that I can alert the user that their message did not go through and keep the posting form open. Here's some pertinent information about my app.
Social.Message = DS.Model.extend({
text: DS.attr("string"),
created: DS.attr("date"),
isPending: DS.attr("boolean"),
posts: DS.hasMany("Social.Post")
});
Social.Post = DS.Model.extend({
text: DS.attr("string"),
status: DS.attr("string"),
created: DS.attr("date"),
message: DS.belongsTo("Social.Message"),
msgStatus: function() {
return ((this.get('status') === 'S') ? true : false);
}.property('status')
});
The lifecycle of a post (status) goes from P (pending) to Q (queued) to S (sent), E (error) is also a possibility, and the status that I'm really looking for. Here's the saveMessage method:
saveMessage: function(text){
var acct = Social.Account.find(this.get("id")),
msg = Social.store.createRecord(
Social.Message,
{
text: text,
created: new Date()
}
);
acct.get("messages").addObject(msg);
Social.store.commit();
Ember.run.later(this, function() {
msg.get('posts').forEach(function(p){
p.reload();
});
}, 1000);
}
You can see that I pause for a second to let the server process, then attempt to reload the Post object with the response from Twitter. Those last few lines are where I think this new code would go, but I'm not sure how to listen to something that might not come back. I'd rather not "wait" for a second, instead it would be nice if the message could just update. Not sure how to accomplish that though.
Thoughts?
You need to run your code as a callback after the record is created. This is how:
msg.one('didCreate', function() {
// transition to new route showing data just created
});
Social.store.commit();
This will add a one time call on the record for when it is created. There are also 'didUpdate' and 'didDelete' hooks as well. You need to add these callbacks before the create is called (obviously).
I'm not sure how to handle the error condition as I haven't looked into that yet.
Edit: this is actually broken, per https://github.com/emberjs/data/issues/405, so waiting may be the only option currently.
It sounds like you don't want the two-way data binding here and you might benefit from one-way instead. Here is a great full length blog post that explains it a bit more in depth
http://www.solitr.com/blog/2012/06/ember-input-field-with-save-button/

How do I use req.flash() with EJS?

I want to be able to flash a message to the client with Express and EJS. I've looked all over and I still can't find an example or tutorial. Could someone tell me the easiest way to flash a message?
Thanks!
I know this is an old question, but I recently ran across it when trying to understand flash messages and templates myself, so I hope this helps others in my situation. Considering the case of Express 4, the express-flash module, and an ejs template, here are 2 routes and a template that should get you started.
First generate the flash message you want to display. Here the app.all() method maps to /express-flash. Request baseurl/express-flash to create a message using the req.flash(type, message) setter method before being redirected to baseurl/.
app.all('/express-flash', req, res ) {
req.flash('success', 'This is a flash message using the express-flash module.');
res.redirect(301, '/');
}
Next map the message to the template in the res.render() method of the target route, baseurl/. Here the req.flash(type) getter method returns the message or messages matching the type, success, which are mapped to the template variable, expressFlash.
app.get('/', req, res ) {
res.render('index', {expressFlash: req.flash('success') });
}
Finally, display the value of expressFlash, if it exists, in index.ejs.
<p> Express-Flash Demo </p>
<% if ( expressFlash.length > 0 ) { %>
<p>Message: <%= expressFlash %> </p>
<% } %>
Then start the server and visit baseurl/express-flash. It should trigger a redirect to baseurl/ with the flash message. Now refresh baseurl/ and see the message disappear.
<% if ( message ) { %>
<div class="flash"><%= message %></div>
<% } %>
Is this what you want? You can use some client-side JS to have it fading out. jQuery example:
var message = $( '.message' );
if ( message.length ) {
setTimeout( function() {
message.fadeOut( 'slow' );
}, 5000 );
}
req.flash() can be used in two ways.
If you use two arguments
req.flash(type, message);
where type is a type of message and message is actual message (both strings), then it adds message to the queue of type type. Using it with one argument
req.flash(type);
returns array of all messages of type type and empties the queue. Additionally this method is attached to the session, so it works per session. In other words, each user has its own set of flash queues. So in your view you can do something like this:
var messages = req.flash('info');
and then send the messages variable to the template and use it there (note that messages is an array and you can iterate it). Remember that using req.flash('info', 'test'); will append a test message of type info only for a current user (associated with the req object).
Keep in mind that this mechanism is quite weak. For example if a user double clicks on link (two requests send to the server), then he will not see messages, because the first call will empty the queue (of course unless the request generates messages).
I was struggling with this as well; Getting my flash message to appear in my .ejs.
BUT, I finally got it to WORK and this is what I understand.
Once the getter req.flash('_msgName_'); is called it is cleared!
Also when you app.use(session()); cookie must equal cookie: {maxAge: 720}, essentially a big number.
I was using a console.log() to test the getter, and it was displaying in my console, but not in my .ejs. I took the console.log() out and it worked.
Here is some of my code.
Server.js ~
app.get('/', function(req, res) {
// This is where we will retrieve the users from the database and include them in the view page we will be rendering.
User.find({},function(err, allUsers){
if(err){
console.log("Oh no we got an error\n ERROR :: "+err);
} else {
// console.log(allUsers);
res.render('index',{users : allUsers, msg : req.flash('vError')});
}
});
});
// Add User Request
app.post('/users', function(req, res) {
console.log("REQUESTED DATA:\t", req.body);
var user = new User(
{
name: req.body.name,
age: req.body.age
}
);
// Saves user to DB.
user.save(function(err) {
if(err) {
console.log('OOPS, Something went Wrong... \n ERROR :: '+err+'\n');
for(var key in err.errors){
// console.log(err.errors[key].message);
req.flash('vError', err.errors[key].message);
}
// **HERE I WAS ACCIDENTALLY CLEARING IT!!!** \\
// console.log(req.flash('vError'));
// res.render('index',{users : [], msg : req.flash('vError')});
res.redirect('/');
} else {
console.log('\tSuccessfully added a new User to the DB!');
res.redirect('/');
}
})
});
index.ejs ~
<% if(msg.length > 0) { %>
<% for (var error in msg) { %>
<h3><%= msg[error] %></h3>
<% } %>
<% } %>
If you use visionmedia's express-messages helper module, it becomes very simple. Github link
As it says in the module's docs:
Install the module with npm
npm install express-messages
You then assign a dynamic helper for messages in the app.config:
app.dynamicHelpers({ messages: require('express-messages') });
In your EJS file, insert the following where you want your messages
<%- messages() %>
EJS renders this into:
<div id="messages">
<ul class="info">
<li>Email queued</li>
<li>Email sent</li>
</ul>
<ul class="error">
<li>Email delivery failed</li>
</ul>
</div>
(of course, you can alter what it renders to in the module's code)
Then, to actually flash a message, include a call to:
req.flash('info', 'Your message goes here');

Categories