ruby erb 64 lines · 4 tabs

Undo delete with a Turbo Stream “restore” action

Shared by codesnips Jan 2026
4 tabs
class DocumentsController < ApplicationController
  before_action :set_document, only: :destroy

  def destroy
    @document.discard!

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to documents_path, notice: "Document deleted." }
    end
  end

  def restore
    @document = Document.unscoped.find(params[:id])
    @document.undiscard!

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to documents_path, notice: "Document restored." }
    end
  end

  private

  def set_document
    @document = Document.find(params[:id])
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows how a Rails controller pairs a soft-delete with a Turbo Stream flash that offers a one-click undo, so a mistaken deletion can be reversed without a full-page round trip.

The documents_controller.rb tab handles both directions of the flow. destroy calls discard! rather than destroy!, which sets a discarded_at timestamp instead of removing the row. Because the record still exists, a restore is always possible. The response renders a Turbo Stream that removes the row from the list (turbo_stream.remove) and prepends a flash into the flash frame. That flash carries an undo link pointing at the member restore route. The restore action calls undiscard!, clears the timestamp, and streams the row back into position with turbo_stream.prepend plus a confirmation flash. Note how each action responds to format.turbo_stream while keeping an format.html fallback with redirect_to, so the feature degrades gracefully when JavaScript is unavailable.

The Document model uses the discard gem's Discard::Model concern, which supplies discard!, undiscard!, and the kept/discarded scopes. Setting default_scope { kept } means ordinary queries never see soft-deleted rows, which is why restore must load the record through unscoped — the default scope would otherwise raise RecordNotFound for a discarded document. This is a classic pitfall with soft-delete: the scope that hides deleted rows also hides them from the very code trying to bring them back.

The destroy.turbo_stream.erb and restore.turbo_stream.erb tabs are the view halves. They use turbo_stream helpers with partials so the same _document and _flash partials render everywhere. The undo link uses button_to with method: :patch so it issues a real state-changing request rather than a GET.

The trade-off is that discarded rows accumulate and need periodic purging, and unique indexes must account for discarded_at to allow re-creating a "deleted" record. In exchange, undo becomes trivial and auditable, and the UI stays responsive because only two small stream fragments cross the wire.


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.

Undo delete with a Turbo Stream “restore” action — share card
Link copied