ruby erb 77 lines · 4 tabs

Turbo Streams: append server-side validation warnings

Shared by codesnips Jan 2026
4 tabs
class SubscriptionsController < ApplicationController
  def new
    @subscription = current_account.subscriptions.new
  end

  def create
    @subscription = current_account.subscriptions.new(subscription_params)
    @warnings = @subscription.soft_warnings

    respond_to do |format|
      if @subscription.save
        format.turbo_stream # renders create.turbo_stream.erb with warnings
        format.html { redirect_to @subscription, notice: "Subscription created" }
      else
        format.turbo_stream do
          render :create, status: :unprocessable_entity
        end
        format.html { render :new, status: :unprocessable_entity }
      end
    end
  end

  private

  def subscription_params
    params.require(:subscription).permit(:plan, :seats, :billing_email)
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows how server-side validation warnings are streamed back into a form without a full page reload, using Turbo Streams. The pattern separates hard errors (which block submission) from soft warnings (which the user can acknowledge and submit anyway), a distinction plain HTML forms cannot express on their own.

In SubscriptionsController, the create action attempts to save the record and branches on the result. When @subscription.save fails, it responds to the turbo_stream format by rendering a create.turbo_stream.erb template rather than re-rendering the whole page, so only the affected DOM regions are updated. The controller also computes @warnings from a service object even on a successful save, which is the key idea: warnings are advisory and do not fail validation. Falling back to format.html keeps the endpoint usable for non-Turbo clients and progressive enhancement.

The model in Subscription model keeps real invariants in validates, while soft_warnings returns non-blocking advisories — a high price tier, or a trial ending soon. Because these are computed rather than stored, they never pollute errors and never prevent a save. This split matters: mixing warnings into errors.add would incorrectly block the form, and rendering them as blocking messages would train users to ignore them.

The create.turbo_stream.erb template drives the DOM diff. It uses turbo_stream.replace to swap the form partial (so field-level errors render inline) and turbo_stream.append to add each warning into a dedicated #warnings container, keyed by DOM id so repeated submissions replace rather than stack duplicates. The _warning partial wraps each message with dom_id, letting a later stream target and update it precisely.

The trade-off is that the server becomes responsible for HTML fragments and DOM ids, which couples backend templates to markup structure. In exchange there is no client-side validation duplication and the source of truth stays in Ruby. This approach fits forms where rules are non-trivial, change often, or depend on account state that only the server knows. A common pitfall is forgetting the id: on appended nodes, which causes warnings to accumulate on every keystroke or resubmit.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Turbo Streams: append server-side validation warnings — share card
Link copied