JohnSalzarulo JohnSalzarulo - 27 days ago 7
CoffeeScript Question

How to set params when subscribing to Action Cable channel

I've been trying to get my head around action cable for what seems like months. Please help.

I have a "Connection" - I can't set the

identified_by :current_user
because this endpoint also needs to be consumed by an external API that uses WebSockets. Can't use browser cookies to authenticate the API endpoint.

Files & Support



Connection:
/app/channels/application_cable/connection.rb


module ApplicationCable
class Connection < ActionCable::Connection::Base

end
end


Channel:
/app/channels/application_cable/channel.rb


module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end


I have a specific Visits Channel:
/app/channels/visits_channel.rb


class VisitChannel < ApplicationCable::Channel
def subscribed
stream_from "visit_#{params[:visit_id]}"
end
end


Then I have my coffeescript channel:
/app/assets/javascripts/channels/visit.coffee


App.visit = App.cable.subscriptions.create { channel: 'VisitChannel', visit_id: '42' },
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

push: ->
@perform 'push'


Then I have a callback on my visit model:
/app/models/visit.rb


class Visit < ApplicationRecord

after_save :push_to_action_cable

**** detail of model removed ****

def push_to_action_cable
ActionCable.server.broadcast("visit_#{self.id}", self)
end

end


This is working perfectly, it puts to the console the object every time and only that object with an ID of 42

Here is my question:



Within the coffeescript channel: found at
/app/assets/javascripts/channels/visit.coffee
- How do I set the
visit_id
so that I can "listen" for the changes on only the visit I want?

App.visit = App.cable.subscriptions.create { channel: 'VisitChannel', visit_id: 'HOW_DO_I_SET_THIS?' },
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

push: ->
@perform 'push'


What I have tried:



I have tried every variation of things like:

App.visit = App.cable.subscriptions.create { channel: 'VisitChannel', visit_id: <%= @visit.id %> }


results in:

ExecJS::RuntimeError in Visits#action_cable
Showing /Users/johnsalzarulo/code/uvohealth/app/views/layouts/application.html.erb where line #9 raised:

SyntaxError: [stdin]:1:81: unexpected <


and

App.visit = App.cable.subscriptions.create (channel: 'VisitChannel', visit_id: "#{ params[:id] }")


results in:

ExecJS::RuntimeError in Visits#action_cable
Showing /Users/johnsalzarulo/code/uvohealth/app/views/layouts/application.html.erb where line #9 raised:

SyntaxError: [stdin]:1:93: unexpected :


and

App.visit = App.cable.subscriptions.create (channel: 'VisitChannel', visit_id: "#{ @visit.id }")


results in:

visit.self-e04de4513d06884493c48f4065f94d23255be682f915e26766c54bb9d17ef305.js?body=1:4 Uncaught TypeError: Cannot read property 'id' of undefined
at visit.self-e04de4513d06884493c48f4065f94d23255be682f915e26766c54bb9d17ef305.js?body=1:4
at visit.self-e04de4513d06884493c48f4065f94d23255be682f915e26766c54bb9d17ef305.js?body=1:18
(anonymous) @ visit.self-e04de4513d06884493c48f4065f94d23255be682f915e26766c54bb9d17ef305.js?body=1:4
(anonymous) @ visit.self-e04de4513d06884493c48f4065f94d23255be682f915e26766c54bb9d17ef305.js?body=1:18


and

App.visit = App.cable.subscriptions.create (channel: 'VisitChannel', visit_id: "#{ visit.id }")


results in:

visit.self-b636f38376edc085c15c2cfc4d524bafc5c5163a8c136b80ba1dda12813fc0b5.js?body=1:4 Uncaught ReferenceError: visit is not defined
at visit.self-b636f38376edc085c15c2cfc4d524bafc5c5163a8c136b80ba1dda12813fc0b5.js?body=1:4
at visit.self-b636f38376edc085c15c2cfc4d524bafc5c5163a8c136b80ba1dda12813fc0b5.js?body=1:18
(anonymous) @ visit.self-b636f38376edc085c15c2cfc4d524bafc5c5163a8c136b80ba1dda12813fc0b5.js?body=1:4
(anonymous) @ visit.self-b636f38376edc085c15c2cfc4d524bafc5c5163a8c136b80ba1dda12813fc0b5.js?body=1:18


In Closing



I have tried many many more combinations. The only thing that KIND of works was throwing a
<script>
into the view template for that page that explicitly subscribed to the visit, but then I didn't get the benifit of the callbacks, plus I know this isn't the "rails way".

It's been hours of reading these docs and trying to make this work. Can anyone shed some light on what I'm missing here?

Answer Source

A few things to think about here:

  1. The order in which the scripts all load.
  2. The specific timing of "instantiation" of the ruby variables, (Where / when they can be accessed).
  3. Realizing that plain ol' JavaScript can be used within stupid CoffeeScript.

That said - Here's the solution that worked for me:

Files and Support

All of the files used in the question asked above are unchanged except for what's below. To get this working you'll need to reference the files above and the files below for the full stack.

The main template for my app: app/views/layouts/application.html.erb Pay attention to the line within the head tag yield(:head_attributes)

<!DOCTYPE html>
<html>
  <head <%= yield(:head_attributes) %> >
    <title>Uvo Health</title>
    <%= action_cable_meta_tag %>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>

  <body  <%= yield(:body_attributes) %> >
    <%= render 'layouts/navbar' unless @hide_nav %>
    <%= render 'shared/flash_messages' %>
    <%= yield %>
    <%= yield :page_js %>
  </body>

</html>

The view for the page in which I am trying to use actioncable. In my case it's: app/views/visits/action_cable.html.erb - Most times it will probably be your show.html.erb or index.html.erb Pay attention to the content_for

<%= content_for(:head_attributes) do %>data-visit-id="<%= @visit.id %>"<% end %>

<div class='container'>
  <%= render 'visits/visit_overview' %>
</div>

Then in my visit channel /app/assets/javascripts/channels/visit.coffee

App.visit = App.cable.subscriptions.create { channel: 'VisitChannel', visit_id: document.querySelector('head').dataset.visitId },
  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

  push: ->
    @perform 'push'

What's happening:

  1. As the page loads the view adds the data attribute of data-visit-id to the head of the page.
  2. The channel of visit.coffee reads this attribute from the head of the page.
  3. Then visit.coffee can use this variable to subscribe to the appropriate channel.

Other solutions wouldn't work because I was trying to access things in the wrong 'order' meaning, load a variable before it was instantiated. Hope this is helpful for others. This one stumped me for a solid 5 hours.