Ruby on Rails 7: Hotwire ta Turbo dlia Reaktyvnykh Dodatkiv

Povnyi posibnyk z Hotwire ta Turbo v Rails 7. Stvorennia reaktyvnykh dodatkiv bez JavaScript z Turbo Drive, Frames ta Streams.

Posibnyk z Hotwire ta Turbo dlia Ruby on Rails 7

Rails 7 zrobyla revoliutsiiu u veb-rozrobtsi, intehruvavshi Hotwire za zamovchuvannia. Tsei stek dozvoliaie stvoriuvaty vysoce reaktyvni dodatky bez napysannia zhodnoi riadka vlasnoho kodu JavaScript. Turbo Drive, Turbo Frames ta Turbo Streams zaminiuiut tradytsiini pidkhody SPA filosofiieiu "HTML cherez drit".

Chomu Hotwire?

Hotwire drastychno zmenshuie skladnist frontendu. Ne potribno React chy Vue dlia dynamichnykh interfejsiv: server nadsilaie hotovyi do vykorystannia HTML, a Turbo avtomatychno opratsoovuie onovlennia DOM.

Rozuminnia Arkhitektury Hotwire

Hotwire skladaietsia z triokh komplementarnykh tekhnolohii, shcho pratsiuiut razom dlia zabezpechennia plavnoho dosvidu korystuvacha bez typovoi skladnosti JavaScript-freimvorkiv.

Turbo Drive pryskoruie navigatsiiu, perekhopliuiuchy klyky na posylannia ta vidpravky form. Zamist perenavantazhennia vsiiei storinky, zaminiuietsia lyshe vmist <body>, zberigaiuchy kontekst JavaScript ta CSS.

Turbo Frames rozbyvaiut storinky na nezalezhni sektsii. Kozhen frejm mozhe onovliuvatysia okremo, shcho dozvoliaie tsilespriamovani vzaiemodii bez vplyvu na reshtu storinky.

Turbo Streams zabezpechuiut onovlennia v realnomu chasi cherez WebSocket abo u vidpovid na HTTP-zapyty. Visim CRUD-dii dostupni dlia deklaratyvnoi manipuliatsii DOM.

ruby
# Gemfile
# Installing Turbo Rails (included by default in Rails 7+)
gem 'turbo-rails'

Dlia isnuiuchykh proektiv instaliatsiia vymahaie lyshe odniiei komandy.

bash
# installation.sh
# Installing Turbo in an existing Rails project
rails turbo:install

# Verify the JavaScript import is present
cat app/javascript/application.js
# Should contain: import "@hotwired/turbo-rails"

Pochatkova Konfiguratsiia Turbo Drive

Turbo Drive uvimkneno za zamovchuvannia v Rails 7. Usia navigatsiia avtomatychno staie "turbo" bez dodatkovi konfiguratsiiu. Povedinku mozhna nalashtuvannia cherez data-atrybuty.

erb
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <meta name="turbo-cache-control" content="no-preview">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>
  <body>
    <!-- Progress bar during navigation -->
    <div class="turbo-progress-bar"></div>
    <%= yield %>
  </body>
</html>

Turbo Drive mozhna vymknuty dlia konkretnykh posylan chy form za potreby.

erb
<!-- app/views/pages/examples.html.erb -->
<!-- Link with Turbo enabled (default) -->
<%= link_to "Dashboard", dashboard_path %>

<!-- Disable Turbo for a specific link -->
<%= link_to "Download PDF", export_path, data: { turbo: false } %>

<!-- Disable Turbo for a form (file upload for example) -->
<%= form_with model: @document, data: { turbo: false } do |f| %>
  <%= f.file_field :attachment %>
  <%= f.submit "Upload" %>
<% end %>

<!-- Turbo with confirmation -->
<%= link_to "Delete", item_path(@item),
    data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
Kesh Turbo Drive

Turbo Drive keshuie vidvidani storinky. Atrybut data-turbo-track="reload" na resursakh zmushue povne perenavantazhennia, yakshcho faily CSS/JS zminiatsia.

Turbo Frames dlia Tsilespriamovanykh Onovlen

Turbo Frames vyznachaiut zony storinky, shcho onovliuiutsia nezalezhno. Kozhen frejm maie unikalnyi identyfikator i reahuie lyshe na vidpovidi, shcho mistyat vidpovidnyi frejm.

erb
<!-- app/views/messages/index.html.erb -->
<h1>Messages</h1>

<!-- Frame for the message list -->
<turbo-frame id="messages_list">
  <div id="messages">
    <%= render @messages %>
  </div>

  <%= link_to "Load more", messages_path(page: @next_page) %>
</turbo-frame>

<!-- Frame for the creation form -->
<turbo-frame id="new_message">
  <%= render "form", message: Message.new %>
</turbo-frame>

Vidpovid servera povianna mistyty frejm z tym samym identyfikatorom, shchob onovlennia pratsiiuvalo.

erb
<!-- app/views/messages/_message.html.erb -->
<!-- Partial for an individual message -->
<turbo-frame id="<%= dom_id(message) %>">
  <div class="message">
    <p><%= message.content %></p>
    <span class="metadata">
      By <%= message.author.name %> - <%= time_ago_in_words(message.created_at) %>
    </span>

    <!-- These links stay within the frame -->
    <%= link_to "Edit", edit_message_path(message) %>
    <%= button_to "Delete", message_path(message), method: :delete %>
  </div>
</turbo-frame>

Lienyve Zavantazhennia z Turbo Frames

Frejmy mozhut zavantazhuvaty svii vmist asynkhronno za dopomohoiu atrybutu src. Pochatkovyi vmist vidobrazhaietsia pid chas zavantazhennia.

erb
<!-- app/views/dashboard/show.html.erb -->
<h1>Dashboard</h1>

<!-- Statistics loaded asynchronously -->
<turbo-frame id="stats" src="<%= dashboard_stats_path %>">
  <div class="loading-placeholder">
    <p>Loading statistics...</p>
  </div>
</turbo-frame>

<!-- Notifications loaded when visible (lazy loading) -->
<turbo-frame id="notifications"
             src="<%= notifications_path %>"
             loading="lazy">
  <p>Loading notifications...</p>
</turbo-frame>

<!-- Recent activity with periodic refresh -->
<turbo-frame id="recent_activity"
             src="<%= activity_path %>"
             data-controller="auto-refresh"
             data-auto-refresh-interval-value="30000">
  <%= render "activity/placeholder" %>
</turbo-frame>

Kontroler vidpovidaie podanniiam, shcho mistyat lyshe zapytuvany frejm.

ruby
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  def stats
    @user_count = User.count
    @message_count = Message.count
    @active_today = User.where("last_seen_at > ?", 24.hours.ago).count

    # Partial rendering for the frame
    render partial: "dashboard/stats"
  end
end
erb
<!-- app/views/dashboard/_stats.html.erb -->
<turbo-frame id="stats">
  <div class="stats-grid">
    <div class="stat-card">
      <h3><%= @user_count %></h3>
      <p>Users</p>
    </div>
    <div class="stat-card">
      <h3><%= @message_count %></h3>
      <p>Messages</p>
    </div>
    <div class="stat-card">
      <h3><%= @active_today %></h3>
      <p>Active today</p>
    </div>
  </div>
</turbo-frame>

Готовий до співбесід з Ruby on Rails?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Turbo Streams dlia Onovlen v Realnomu Chasi

Turbo Streams proponuie visim dii dlia manipuliatsii DOM: append, prepend, replace, update, remove, before, after ta morph. Tsi dii mozhut buti zapushcheni cherez vidpovid HTTP abo WebSocket.

ruby
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def create
    @message = current_user.messages.build(message_params)

    respond_to do |format|
      if @message.save
        # Turbo Stream response to add the message
        format.turbo_stream do
          render turbo_stream: [
            turbo_stream.prepend("messages", @message),
            turbo_stream.update("message_count", partial: "messages/count"),
            turbo_stream.replace("new_message", partial: "messages/form",
                                 locals: { message: Message.new })
          ]
        end
        format.html { redirect_to messages_path, notice: "Message created" }
      else
        format.turbo_stream do
          render turbo_stream: turbo_stream.replace(
            "new_message",
            partial: "messages/form",
            locals: { message: @message }
          )
        end
        format.html { render :new, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @message = Message.find(params[:id])
    @message.destroy

    respond_to do |format|
      # Removal with animation
      format.turbo_stream { render turbo_stream: turbo_stream.remove(@message) }
      format.html { redirect_to messages_path, notice: "Message deleted" }
    end
  end
end

Spetsialni Shablony Turbo Stream

Dlia skladnishykh vidpovidei shablon .turbo_stream.erb proponuie bilshu hnuchkist.

erb
<!-- app/views/messages/create.turbo_stream.erb -->
<!-- Prepend the new message to the list -->
<%= turbo_stream.prepend "messages" do %>
  <%= render @message %>
<% end %>

<!-- Update the counter -->
<%= turbo_stream.update "message_count" do %>
  <span><%= Message.count %> messages</span>
<% end %>

<!-- Reset the form -->
<%= turbo_stream.replace "new_message" do %>
  <%= render "form", message: Message.new %>
<% end %>

<!-- Display a flash notification -->
<%= turbo_stream.prepend "flash_messages" do %>
  <div class="flash flash-success" data-controller="auto-dismiss">
    Message sent successfully!
  </div>
<% end %>
Format Vidpovidi

Vidpovidi Turbo Stream povynni maty content-type text/vnd.turbo-stream.html. Rails opratsoovuie tse avtomatychno z respond_to ta formatom turbo_stream.

Broadcasting v Realnomu Chasi z Action Cable

Turbo Streams diisnio blyskuche pratsiuie z WebSocket broadcasting. Korystuvachi otrymaiut onovlennia mytt'ievo bez pollinhu.

ruby
# app/models/message.rb
class Message < ApplicationRecord
  belongs_to :room
  belongs_to :author, class_name: "User"

  # Automatic broadcast after creation
  after_create_commit do
    broadcast_append_to(
      room,
      target: "messages",
      partial: "messages/message",
      locals: { message: self }
    )
  end

  # Broadcast after update
  after_update_commit do
    broadcast_replace_to(
      room,
      target: dom_id(self),
      partial: "messages/message",
      locals: { message: self }
    )
  end

  # Broadcast after deletion
  after_destroy_commit do
    broadcast_remove_to(room, target: dom_id(self))
  end
end

Podannia pidpysuietsia na vidpovidnyi strim za dopomohoiu helpera turbo_stream_from.

erb
<!-- app/views/rooms/show.html.erb -->
<h1><%= @room.name %></h1>

<!-- Subscribe to the WebSocket stream -->
<%= turbo_stream_from @room %>

<!-- Container for messages -->
<div id="messages">
  <%= render @room.messages.order(created_at: :asc) %>
</div>

<!-- New message form -->
<%= form_with model: [@room, Message.new], id: "new_message" do |f| %>
  <%= f.text_area :content, placeholder: "Your message..." %>
  <%= f.submit "Send" %>
<% end %>

Asynchronnyi Broadcasting dlia Vazhkykh Operatsii

Shchob ne blokuvaty zapyty, broadcasting mozhe vykonuvatysia u fonovomu rezhymi.

ruby
# app/models/report.rb
class Report < ApplicationRecord
  belongs_to :user

  after_create_commit :generate_async

  private

  def generate_async
    GenerateReportJob.perform_later(self)
  end
end
ruby
# app/jobs/generate_report_job.rb
class GenerateReportJob < ApplicationJob
  queue_as :default

  def perform(report)
    # Simulating a long operation
    report.update!(status: "processing")

    # Notify user of start
    report.broadcast_replace_to(
      report.user, :reports,
      target: dom_id(report),
      partial: "reports/report"
    )

    # Generate the report
    result = ReportGenerator.new(report).generate
    report.update!(content: result, status: "completed")

    # Notify user of completion
    report.broadcast_replace_to(
      report.user, :reports,
      target: dom_id(report),
      partial: "reports/report"
    )
  end
end

Prosunuti Paterny z Turbo

Redahuvannia Form v Riadku

Poshyrenyi patern poliahaie u zamini statychnoho vmistu formoiu redahuvannia v riadku.

erb
<!-- app/views/tasks/_task.html.erb -->
<turbo-frame id="<%= dom_id(task) %>">
  <div class="task">
    <span class="task-content"><%= task.content %></span>
    <div class="task-actions">
      <%= link_to "Edit", edit_task_path(task) %>
      <%= button_to "Delete", task_path(task), method: :delete,
          data: { turbo_confirm: "Delete this task?" } %>
    </div>
  </div>
</turbo-frame>
erb
<!-- app/views/tasks/edit.html.erb -->
<turbo-frame id="<%= dom_id(@task) %>">
  <%= form_with model: @task do |f| %>
    <%= f.text_field :content, autofocus: true %>
    <div class="form-actions">
      <%= f.submit "Save" %>
      <%= link_to "Cancel", task_path(@task) %>
    </div>
  <% end %>
</turbo-frame>

Za zamovchuvannia, posylannia v frejmi zalyshaiutsia v mezhakh tsoho frejmu. Atrybut data-turbo-frame dozvoliaie tsilyty na inshyi frejm abo na vsiu storinku.

erb
<!-- app/views/search/_results.html.erb -->
<turbo-frame id="search_results">
  <ul>
    <% @results.each do |result| %>
      <li>
        <!-- This link navigates the entire page, not the frame -->
        <%= link_to result.title, result_path(result),
            data: { turbo_frame: "_top" } %>
      </li>
    <% end %>
  </ul>
</turbo-frame>
erb
<!-- app/views/layouts/_sidebar.html.erb -->
<aside>
  <!-- This frame loads content and updates main -->
  <turbo-frame id="sidebar_nav" target="main_content">
    <%= render "navigation" %>
  </turbo-frame>
</aside>

<main>
  <turbo-frame id="main_content">
    <%= yield %>
  </turbo-frame>
</main>
Vidladka Turbo

U seredovyshchi rozrobky vidkryite konsol JavaScript ta vvedite Turbo.setProgressBarDelay(0), shchob odrazhu pobachyty smuzhu prohresu. Turbo.session.drive = false tymchasovo vymykaie Turbo Drive.

Obrobka Pomylok ta Stany Zavantazhennia

Dobryi UX vymahaie obrobky staniv zavantazhennia ta merezhnykh pomylok.

erb
<!-- app/views/shared/_form_with_loading.html.erb -->
<%= form_with model: @model,
    data: {
      controller: "form-loading",
      action: "turbo:submit-start->form-loading#disable turbo:submit-end->form-loading#enable"
    } do |f| %>
  <%= yield f %>

  <button type="submit" data-form-loading-target="submit">
    <span data-form-loading-target="text">Save</span>
    <span data-form-loading-target="spinner" class="hidden">
      <svg class="animate-spin h-5 w-5"><!-- spinner SVG --></svg>
    </span>
  </button>
<% end %>
app/javascript/controllers/form_loading_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["submit", "text", "spinner"]

  disable() {
    this.submitTarget.disabled = true
    this.textTarget.classList.add("hidden")
    this.spinnerTarget.classList.remove("hidden")
  }

  enable() {
    this.submitTarget.disabled = false
    this.textTarget.classList.remove("hidden")
    this.spinnerTarget.classList.add("hidden")
  }
}

Obrobka Pomylok Servera

ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound do |exception|
    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: turbo_stream.replace(
          "main_content",
          partial: "shared/not_found"
        )
      end
      format.html { render "errors/not_found", status: :not_found }
    end
  end
end

Optymizatsiia Produktyvnosti

Kilka tekhnologij zabezpechuiut produktyvni dodatky Turbo.

ruby
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  # Avoid N+1 queries with eager loading
  def index
    @messages = Message.includes(:author, :room)
                       .order(created_at: :desc)
                       .page(params[:page])
  end

  # Fragment caching for static elements
  def show
    @message = Message.find(params[:id])
    fresh_when @message
  end
end
erb
<!-- app/views/messages/_message.html.erb -->
<!-- Partial-level caching -->
<% cache message do %>
  <turbo-frame id="<%= dom_id(message) %>">
    <div class="message">
      <%= message.content %>
      <small>By <%= message.author.name %></small>
    </div>
  </turbo-frame>
<% end %>

Vysnovok

Hotwire ta Turbo transformuiut rozrobku na Rails, usuvaiuchy skladnist tradytsiinykh JavaScript-freimvorkiv. Turbo Drive pryskoruie navigatsiiu, Turbo Frames dozvoliaiut tsilespriamovani onovlennia, a Turbo Streams proponuie potuzhni mozhlyvosti v realnomu chasi.

Chek-lyst dlia Pochatku Roboty z Hotwire

  • Vykorystovuvaty Rails 7+ dlia vbudovanoii integratsiiu z Hotwire
  • Zrozumity try komponenty: Drive, Frames ta Streams
  • Identyfikuvaty zony storinky, shcho skorystaiiut vid Turbo Frames
  • Vykorystovuvaty respond_to z format.turbo_stream v kontrolerakh
  • Vprovadyty broadcasting dlia funktsii realnoho chasu
  • Poiednaty zi Stimulus dlia prostykh JavaScript-vzaiemodii

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Tsei pidkhid "HTML cherez drit" dozvoliaie stvoriuvaty suchasni, reaktyvni dodatky, zberigaiuchy prostotu ta produktyvnist, shcho roblyat Ruby on Rails takym potuzhnym. Rezultat: menshe kodu dlia pidtrymky, chudova produktyvnist ta optymalnyi dosvid rozrobnyka.

Теги

#ruby on rails
#hotwire
#turbo
#turbo frames
#turbo streams

Поділитися