rails-37-style-core-rails-views

star 0

Turbo Streams, partials, fragment caching, view helpers

Chwistophe By Chwistophe schedule Updated 3/6/2026

name: rails-37-style-core-rails-views description: Turbo Streams, partials, fragment caching, view helpers license: MIT

Views

Turbo Streams, partials over components, and server-rendered HTML.


Turbo Streams for Partial Updates

<%# app/views/cards/comments/create.turbo_stream.erb %>
<%= turbo_stream.before [@card, :new_comment],
    partial: "cards/comments/comment",
    locals: { comment: @comment } %>

<%= turbo_stream.update [@card, :new_comment],
    partial: "cards/comments/new",
    locals: { card: @card } %>

Morphing for Complex Updates

<%# app/views/cards/update.turbo_stream.erb %>
<%= turbo_stream.replace dom_id(@card, :card_container),
    partial: "cards/container",
    method: :morph,
    locals: { card: @card.reload } %>

Turbo Stream Subscriptions in Views

<%# app/views/cards/show.html.erb %>
<%= turbo_stream_from @card %>
<%= turbo_stream_from @card, :activity %>

<div data-controller="beacon" data-beacon-url-value="<%= card_reading_path(@card) %>">
  <%= render "cards/container", card: @card %>
  <%= render "cards/messages", card: @card %>
</div>

Partials Over ViewComponents

<%# Use standard partials %>
<%= render "cards/container", card: @card %>
<%= render "cards/display/perma/meta", card: @card %>

<%# With caching %>
<% cache card do %>
  <section id="<%= dom_id(card, :card_container) %>">
    <%= render "cards/container/content", card: card %>
  </section>
<% end %>

Fragment Caching Patterns

Basic Fragment Cache

<% cache card do %>
  <%= render "cards/preview", card: card %>
<% end %>

Composite Cache Keys

<%# Include context that affects output %>
<% cache [card, Current.user, timezone_from_cookie] do %>
  <%= render "cards/preview", card: card %>
<% end %>

Collection Caching

<%= render partial: "cards/preview",
           collection: @cards,
           cached: true %>

Cache with Touch Chains

class Comment < ApplicationRecord
  belongs_to :card, touch: true  # Invalidates card cache
end

class Card < ApplicationRecord
  belongs_to :board, touch: true  # Invalidates board cache
end

View Helpers: Stimulus-Integrated Components

Dialog Helper

# app/helpers/dialog_helper.rb
module DialogHelper
  def dialog_tag(id, **options, &block)
    options[:data] ||= {}
    options[:data][:controller] = "dialog #{options.dig(:data, :controller)}".strip
    options[:data][:action] = "click->dialog#closeOnOutsideClick keydown.esc->dialog#close"

    tag.dialog(id: id, **options, &block)
  end

  def dialog_close_button(**options)
    options[:data] ||= {}
    options[:data][:action] = "dialog#close"

    tag.button("Close", **options)
  end
end

Usage:

<%= dialog_tag "settings-dialog", class: "settings" do %>
  <h2>Settings</h2>
  <%= dialog_close_button class: "btn" %>
<% end %>

Auto-Submit Form Helper

# app/helpers/form_helper.rb
module FormHelper
  def auto_submit_form_with(**options, &block)
    options[:data] ||= {}
    options[:data][:controller] = "auto-submit #{options.dig(:data, :controller)}".strip
    options[:data][:auto_submit_delay_value] = options.delete(:delay) || 300

    form_with(**options, &block)
  end
end

Button Helpers

# app/helpers/button_helper.rb
module ButtonHelper
  def copy_button(content:, **options)
    options[:data] ||= {}
    options[:data][:controller] = "copy-to-clipboard"
    options[:data][:copy_to_clipboard_content_value] = content
    options[:data][:copy_to_clipboard_success_class] = "copied"
    options[:data][:action] = "click->copy-to-clipboard#copy"

    tag.button("Copy", **options)
  end
end

HTTP Caching in Views

Fresh When with ETags

# In controller
def show
  @card = Card.find(params[:id])
  fresh_when etag: [@card, Current.user, timezone_from_cookie]
end

Conditional GET

def index
  @cards = Card.recent
  fresh_when etag: @cards
end

Turbo Frame Patterns

Lazy Loading Frames

<%= turbo_frame_tag "notifications",
    src: notifications_path,
    loading: :lazy do %>
  <p>Loading notifications...</p>
<% end %>

Frame for Inline Editing

<%= turbo_frame_tag dom_id(card, :title) do %>
  <h1><%= card.title %></h1>
  <%= link_to "Edit", edit_card_path(card) %>
<% end %>

Frame-Targeted Forms

<%= turbo_frame_tag dom_id(@card, :edit) do %>
  <%= form_with model: @card do |f| %>
    <%= f.text_field :title %>
    <%= f.submit %>
  <% end %>
<% end %>

Broadcast Patterns

Model-Level Broadcasting

class Comment < ApplicationRecord
  after_create_commit -> {
    broadcast_append_to card, target: "comments"
  }

  after_destroy_commit -> {
    broadcast_remove_to card
  }
end

Scoped Broadcasting (Multi-Tenant)

# Always scope broadcasts by account
broadcast_to [Current.account, card], target: "comments"

Rendering Conventions

Prefer Locals Over Instance Variables

<%# Good - explicit dependencies %>
<%= render "cards/preview", card: card, draggable: true %>

<%# Avoid - implicit dependencies %>
<%= render "cards/preview" %>  <%# Uses @card implicitly %>

Partial Naming

app/views/
├── cards/
│   ├── _card.html.erb           # Single card
│   ├── _preview.html.erb        # Card preview/summary
│   ├── _container.html.erb      # Card with wrapper
│   ├── _form.html.erb           # Card form
│   └── container/
│       └── _content.html.erb    # Nested partial

DOM ID Conventions

<%# Use Rails dom_id helper %>
<div id="<%= dom_id(card) %>">           <%# card_123 %>
<div id="<%= dom_id(card, :preview) %>"> <%# preview_card_123 %>
<div id="<%= dom_id(card, :comments) %>"> <%# comments_card_123 %>
Install via CLI
npx skills add https://github.com/Chwistophe/agent-skills-unofficial-37-signals-rails-way-fizzy --skill rails-37-style-core-rails-views
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator