How to set params when subscribing to Action Cable channel
Asked Answered
C

2

10

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?

Comparison answered 31/8, 2017 at 0:10 Comment(1)
Had the same question!Pedro
C
7

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. 🙄

Comparison answered 31/8, 2017 at 17:0 Comment(0)
B
3

Set the visit_id in a HTML tag, maybe in the body tag in your layout file.

<body data-visit-id="<%= @visit.id %>">

Now read it from JS like this:

document.querySelector('body').dataset.visitId

Your subscription creation line would look like this:

App.visit = App.cable.subscriptions.create (channel: 'VisitChannel', visit_id: document.querySelector('body').dataset.visitId)
Bouley answered 31/8, 2017 at 1:39 Comment(2)
Thank you so much! Although your specific implementation did not work in my use case, this answer led me to a solution that's pretty clean and works consistently. I'll be outlining my specific method in a moment. The only issue with setting the value on body is that rails loads the channel script /app/assets/javascripts/channels/visit.coffee before the body loads. So, the variable was unset. However, I basically did what you recommended just on the head and it's working well. (As I said writing it up now)Comparison
you can add javascript at end of html before </body> tag.Jacks

© 2022 - 2024 — McMap. All rights reserved.