27/09/2020

Utiliser plusieurs canaux Action Cable

Rails
Ruby
Action cable

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 moult calculs et graphiques qui montrent le résultat des ventes d'un produit. Imaginez que pendant que vous regarder ce magnifique tableau, un commercial enregistre une grosse commande. Il faudrait rafraîchir le tableau pour la voir apparaître puisqu'elle a eu lieu après le calcul et l'affichage des résultats des ventes...

Action Cable != Broadcast

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 appartenant à 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

Dash

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

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

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 souscrit à 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 ;-)