Integrate stimulus with websockets


#1

Hey I was wondering what was the best way to integrate stimulus with websockets
What I am currently doing is that I have a websocket with some javascript updating the controller html attribute and I have the stimulus controller refreshing with a timer.

Here are my questions.

  • I see there are onlick, onsubmit handlers, are there other handlers ? (can you add some ? typically when my websocket receives new data I would like to trigger an event to refresh the element)

  • Is there a way to re-initialize the element ? or maybe deconnect it and reconnect it. Just thinking out loud here.

(detail, I’m using phoenix with elixir, but I think this is applicable for any other backend framework that supports websockets)


#2

Ok here is what I ended up doing in case it can help anybody

  • my websocket code is now firing an “update” event on update
  • The controller is listening to that update event and re-rendering the UI accordingly.

Here is what the code look like

channel.on('update', ({lastSyncedAt}) => {
    // this update is used on several elements with various controllers, so I need to get the controller name 
    // to update the proper attribute
    const constrollerName = elementToUpdate.getAttribute('data-controller');
    elementToUpdate.setAttribute(`data-${constrollerName}-last-synced-at`, lastSyncedAt);
   // I fire an "update" event on the element
    const updateEvent = new Event('update');
    elementToUpdate.dispatchEvent(updateEvent);
})
<div
          class="media-body trading_account_last_synced_at"
          data-controller="main-sidebar-trading-account"
          data-url-hash="<%= url_hash %>"
          <-- Here is where I am hooked up to the update event -->
          data-action="update->main-sidebar-trading-account#show"
          <-- this is the element I update with my websocket -->
          data-main-sidebar-trading-account-last-synced-at="<%= last_synced_at %>"
        >
          <span class="media-heading text-semibold"><%= name %></span>
          <span class="text-muted">
            <i
              class="icon-checkmark3 text-size-small text-success"
              style="margin-top: -1px; font-size: 13px;"
              data-target="main-sidebar-trading-account.checkMark"
            >
            </i>
              <%= account_number %>
          </span>
        </div>

hopefully the code is clear enough, let me know if anything doesn’t make sense


#3

digging into javascript api, I have made a little update to the code.
It turns out that by using CustomEvent, your events can have value. So with the websocket I just fire a custom event and let the controller take care of the update.
Here is how the code looks now.

channel.on('update', ({lastSyncedAt}) => {
    const updateEvent = new CustomEvent('update', {detail: lastSyncedAt});
    elementToUpdate.dispatchEvent(updateEvent);
  });

and the js controller is now just

set lastSyncedAt(value) {
    return this.data.set('last-synced-at', value);
  }

update({detail}) {
    this.lastSyncedAt = detail;
    this.show();
  }

and I just changed the event handler on the html so now

data-action="update->main-sidebar-trading-account#update"

the weird variable naming detail, comes from the CustomEvent api, you can only pass an object with a detail attribute.
Hope this helps someone


#4

Pretty cool! I like the use of custom events here and how that looks in the data-action attribute. However, I should point out that CustomEvent isn’t supported in older browsers (aka IE). There seems to be a polyfill available—I haven’t tried it personally but passing it along for reference: https://github.com/webmodules/custom-event


#5

actually I have changed the code a bit since then. I was looking into that open source repo alloy-ci. They are using stimulus with websockets as well (phoenix).
They are doing the websocket connection in the component. That way you don’t need to use customEvents at all. Here is how the code looks like (it feels like it makes way more sense)

/* global moment */
import {Controller} from 'stimulus';
import socket from '../socket';

export default class extends Controller {
  static targets = ['checkMark']

  get checkmark() {
    return this.checkMarkTarget;
  }

  get lastSyncedAt() {
    return this.data.get('last-synced-at');
  }

  set lastSyncedAt(value) {
    return this.data.set('last-synced-at', value);
  }

  connect() {
    this.show();
    this.connectToTradingAccountUpdates();
  }

  connectToTradingAccountUpdates() {
    const urlHash = this.data.get('url-hash');
    const channel = socket.channel(`trading_account:${urlHash}`, {});
    channel.join()
      .receive('ok', () => console.log('Receving trading account updates'))
      .receive('error', resp => {
        console.log('failed to connect to trading account updates', resp);
      });
    channel.on('update', ({lastSyncedAt}) => {
      this.lastSyncedAt = lastSyncedAt;
      this.show();
    });
  }

  show() {
    if (this.lastSyncedAt) {
      const lastSyncedAtUtc = moment.utc(+this.lastSyncedAt);
      const twelveMinutesAgo = moment.utc().subtract(12, 'minutes');
      if (lastSyncedAtUtc.isBefore(twelveMinutesAgo)) {
        this.checkmark.classList.remove('icon-checkmark3', 'text-success');
        this.checkmark.classList.add('text-danger-800', 'icon-cross2');
      }
    } else {
      this.checkmark.classList.remove('text-success', 'icon-checkmark3');
      this.checkmark.classList.add('icon-question4');
    }
  }
}