Using websockets only for some pages

I am trying to turn on websockets only for some pages of my app and not others. Unfortunately, every example I have found shows modifying socket.js which then turns on websockets for the entire app.

Any way to do this using brunch or do I need to make major changes?

I am using standard Phoenx/Elixir/Brunch.

And second question, is there any way to access classes, variables I create in custom js in the the static/js folders? I get them to load on app.js with import calls but my templates are unable to access the classes or variables I create.

I’m a bit of a newbie still so still getting the hang of this.

Thanks in advance!

2 Likes

Well, if you want to activate sockects only for specific pages, then you I guess should include sockect.js file only in those views and take out from main bundler.

1 Like

Hi,

I have suffered the same issue. I only wanted to execute specific JS depending on the rendered template (some for the channels/websockets others for other things). As @krapans has suggested, you only have to include the JS file that will connect to the channel in those pages where you need, but now what we have is a brunch configuration problem.

You can read the post that I wrote about that: Post

I have a project in production using this approach and it is working without any problem.

Good luck !!

1 Like

I delete default socket.js and split the part that sets up a socket and the one that connects to a channel, something like that in some_socket.js

import {Socket} from "phoenix";
const someSocket = new Socket("/socket");

// set up channel / auth token, join channel, add socket-specific functions

export {someFunction as someFunction};
export {otherFunction as otherFunction};
// ...
export {someSocket as socket};

then in a template(s) where I need this functionality I render a partial that initiates everything

var someSocket = require('app/some_socket');
someSocket.socket.connect();

// set up event handlers etc, use socket functions as someSocket.someFunction(param1);

this way you can use the sockets where you want (not only per layout) without worrying that something will (try to) happen when you include your app.js, it’s not that much code to justify splitting imo.

We also split the bigger parts of the functionality into different files using joinTo as mentioned in the post by @mendrugory

1 Like

Thanks everyone for the great responses! I am going to try a few of these next. And happy to hear I was not missing something totally simple :).

Is there a recommended solution for this? I too would like to setup socket connects on specific template views, and don’t want to include that globally.

Check out render_existing/3.

Specifically the section called rendering based on controller template.

1 Like

If you’re using authentication for websockets you need to supply a token anywhere in your html. You could let that one render only on specific pages and make the javascript only connect if a token is present.

1 Like

I was following the documentation commented in socket.js (excerpt below), but how can I restrict socket.connect in JS if the user is not logged in?

window.userToken is only assigned if a logged in user is present (if @conn.assigns.user do...end). Otherwise, the token will be undefined (resulting in console errors(web socket can’t connect))


In your “lib/web/router.ex”:

pipeline :browser do
  ...
  plug MyAuth
  plug :put_user_token
end

defp put_user_token(conn, _) do
  if current_user = conn.assigns[:current_user] do
    token = Phoenix.Token.sign(conn, "user socket", current_user.id)
    assign(conn, :user_token, token)
  else
    conn
  end
end

Now you need to pass this token to JavaScript. You can do so
inside a script tag in “lib/web/templates/layout/app.html.eex”:

<script>window.userToken = "<%= assigns[:user_token] %>";</script>
1 Like

On that same file you could let socket.... & socket.connect() conditionally by just checking if there is a window.userToken?

1 Like

Just do not try to connect without the token.

if(window.userToken) {
  socket.connect()
}
1 Like

Something like this?

socket.js

import {Socket} from "phoenix"

if(window.userToken) {
 let socket = new Socket("/socket", {params: {token: window.userToken}})
 socket.connect()

 const createSocket = (topicId) => {
  ....
  }
 ...
 window.createSocket = createSocket;
}

templates\topic\show.html.eex

<script>
document.addEventListener("DOMContentLoaded", function() {
  <%= if @conn.assigns.current_user do %>
  window.userToken = "<%= assigns[:user_token] %>";
  window.createSocket(<%= @topic.id %>)
  <% end %>
});
</script>

In the console, I either get type errors or can’t establish a connection to the server at ws://localhost:4000/socket/websocket?token=undefined&vsn=2.0.0

Note:
I have a module plug in the browser pipeline that signs the token.

  def call(conn, _params) do
    if current_user = conn.assigns[:current_user] do
      token = Phoenix.Token.sign(conn, "user socket", current_user.id)
      assign(conn, :user_token, token)
    else
      conn
    end
  end

and my user_socket.ex

  def connect(%{"token" => token}, socket) do
    # max_age: 1209600 is equivalent to two weeks in seconds
    case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
      {:ok, user_id} ->
        {:ok, assign(socket, :user, user_id)}
      {:error, reason} ->
        :error
    end
  end
1 Like

I was thinking that would work yes. Can you log window.userToken and see what it’s outputting? It’s strange because on the error message it’s being sent as undefined

1 Like

If there’s a logged in user, it logs the token.

If no user is logged in, it returns undefined.

1 Like

Are you assigning the channel in JS somewhere else, besides the socket.js file?
Like socket.channel(....);

note: this solution on your particular case, since you’re waiting for the “DOM” event to fire to add those variables to window isn’t particularly fitting either way

1 Like

No. I only have a socket.channel function in socket.js which joins the topic channel ( topic_channel.ex where I have the join and handle_in functions )

The only way I got it to seemingly work (shown below) is by setting the window.userToken in app.html.eex's head section instead of in a script tag in template\topic\show.html.eex with an event listener for DOMContentLoaded. If you check my previous reply above, I had the window.userToken assignment right above window.createSocket. I don’t know, but maybe that was the culprit?

app.html.eex

<head>
...
  <script>
    <%= if @conn.assigns.current_user do %>
      window.userToken = "<%= assigns[:user_token] %>";
    <% end %>   
  </script>
</head>

template\topic\show.html.eex

<script>
document.addEventListener("DOMContentLoaded", function() {
  <%= if @conn.assigns.current_user do %>
  window.createSocket(<%= @topic.id %>)
  <% end %>
</script>

and I wrapped socket.connect() in the userToken condition as below:

socket.js

if (window.userToken) {
  socket.connect()
}
1 Like