Comptes à Rebours en Direct Sans JavaScript : Turbo Streams + relative_time_in_words

La plupart des minuteries de compte à rebours nécessitent des bibliothèques JavaScript. Mais avec le nouveau helper relative_time_in_words de Rails 8 et Turbo Streams, vous pouvez construire des comptes à rebours qui se mettent à jour en direct entièrement côté serveur. Pas de packages npm, pas de calculs de dates côté client, et cela fonctionne correctement quand JavaScript est désactivé.

Ceci est un exemple un peu artificiel—une exploration amusante de ce qui est possible avec le rendu purement côté serveur. En production, vous combineriez probablement des comptes à rebours JavaScript côté client avec une validation côté serveur, ce qui utilise moins de ressources serveur tout en maintenant la précision là où ça compte (l’application réelle de l’échéance). Mais voyons jusqu’où nous pouvons pousser l’approche côté serveur.

Le Nouveau Helper

Rails 8 ajoute relative_time_in_words, qui gère à la fois les temps passés et futurs :

relative_time_in_words(3.hours.from_now)
# => "in about 3 hours"

relative_time_in_words(2.days.ago)
# => "2 days ago"

C’est différent de time_ago_in_words, qui ne gère que le passé. Avoir un seul helper pour les deux directions rend les interfaces de compte à rebours beaucoup plus simples.

Construire une Minuterie d’Enchères en Direct

Construisons un compte à rebours d’enchères qui se met à jour chaque minute sans écrire de JavaScript.

Le Modèle

class Auction < ApplicationRecord
  def ended?
    ends_at <= Time.current
  end

  def time_remaining
    ends_at - Time.current
  end
end

La Vue

<%= turbo_stream_from "auction_#{@auction.id}" %>

<div class="auction-timer">
  <span id="countdown_<%= @auction.id %>"
        class="<%= urgency_class(@auction.ends_at) %>">
    <%= relative_time_in_words(@auction.ends_at) %>
  </span>
</div>

Styles Basés sur l’Urgence

Ajoutez un helper qui retourne des classes CSS basées sur le temps restant :

# app/helpers/countdowns_helper.rb
module CountdownsHelper
  def urgency_class(deadline)
    return "ended" if deadline <= Time.current

    remaining = deadline - Time.current
    case remaining
    when 0..1.hour then "critical"
    when 1.hour..1.day then "warning"
    else "normal"
    end
  end
end
.critical { color: #dc2626; font-weight: bold; }
.warning { color: #d97706; }
.normal { color: #059669; }
.ended { color: #6b7280; }

Le Job de Diffusion

C’est là que Turbo Streams entre en jeu. Un job récurrent diffuse les mises à jour du compte à rebours :

# app/jobs/countdown_broadcast_job.rb
class CountdownBroadcastJob < ApplicationJob
  def perform(auction)
    return if auction.ended?

    Turbo::StreamsChannel.broadcast_update_to(
      "auction_#{auction.id}",
      target: "countdown_#{auction.id}",
      html: render_countdown(auction)
    )

    # Planifier la prochaine mise à jour
    self.class.set(wait: update_interval(auction)).perform_later(auction)
  end

  private

  def render_countdown(auction)
    ApplicationController.render(
      partial: "auctions/countdown",
      locals: { auction: auction }
    )
  end

  def update_interval(auction)
    remaining = auction.time_remaining
    case remaining
    when 0..5.minutes then 10.seconds
    when 5.minutes..1.hour then 1.minute
    else 5.minutes
    end
  end
end

Le job se planifie lui-même avec des intervalles dynamiques—mettant à jour toutes les 10 secondes quand l’échéance est proche, mais seulement toutes les 5 minutes quand elle est loin. Cela garde votre file de jobs gérable.

Le Partial

<%# app/views/auctions/_countdown.html.erb %>
<span class="<%= urgency_class(auction.ends_at) %>">
  <% if auction.ended? %>
    Terminée
  <% else %>
    <%= relative_time_in_words(auction.ends_at) %>
  <% end %>
</span>

Démarrer le Compte à Rebours

Lancez le job de diffusion quand la page d’enchères est consultée :

class AuctionsController < ApplicationController
  def show
    @auction = Auction.find(params[:id])
    CountdownBroadcastJob.perform_later(@auction) unless @auction.ended?
  end
end

Autres Cas d’Utilisation

Ce pattern fonctionne pour tout affichage sensible au temps :

Ventes Flash

<div id="sale_timer">
  La vente se termine <%= relative_time_in_words(@sale.ends_at) %>
</div>

SLAs de Tickets de Support

def sla_status(ticket)
  deadline = ticket.created_at + ticket.sla_hours.hours
  if deadline.future?
    "Réponse attendue #{relative_time_in_words(deadline)}"
  else
    "SLA non respecté #{relative_time_in_words(deadline)}"
  end
end

Horaires d’Événements

<% if @event.starts_at.future? %>
  Commence <%= relative_time_in_words(@event.starts_at) %>
<% else %>
  A commencé <%= relative_time_in_words(@event.starts_at) %>
<% end %>

Points d’Attention

Surcharge de la File de Jobs

Chaque compte à rebours actif génère des jobs récurrents. Pour les pages à fort trafic, considérez :

  • Regrouper les mises à jour pour plusieurs enchères
  • Utiliser Action Cable pour des mises à jour vraiment en temps réel
  • Limiter le nombre de jobs de compte à rebours concurrents

Fuseaux Horaires

relative_time_in_words utilise Time.current, qui respecte Time.zone. Assurez-vous que la gestion des fuseaux horaires de votre application est cohérente.

Dégradation Gracieuse

Le compte à rebours fonctionne sans JavaScript—les utilisateurs voient simplement un temps statique qui était précis au chargement de la page. Turbo Streams ajoute les mises à jour en direct comme une amélioration.

I18n

Le helper utilise les traductions intégrées de Rails. Personnalisez-les dans vos fichiers de locale :

fr:
  datetime:
    relative:
      past: "il y a %{time}"
      future: "dans %{time}"

Conclusion

Cette approche vous donne des comptes à rebours en direct avec une complexité minimale. Le serveur est la source de vérité pour le temps, éliminant les problèmes de décalage d’horloge côté client. Et comme c’est juste du HTML diffusé en streaming, ça fonctionne partout où Turbo fonctionne.

Pour une précision vraiment en temps réel (seconde par seconde), vous auriez toujours besoin de JavaScript. Mais pour la plupart des cas d’utilisation—enchères, ventes, SLAs, événements—des mises à jour à la minute suffisent, et cette approche côté serveur est plus simple à construire et maintenir.

Consultez le PR #55405 pour l’implémentation de relative_time_in_words.