The frame is always going to be there, regardless of what you get as a response. If there is a frame with the same id
in the response than it will be updated, if not, you will get frame missing error. To see what you actually get as a response you have to look at the network tab:
https://stackoverflow.com/a/75916578/207090
Just to make it clear, if you have this on the index page:
<!-- app/views/tweets/index.html.erb -->
<turbo-frame id="new_tweet">
<a href="/tweets/new">New tweet</a>
</turbo-frame>
Then you need the same frame on the page that you're requesting or redirecting to:
<!-- app/views/tweets/new.html.erb -->
<turbo-frame id="new_tweet">
<form action="/tweets" method="post">
<textarea name="tweet[content]"></textarea>
<input type="submit" name="commit" value="Create Tweet">
</form>
</turbo-frame>
As I understood, your turbo frame is in _tweet.html.erb
partial and I don't see that partial being rendered from your show
page.
Here is my tweetster clone. Create new tweets from the index page in a frame, then insert newly created tweet with turbo stream. Same thing for replies and replies on replies and it works forever.
There are also so many ways to set this up, this is just one that came to mind.
# db/migrate/20230413231722_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[7.0]
def change
create_table :tweets do |t|
t.text :content
t.references :tweet
end
end
end
# app/models/tweet.rb
class Tweet < ApplicationRecord
validates :content, presence: true
has_many :replies, class_name: "Tweet", dependent: :destroy
belongs_to :tweet, optional: true
def reply?
tweet_id.present?
end
end
# config/routes.rb
resources :tweets do
get :reply, on: :member
end
You can pretty much ignore this controller, I've only added reply
action and turbo_stream
format to create
, because after the tweet is created we're done with the frame and we have to make our great escape from it:
# app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
def index # GET /tweets
@tweets = Tweet.where(tweet_id: nil) # ignore replies
end
def new = tweet # GET /tweets/new
# show and edit are not used
def show = tweet # GET /tweets/1
def edit = tweet # GET /tweets/1/edit
def reply # GET /tweets/1/reply
render partial: "form", locals: {tweet: tweet.replies.new}
end
def create # POST /tweets
respond_to do |f|
if tweet(tweet_params).save
# NOTE: render `create.turbo_stream.erb` where we'll handle some
# things regarding turbo_frame
f.turbo_stream
f.html { redirect_to tweet_url(tweet), notice: "Saved." }
else
f.html { render tweet.new_record? ? :new : :edit, status: 422 }
end
end
end
alias_method :update, :create # PATCH/PUT /tweets/1
def destroy # DELETE /tweets/1
tweet.destroy
respond_to do |f|
f.turbo_stream { render turbo_stream: turbo_stream.remove(tweet) }
f.html { redirect_to tweets_url, notice: "Destroyed." }
end
end
private
def tweet attributes = {}
@tweet ||= begin
model = params[:id] ? Tweet.find(params[:id]) : Tweet.new
model.attributes = attributes
model
end
end
def tweet_params = params.require(:tweet).permit!
end
# app/views/tweets/index.html.erb
<%= link_to "New tweet", new_tweet_path, data: {turbo_frame: :new_tweet} %>
<%= turbo_frame_tag :new_tweet %>
<%= tag.div id: :tweets, class: "grid gap-4 mt-4" do %>
<%= render @tweets.order(id: :desc) %>
<% end %>
# app/views/tweets/new.html.erb
<%= render "form", tweet: @tweet %>
# app/views/tweets/_form.html.erb
<%# NOTE: figure out correct turbo frame id for replies and tweets %>
<% frame_id = tweet.reply? ? dom_id(tweet.tweet, :new_reply) : dom_id(tweet) %>
<%= turbo_frame_tag frame_id do %>
<%= form_with model: tweet, class: {"ml-8": tweet.reply?} do |f| %>
<%= tag.div safe_join(f.object.errors.full_messages, tag.br), class: "text-red-500" if f.object.errors.any? %>
<%= f.hidden_field :tweet_id %>
<%= f.text_area :content, placeholder: (tweet.reply? ? "reply..." : "new tweet..."), class: "rounded-xl w-full" %>
<%= f.button "Save", class: "font-bold" %>
<% end %>
<% end %>
# app/views/tweets/_tweet.html.erb
<%= tag.div id: dom_id(tweet), class: ["grid gap-4", {"ml-8": tweet.reply?}] do %>
<%= tag.div class: "flex justify-between rounded-xl shadow border p-4" do %>
<%= tweet.content %>
<%= tag.div class: "flex gap-2" do %>
<%= link_to "reply", reply_tweet_path(tweet), data: {turbo_frame: dom_id(tweet, :new_reply)} %>
<%= button_to "delete", tweet_path(tweet), method: :delete, class: "hover:text-red-500" %>
<% end %>
<% end %>
<%= turbo_frame_tag dom_id(tweet, :new_reply), class: "contents" %>
<%= tag.div id: dom_id(tweet, :replies) do %>
<%= render tweet.replies %>
<% end %>
<% end %>
# i'm only using tag.div for syntax highlight for SO (erb + html tags don't mix well)
This is probably the main part where you need to make things happen outside of the frame, maybe replace a frame with a different frame, maybe append a flash alert somewhere, etc:
# app/views/tweets/create.turbo_stream.erb
<%# there is probably a way to make it the same for tweets and replies %>
<% if @tweet.reply? %>
<% parent_tweet = @tweet.tweet %>
<%= turbo_stream.update dom_id(parent_tweet, :new_reply) %>
<%= turbo_stream.append dom_id(parent_tweet, :replies), @tweet %>
<% else %>
<#% this v is for tweets and same ^ for replies %>
<%# NOTE: this `update` will remove content from turbo_frame %>
<%# but you can add whatever updates you need here %>
<%= turbo_stream.update :new_tweet %>
<%# NOTE: this will add new tweet to `<div id="tweets">` %>
<%= turbo_stream.prepend :tweets, @tweet %>
<% end %>
If you want to "just" redirect out of the frame:
https://stackoverflow.com/a/75750578/207090