Utiliser plusieurs canaux Action Cable

Action Cable != Broadcast

Depuis Rails 5 les développeurs Rails peuvent utiliser un mécanisme de Pub/Sub pour mettre à jour des pages dynamiquement. Le scénario classique est l’affichage d’un tableau de bord avec moultes calculs et graphiques qui montrent le résultat des ventes d’un produit. Imaginer que pendant que vous regarder ce magnifique tableau, un commercial enregistre une grosse commande. Il faudrait rafraichir le tableau pour la voir apparaitre puisqu’elle a eu lieu après le calcul et l’affichage des résultats des ventes…

Mais ça c’était avant. En effet Action Cable permet d’abonner le tableau de résultats à tout changement ayant lieu dans les ventes. Quand ce changement a lieu, il suffit de diffuser le nouveau contenu pour que les pages abonnées soient averties de la mise à jour et remplace leur contenu par le nouveau contenu reçu.

Pour que ce soit plus clair, rien ne vaut un exemple concret et son code.

Nous allons créer une liste de produits (nom, catégorie, prix) et une page qui devra afficher la somme des prix des produits appartenants à une des catégorie.

Si tout fonctionne bien, la page ‘Dash’ affichant la somme des produits ‘A’ sera actualisée si vous modifiez, dans une autre page, le prix d’un des produits de la même catégorie.

Allez, au boulot !

$ rails new testActionCableApp
$ cd testActionCableApp
$ rails g scaffold Product name category price:integer
$ rails db:migrate

Ajouter maintenant des produits sans oublié la catégorie ! Une fois le catalogue produits bien rempli, nous allons créer le “Dashboard” et brancher les câbles.

$ rails g controller Dash index
$ rails g channel products

Modifiez comme ci-dessous les fichiers suivants

app/views/dash/index.html.erb

<h1>Dash</h1>

<p>
    <%= form_tag "index", method: 'get' do %>
        <%= label_tag(:category, "Category:") %>
        <%= text_field_tag :category, @category %>
        <%= submit_tag("Search") %>
    <% end %>
</p>

<div id="sum-panel" 
     data-channel-subscribe="products" 
     data-category="<%= @category %>">

    <h2>
        <%= "Total catégorie '#{ @category }' = #{ @sum }" %> 
    </h2>

</div>

Cette petite page est constituée d’un formulaire dans lequel l’utilisateur viendra saisir la catégorie pour laquelle il veut afficher le total des prix.

app/controllers/dash_controller.rb

class DashController < ApplicationController
  def index
    unless params[:category].blank?
      @category = params[:category]

      @sum = Product
                .where(category: @category)
                .sum(:price)
    end
  end
end

Ici on calcule la somme des prix pour la catégorie choisie par l’utilisateur

app/channels/products_channel.rb

class ProductsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "category_#{ params[:category] }"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

On souscrit au canal Produits. Les flux sont nommés par le nom de la catégorie (ex: ‘categoy_A’).

app/javascript/channels/products_channel.js

import { logger } from "@rails/actioncable";
import consumer from "./consumer"

$(document).on('turbolinks:load', function () {
  consumer.subscriptions.create(
    {
      channel: "ProductsChannel",
      category: $('#sum-panel').attr('data-category')
    }
    , {
    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) {
      // Called when there's incoming data on the websocket for this channel

      const dashElement = document.querySelector("main.dash")

      if (dashElement) {
        dashElement.innerHTML = data.html
      }

    }
  });
})

C’est ici que ça devient intéressant car on soucrit à un flux nommé. Sinon, toutes la pages abonnées recevraient le même contenu, quelque soit la catégorie choisie par l’utilisateur.

Cette information est obtenue en allant lire dans la page les données sum-panel.data-category

app/controllers/products_controller.rb

  # PATCH/PUT /products/1
  # PATCH/PUT /products/1.json
  def update
    respond_to do |format|
      if @product.update(product_params)
        format.html { redirect_to @product, notice: 'Product was successfully updated.' }
        format.json { render :show, status: :ok, location: @product }

        @category = @product.category
        @sum = Product
                  .where(category: @category)
                  .sum(:price)

        ActionCable.server.broadcast "category_#{ @category }",
                            html: render_to_string('dash/index', layout: false)

      else
        format.html { render :edit }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

Il ne reste plus qu’à envoyer dans le tuyaux le nouveau contenu qui devra apparaître dans toutes les pages abonnées.

Effet ‘waouh!’ garanti ;-)