RUBY ON RAILS · 14 MIN READ ·

Rails Turbo Morphing: Realtime DOM-updates met broadcasts_refreshes

Rails Turbo Morphing patcht de DOM chirurgisch bij een paginavernieuwing. Leer broadcasts_refreshes, scroll-anchoring en wanneer morphing custom ActionCable JS vervangt.

Rails Turbo Morphing: Realtime DOM-updates met broadcasts_refreshes

Zes maanden geleden stapte ik een codebase binnen waar een React-ontwikkelaar een volledig op maat gemaakt realtime dashboard op een Rails-app had geplakt. Elke rij in een tabel werkte live bij. De grafieken vernieuwden op een timer. Een klik op een rij klapte hem ter plekke uit zonder een paginalading. Het was werkelijk indrukwekkend. Het was ook drieduizend regels custom ActionCable JavaScript, een parallelle serializer-laag die de view-logica dupliceerde die al in de ERB-templates zat, en een achterstand van een maand aan bugs waarbij de JavaScript-state was afgedreven van wat de database daadwerkelijk bevatte.

We hebben de hele boel in een week vervangen. De vervanging was ongeveer tachtig regels Ruby, twee model-callbacks en één meta-tag in de layout.

Dat is het verhaal achter Rails Turbo Morphing: realtime DOM-updates die je bestaande server-gerenderde templates hergebruiken, voor vrijwel niets. Na negentien jaar Rails heb ik zelden een feature gezien die zo netjes het patroon verving dat het overbodig maakte.

Wat Rails Turbo Morphing eigenlijk doet

Turbo Drive — de standaard sinds Rails 7 met Hotwire — geeft je single-page-app-achtige navigatie door linkklikken en formulierinzendingen te onderscheppen, de nieuwe pagina op te halen en de volledige <body> te vervangen. Snel, maar grof.

Turbo Frames, die ik besprak in de Turbo Frames deep-dive, laat je benoemde secties van een pagina onafhankelijk bijwerken. Chirurgisch, maar je moet de pagina-indeling vooraf rond de frame-grenzen ontwerpen.

Turbo Morphing zit daartussenin. Als een update binnenkomt — van een formulierinzending, een ActionCable-broadcast of een expliciete redirect — gebruikt Turbo een DOM-diffing-bibliotheek genaamd morphdom om alleen de gewijzigde nodes te patchen. De rest van de pagina blijft staan: scrollpositie behouden, formuliervelden onaangeroerd, open dropdowns nog steeds open. De server rendert nog steeds de volledige pagina. De browser past alleen de diff toe.

Er zijn twee afzonderlijke mechanismen in Rails 8 die morphdom gebruiken:

  1. Paginavernieuwing met morphing: De volledige paginanavigatie van Turbo gebruikt morphdom in plaats van volledige <body>-vervanging. Geactiveerd door een redirect, een model-broadcast of een formulierinzending die de pagina herlaadt.
  2. turbo_stream.morph-actie: Een Turbo Stream-actie die via ActionCable of een respond_to :turbo_stream-blok chirurgisch een specifiek DOM-doel patcht.

Beide gebruiken dezelfde diffing-logica. Het meeste hieronder gaat over paginavernieuwing met morphing, omdat dat negentig procent van de realtime use cases afhandelt met de minste overhead.

Rails Turbo Morphing instellen

Je hebt Rails 8 nodig met turbo-rails 2.0 of hoger. Controleer je Gemfile:

gem "turbo-rails", ">= 2.0"

Schakel daarna morphing in in je layout. Voeg één regel toe in <head> van app/views/layouts/application.html.erb:

<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
    <%= stylesheet_link_tag "application" %>
    <%= javascript_importmap_tags %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

turbo_refreshes_with genereert twee meta-tags die Turbo aan de clientkant uitleest:

<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">

Dat is de volledige installatie. Elke volgende paginavernieuwing — geactiveerd door een redirect, een turbo_stream.refresh-broadcast of een expliciete controlleractie — gebruikt nu morphdom om de DOM te patchen en de scrollpositie te bewaren.

Het eenvoudige geval: broadcasts_refreshes op een model

Zodra morphing is ingeschakeld in de layout, is het eenvoudigste realtime-patroon één regel op je model:

class Order < ApplicationRecord
  belongs_to :customer
  has_many :line_items

  broadcasts_refreshes
end

broadcasts_refreshes koppelt zich aan ActiveRecord-callbacks — after_create_commit, after_update_commit, after_destroy_commit — en broadcast een :refresh Turbo Stream-actie naar een kanaal vernoemd naar het record (bijvoorbeeld Order:42). Elke verbonden browser die is geabonneerd op dat kanaal ontvangt het signaal en activeert een paginavernieuwing met morphdom.

In de view, abonneer je op de stream:

<%# app/views/orders/show.html.erb %>
<%= turbo_stream_from @order %>

<div id="order_<%= @order.id %>">
  <h1><%= @order.reference_number %></h1>
  <p>Status: <strong><%= @order.status %></strong></p>
  <p>Totaal: <%= number_to_currency(@order.total) %></p>

  <%= render @order.line_items %>
</div>

Wanneer een andere gebruiker — of een achtergrondtaak — deze order bijwerkt, ontvangt elke browser die deze pagina bekijkt een turbo:refresh-signaal. Turbo haalt de pagina vers op van de server, vergelijkt die met de huidige DOM via morphdom en patcht alleen wat er veranderd is. De orderstatus werkt ter plekke bij. De regelitems worden bijgewerkt. De scrollpositie verschuift niet. Tekst die de gebruiker in een formulierveld heeft getypt, blijft bewaard omdat morphdom overeenkomende DOM-nodes identificeert aan de hand van het id-attribuut.

De server rendert exact dezelfde template als altijd. Er is geen apart API-endpoint, geen serializer, geen JSON-structuur die je naast je views moet bijhouden. De ERB-template is de enige bron van waarheid voor zowel de initiële render als elke volgende live update.

Broadcasten naar collecties en scoped kanalen

broadcasts_refreshes broadcast standaard naar het eigen kanaal van het model — Order:42 voor order met id 42. Dat werkt perfect voor show-pagina’s. Voor index-pagina’s of dashboards waar je alle verbonden clients wil vernieuwen wanneer een record in een collectie wijzigt, broadcast je naar een breder bereik:

class Order < ApplicationRecord
  belongs_to :customer

  # Notificeer iedereen die de algemene orders-stream bekijkt
  broadcasts_refreshes_to :orders

  # Notificeer iedereen die de orders van deze klant bekijkt (multi-tenant veilig)
  broadcasts_refreshes_to -> { [customer, :orders] }
end

Je kunt beide combineren. Wanneer een order wordt opgeslagen, informeert die zowel de generieke :orders-stream als de klant-specifieke stream. Abonneer je in de view op het kanaal dat de pagina nodig heeft:

<%# Indexpagina — abonneer op de :orders-stream %>
<%= turbo_stream_from :orders %>

<table id="orders-table">
  <tbody>
    <%= render @orders %>
  </tbody>
</table>

Wanneer een order in het systeem wordt bijgewerkt, ontvangen clients op deze indexpagina een vernieuwingssignaal. morphdom vergelijkt de gerenderde tabel met de huidige DOM en werkt alleen de gewijzigde rijen bij. Voor een orderbeheer-dashboard met meerdere medewerkers die tegelijkertijd open tabbladen hebben, is dit de complete realtime-laag — zonder één regel custom JavaScript.

Scroll-anchoring en DOM-state bewaren

scroll: :preserve in turbo_refreshes_with verhelpt de meest gehoorde klacht over volledige paginavernieuwingen: de viewport springt terug naar boven. Maar er zijn twee andere scenario’s die je snel zult tegenkomen.

Permanente elementen — open modals, dropdownmenu’s, videospelers, alles wat niet vervangen mag worden midden in een interactie — krijgen het attribuut data-turbo-permanent:

<div id="notification-drawer" data-turbo-permanent>
  <%# morphdom slaat dit element volledig over tijdens patching %>
</div>

morphdom laat elk element met data-turbo-permanent precies zoals het is. Het element moet een stabiel id hebben — morphdom matcht permanente elementen aan de hand van hun id-attribuut.

Tijdelijke elementen — flash-berichten, toast-notificaties, voortgangsbalken — moeten verdwijnen bij een paginavernieuwing. Voeg data-turbo-temporary toe:

<%# app/views/shared/_flash.html.erb %>
<% flash.each do |type, message| %>
  <div class="flash flash-<%= type %>" data-turbo-temporary>
    <%= message %>
  </div>
<% end %>

Turbo verwijdert data-turbo-temporary-elementen voordat de morphdom-diff wordt uitgevoerd. De vernieuwde render bevat het flash-bericht niet (de redirect heeft het al uit de sessie geconsumeerd), dus morphdom hoeft nooit een verouderde notificatie te reconciliëren met een lege serverrespons. Geen flash-berichten meer die blijven hangen bij volgende gemorphte vernieuwingen.

Chirurgische updates met turbo_stream.morph

Soms is een volledige paginavernieuwing — zelfs een gemorphte — breder dan nodig. Je wilt na een formulierinzending één specifiek element bijwerken zonder een rondreis voor de hele pagina te activeren. turbo_stream.morph richt zich op één enkel DOM-element:

# app/controllers/notes_controller.rb
def update
  @note = Note.find(params[:id])

  if @note.update(note_params)
    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: turbo_stream.morph(
          "note_#{@note.id}",
          partial: "notes/note",
          locals: { note: @note }
        )
      end
      format.html { redirect_to @note }
    end
  else
    render :edit, status: :unprocessable_entity
  end
end

turbo_stream.morph neemt een doel-DOM-id en een partial. Het patcht dat ene element via morphdom — niet de hele pagina, alleen #note_42. De rest van de pagina blijft onaangeroerd. Geen Turbo Frame-grens die je vooraf moet definiëren, geen custom JavaScript, en de partial is dezelfde die al gebruikt wordt voor de initiële render.

Dit vervangt turbo_stream.replace — destructief, het doel verwijderen en verse HTML invoegen — door een chirurgische diff. Voor de meeste gevallen is het strikt beter: formulierinputs binnen het doel worden bewaard als hun waarden overeenkomen met de opnieuw gerenderde HTML, CSS-transities animeren correct omdat bestaande elementen in-place worden bijgewerkt in plaats van verwijderd en opnieuw ingevoegd, en Stimulus-controllerinstances op subelementen overleven de patch.

Wanneer Rails Turbo Morphing het verkeerde gereedschap is

Morphing lost niet elk realtime-probleem op. Ik wil eerlijk zijn over waar het tekortschiet.

Collaboratief bewerken: Twee gebruikers die tegelijkertijd hetzelfde record bewerken, zullen elkaars morphdom-patches in de weg zitten. morphdom probeert data-turbo-permanent-velden te bewaren, maar voor elke textarea of input zonder dat attribuut overschrijft een externe vernieuwing wat de huidige gebruiker aan het typen is. Voor echte collaboratieve bewerking heb je operational transform of CRDT nodig — dat is een fundamenteel andere architectuur, en geen hoeveelheid data-turbo-permanent maakt morphdom daarvoor geschikt.

Complexe JavaScript die de DOM bezit: Elke bibliotheek die volledige eigendom claimt over een DOM-deelboom — een rich text editor zoals Trix, een grafiekbibliotheek, een drag-and-drop kanban-bord — zal botsen met morphdom-patches op die deelboom. De oplossing is data-turbo-permanent op het root-element dat de bibliotheek bezit, gecombineerd met expliciete verversingslogica in de JavaScript. Dat is vaak meer werk dan het klinkt, en soms meer werk dan de custom ActionCable-laag behouden die je wilde vervangen.

Hoogfrequente updates: Als een record tientallen keren per seconde broadcast, genereert elke gemorphte vernieuwing toch nog een volledige serverrender en een netwerkaanvraag. Voor sensortelemetrie, live koerstickers of live veilingsystemen zijn fijnmazige WebSocket-berichten nog steeds het juiste antwoord. Solid Cable zonder Redis geeft je de cable-laag goedkoop wanneer je die nodig hebt.

Voor al het andere — dashboards, beheerpanelen, orderbeheer, notificatiefeeds, multi-user CRUD-interfaces — is Turbo Morphing in 2026 de juiste standaard. Het hergebruikt je bestaande renderingpijplijn, vereist vrijwel geen extra code, en elimineert een hele categorie bugs waarbij JavaScript-state afwijkt van de database.

Veelgestelde vragen

Wat is het verschil tussen Rails Turbo Morphing en Turbo Frames?

Turbo Frames vervangen een benoemde sectie van een pagina door een server-gerenderd fragment. Je definieert frame-grenzen vooraf in de HTML, en alleen de inhoud binnen dat frame wordt bijgewerkt bij elke interactie. Turbo Morphing vernieuwt de hele pagina maar gebruikt morphdom om alleen gewijzigde DOM-nodes te patchen — je hoeft dus geen grenzen vooraf te plannen. Morphing is eenvoudiger in te voeren in een bestaande applicatie; Frames zijn explicieter en makkelijker te doorgronden wanneer je nauwkeurige controle nodig hebt over wat er precies en wanneer wordt bijgewerkt.

Hoe werkt broadcasts_refreshes achter de schermen?

broadcasts_refreshes registreert after_create_commit-, after_update_commit- en after_destroy_commit-callbacks. Wanneer een van die wordt geactiveerd, broadcast het een Turbo Stream :refresh-actie naar een ActionCable-kanaal vernoemd naar de modelklasse en het id (bijv. Order:42). Verbonden browsers die via turbo_stream_from zijn geabonneerd ontvangen het signaal en activeren een paginavernieuwing. Als turbo_refreshes_with method: :morph is ingesteld in de layout, gebruikt die vernieuwing morphdom in plaats van volledige <body>-vervanging.

Bewaart Rails Turbo Morphing formulierstate?

Ja, met één belangrijke kanttekening. morphdom matcht elementen aan de hand van het id-attribuut en bewaart invoerwaarden voor elementen die het als hetzelfde knooppunt herkent tussen renders. Als de server een <input id="order_note"> rendert en de browser al een <input id="order_note"> heeft met niet-opgeslagen gebruikersinvoer, laat morphdom de bestaande waarde staan. Als het id verandert — wat gebeurt wanneer je een collectie opnieuw rendert en items van positie wisselen — kan morphdom elementen niet betrouwbaar matchen en kan het invoerstate overschrijven. Houd stabiele ids aan op formulierelementen waarbij je verwacht dat gebruikers typen tijdens een live sessie.

Kan ik Rails Turbo Morphing gebruiken naast Stimulus-controllers?

Ja, en de combinatie werkt uitstekend. Stimulus koppelt controllers aan DOM-elementen via CSS-selectors en activeert connect/disconnect-lifecycle-callbacks wanneer elementen worden toegevoegd of verwijderd. Wanneer morphdom de DOM patcht, activeert Stimulus disconnect voor verwijderde elementen en connect voor nieuwe. Bestaande elementen die morphdom in-place bijwerkt, behouden hun Stimulus-controllerinstances en alle interne state die die controllers bewaren. Je krijgt reactieve DOM-updates van de serverkant en fijnmazige JavaScript-logica via Stimulus zonder dat de twee elkaar in de weg zitten.

Besteed je engineeringtijd aan custom ActionCable JavaScript die logica dupliceert die al in je ERB-templates staat? TTB Software helpt Rails-teams met het vervangen van fragiele realtime JS-lagen door Turbo Morphing en conventionele server-side rendering. We doen dit al negentien jaar.

#rails-turbo-morphing #turbo-8-morphing #broadcasts-refreshes-rails #turbo-stream-morph #rails-8-turbo #hotwire-rails-morphing #turbo-morphdom-rails

Related Articles

Laatste sectie. Bel dan alsjeblieft.

Het is een telefoongesprek. Erger dan dat kan het niet worden.

Geen discovery-deck. Geen 45-minuten "kwalificatiegesprek." 30 minuten, jouw probleem, mijn mening. Als we een fit zijn weet je dat in minuut 12.

Directe lijn — Roger neemt zelf op
+31 6 5123 6132
Ma–vr, 09:00–18:00 CET · Nu beschikbaar

OF
info@ttb.software