Open brendon opened 2 years ago
I managed to get there in the end. Is this the most efficient way to write it?
def update
respond_with online_newsletter,
location: [:admin, online_newsletter] do |format|
if online_newsletter.update(online_newsletter_params)
format.turbo_stream {
render turbo_stream: turbo_stream.update("header", partial: "header")
}
else
format.html { render 'edit', status: :unprocessable_entity }
end
end
end
I'm using this guy's responder but I think it needs to be modified to play nice with the flash responder. He's only trying to deal with Devise issues whereas I'm using Responders generally (because why on Earth would people want to manually do this in all their controller actions?).
https://www.driftingruby.com/episodes/hotwire-turbo-replacing-rails-ujs
So far I'm getting some mileage out of this:
class ApplicationResponder < ActionController::Responder
module TurboFlashResponder
include(Responders::FlashResponder)
def to_turbo_stream
to_html
end
end
include(TurboResponder)
include(TurboFlashResponder)
include(Responders::HttpCacheResponder)
end
# https://gorails.com/episodes/devise-hotwire-turbo
module TurboResponder
def navigation_behavior(error)
if get?
raise error
elsif has_errors? && default_action
render(rendering_options.merge(formats: [:turbo_stream, :html], status: :unprocessable_entity))
else
redirect_to navigation_location
end
end
def options
super.merge(formats: [:turbo_stream, :html])
end
end
Thanks @ClayShentrup, that looks like a great start. @carlosantoniodasilva, do you think there is appetite to make turbo responses a first class citizen in responders
?
In case it's helpful for anyone else, this is what's working for me to make Turbo work with Responders.
Inside application_responder.rb:
def initialize(controller, resources, options = {})
super
if [:js, :turbo_stream].include?(format)
options[:formats] ||= request.formats.map(&:symbol)
end
end
alias :to_turbo_stream :to_html
@rafaelfranca what's the resolution here? is the plan not to offer a to_turbo_stream
?
Not sure if this should have been closed. In my excursion into broadcast turbo streams I'm finding that I want to render :no_content
for actions (like create
) and then immediately broadcast the page changes to all users from the controller using something like:
article_html = render_to_string partial: 'online_newsletters/article', locals: { article: article }
Turbo::StreamsChannel.broadcast_before_to online_newsletter, target: article.subsequent, html: article_html
I'm not a fan of broadcasting from models. I also struggled with the built-in rendering functionality with Turbo::StreamsChannel
as it hard-codes the use of ApplicationController
and I use a couple of different FormBuilder
's in different areas of my app. I know I can create my own stream channel and customise all this, but in the end render_to_string
works well as it maintains the context of the request, and that's ok in my case as I want all users watching the editing interface to get the same updates and there's no per-user differences. That's a side-track anyway.
Responders
treats the turbo_stream
format as an api style format and wants to render a status: :created
with a location
by default. Ideally it should behave more like a hybrid to_html
but if no template is found and if it's a post it just renders head :no_content
. That's my opinion anyway. I think the main problem is that turbo-streams are so versatile that everyone will probably want to do something slightly different.
I am going to reopen this to remind me to look into turbo_stream
again. Now that Responders and Devise are working fine with Turbo for the most common usage (via HTML navigation and such), I want to make sure it works with turbo streams (i.e. responding as turbo stream) as expected too, because it does seem like it's may not be.
Thanks @carlosantoniodasilva, let me know if you need to test anything in the wild :) For now in my respond_with
I just have this:
if article.save
format.turbo_stream { }
Turbo::StreamsChannel.broadcast_before_to online_newsletter, target: article.subsequent,
html: render_to_string(partial: 'online_newsletters/article')
else
format.html { render 'new', status: :unprocessable_entity }
end
Including format.turbo_stream { }
forces the empty response.
I'm using this guy's responder but I think it needs to be modified to play nice with the flash responder. He's only trying to deal with Devise issues whereas I'm using Responders generally (because why on Earth would people want to manually do this in all their controller actions?).
https://www.driftingruby.com/episodes/hotwire-turbo-replacing-rails-ujs
So far I'm getting some mileage out of this:
class ApplicationResponder < ActionController::Responder module TurboFlashResponder include(Responders::FlashResponder) def to_turbo_stream to_html end end include(TurboResponder) include(TurboFlashResponder) include(Responders::HttpCacheResponder) end # https://gorails.com/episodes/devise-hotwire-turbo module TurboResponder def navigation_behavior(error) if get? raise error elsif has_errors? && default_action render(rendering_options.merge(formats: [:turbo_stream, :html], status: :unprocessable_entity)) else redirect_to navigation_location end end def options super.merge(formats: [:turbo_stream, :html]) end end
Reduced to :
# frozen_string_literal: true
class ApplicationResponder < ActionController::Responder
include Responders::FlashResponder
include Responders::HttpCacheResponder
self.error_status = :unprocessable_entity
self.redirect_status = :see_other
alias :to_turbo_stream :to_html
end
@n-rodriguez that is great :)
Can you clarify what's your use for the alias :to_turbo_stream :to_html
? I assume you are adding specific turbo_stream responses to devise related actions/controllers? (or maybe others, which is why you might require it?)
I haven't circled back yet on that from responder's perspective, but I'm guessing I might have to add something along those lines to responders itself so it can handle those appropriately.
@carlosantoniodasilva, I think alias :to_turbo_stream :to_html
is used to make use e.g. of FlashResponder::to_html
method which has to be called to set the flash message appropriately.
But for me it was not enough. Flash messages have to be displayed immediately and not on the next request.
Thus i had to change the set_flash_now?
method.
# config/initializers/responders_override.rb
# frozen_string_literal: true
module Responders
module FlashResponder
def set_flash_now?
@flash_now == true || format == :js || format == :turbo_stream ||
(default_action && (has_errors? ? @flash_now == :on_failure : @flash_now == :on_success))
end
end
end
Without alias :to_turbo_stream :to_html
I found that responders
wouldn't look for a .turbo_stream.erb
file in the error scenario if I called something like:
format.turbo_stream { render 'errors', status: :unprocessable_entity }
I also needed alias :to_turbo_stream :to_html
in my responder.
When a request with a turbo stream format comes in:
Started POST "/2023-10-16/workdays" for ::1 at 2023-10-30 18:37:40 +0100
18:37:40 web.1 | Processing by WorkdaysController#create as TURBO_STREAM
it would respond with this:
2023-10-30 18:37:33 +0100 Read: #<NoMethodError: undefined method `bytesize' for #<ActiveModel::Error attribute=base, type=invalid, options={}>
18:37:33 web.1 |
18:37:33 web.1 | next if part.nil? || (byte_size = part.bytesize).zero?
18:37:33 web.1 | ^^^^^^^^^>
Now with this setup it will correctly render and return the new.turbo_stream.erb
file on the POST (if there are errors on the model):
class ApplicationController < ActionController::Base
self.responder = ApplicationResponder
respond_to :turbo_stream, :html
end
# and
class ApplicationResponder < ActionController::Responder
include Responders::FlashResponder
# Configure default status codes for responding to errors and redirects.
self.error_status = :unprocessable_entity
self.redirect_status = :see_other
alias_method :to_turbo_stream, :to_html
end
I find myself working on this again :D I think I've come closer to something that works. My scenario is this:
respond_to :turbo_stream, :html
def create
article.save
respond_with article
end
In my use case I render new.html
which is a <turbo-frame>
tag. My frame in this case is just a modal interface.
When submitted, if the model is valid, I want to render create.turbo_stream.erb
which contains some non-trivial Turbo Stream page updates. In the case of an error I just want to display new.html.erb
again. Reponders doesn't handle this currently without lots of overriding.
I've come up with the following as modifications to ActionController::Responder
to make this work:
self.error_status = :unprocessable_entity
def to_turbo_stream
turbo_render
rescue ActionView::MissingTemplate => e
navigation_behavior(e)
end
protected
def turbo_render
if @default_response
@default_response.call(options)
elsif !get? && has_errors?
controller.render({ status: error_status, formats: [:html] }.merge!(options))
else
controller.render(options)
end
end
def error_rendering_options
if options[:render]
options[:render]
else
{ action: default_action, status: error_status }.tap do |error_rendering_options|
error_rendering_options[:formats] = [:turbo_stream, :html] if format == :turbo_stream
end
end
end
There will be other ways to skin the cat but the main problem with Responders is that it relies on rescuing ActionView::MissingTemplate
as a means to then look at redisplaying the form.
In my changes:
:html
. This prevents Responders trying to display my create.turbo_stream.erb
page as an error page.:html
to the formats for the default error_rendering_options
. This allows us to find new.html.erb
from a failed update
request with a :turbo_stream
format. So far this seems to works. I'm sure there will be quirks as I go along, and perhaps this is particular to my use case, but I think Turbo Streams aren't going away and it might be time to bake something in, or at least have an extra responder in Responders to optionally include to handle turbo_streams more explicitly?
Thinking slightly harder, it could be that we could extend the configuration to allow for defining formats for the different request types (get, error, success) to keep everything agnostic?
I've come up against an interesting problem. This page has an interesting technique for creating a modal with turbo: https://www.viget.com/articles/fancy-form-modals-with-rails-turbo/
I'm trying to specifically reproduce this logic with responders:
So to translate, the initial edit form is just a
html
view wrapped in a turbo-frame tag called 'modal'. Navigating to this updates the modal with the form. We then use stimulus to show the modal.When submitting the form: if the model saves we want to render a turbo-stream response that updates various areas of the page. If it fails we want to render the original html edit page from before with the form field errors highlighted etc...
So far I can't seem to figure out how to make this work with responders.
Does anyone have any tips? :)