-
Notifications
You must be signed in to change notification settings - Fork 445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Turbo-Streaming ViewComponents #1106
Comments
If this is something we feel is worth implementing I would love to hear people's ideas for how it could work, and I love to pair on the feature. |
Right now, I actually need this feature for something I'm working on @raisedevs. I'm going to be working on a cleaner solution at some point in the future, but right now I can recommend using partials as a compatibility layer here. Either render what you want to directly in the partial, or render a component inside the partial. The latter should make it much easier to switch to the solution once it arises. |
I've taken some time to battle with this one, and here are my findings: Turbo::StreamsChannel.broadcast_append_to(record_or_channel_name, target: dom_target_id, html: ExampleComponent.new.render_in(view_context)) We can use any of the methods from off I think some of their Broadcastable concern is applicable to ViewComponent, but some of it is pointed at |
I think what's particularly notable about the above is that a view context is necessary. That makes it less than optimal for using in Active Record callbacks, which (while not my personal preference) is the way they introduce you to it in the Turbo docs, so some Turbo folks would most likely expect this usage to integrate with ViewComponent. |
I've been leaning on objects like this for now. module Broadcast
class Message
def self.append(message:, view_context:)
new(message, view_context).append
end
def initialize(message, view_context)
@message = message
@view_context = view_context
end
def append
Turbo::StreamsChannel.broadcast_append_later_to(
:messages,
target: "messages",
html: rendered_component
)
end
private
attr_reader :message, :view_context
def rendered_component
MessageComponent.new(message: message).render_in(view_context)
end
end
end I think what I'd like to be able to do is something like this - which would mean we don't rely on a class Message < ApplicationRecord
belongs_to :user
after_create_commit :update_message_count
private
def update_message_count
broadcast_update_to(
user,
:messages,
target: "message-count",
html: CountComponent.new(count: user.messages.count).render_to_html
)
end
end |
I suppose there's still the wider question of components that inherently rely on a view context - say, for example, you're using At one point there was talk of making a new instance of |
Been dwelling on this a bit and I think this requires very little in the way of code at all. I think what it comes down to is folks:
One thing we probably don't want to do is create methods on component instances to broadcast them directly, e.g. We'll definitely want to make it easier to render components to strings in order to enable this, and document the approach too. |
The only thing I can think of that we might want to do is take the top-level broadcast helpers a bit further by allowing folks to pass the |
@boardfish What about adding a section about Turbo on the Compatibility page? |
That sounds right, but I think if the aim is for it to be saying to folks "ViewComponent works with X!", we might need to break it down a bit, since a lot of what's there feels like it's saying "ViewComponent works with X, but you have to pull these strings to do it". Rendering to a string is currently in the FAQs, so I'll probably link to that from there. |
@boardfish Apologies if I'm missing something simple here. I spent some time today working on broadcasting view components from models, based on your work here and in #201. I started with this: # app/models/post.rb
broadcast_append_to(
'posts',
html: PostComponent.new(post: self).render_in(ActionController::Base.new.view_context)
) This worked up until the point that I needed to access url helpers in the component, like this: <%= link_to "Show this post", @post %> At that point, the model broadcast failed with Adding Eventually I switched to using broadcast_append_to(
'posts',
html: ApplicationController.render(
PostComponent.new(post: self)
)
) Is there a reason not to use |
I don't think there is. The docs currently specify that if you want to render to a string from inside a controller action, you would do so through The difference in behavior between these two methods interests me - again, it's got me thinking about the trend of calling helpers without chaining off I wonder if using |
currently all of these work for me without any additional magic
more details: https://blog.corsego.com/turbo-stream-view-components update: sorry for the confusion: this works for me in a controller. did not try in model broadcast |
I came across the helper issues while exploring this yesterday, some interesting challenges here. One thing to note is that |
@DavidColby I was wondering the same. That would imply that descendants of Helpers are
@yshmarov Thanks for highlighting this - it's interesting to note that there are three different ways to render to a string. Looks like our FAQs recommend render_in as the one to use, most likely because the other two are provided by Rails and may be subject to change. |
I wrote this FAQ. It recommends against |
I've found something new linked to using Turbo - raising a separate issue. |
#1137 is linked to this if your live-updating features get too complex for Turbo's own broadcasting features to handle |
(deleted my previous comment when I noticed If The FAQ linked above is no longer there at that URL, there appears to no longer be a "FAQ" section of docs? Not sure if that means the advice about render in a controller is no longer given. |
I've been doing some more work in this corner. So far, I'm feeling the following:
render turbo_stream: turbo_stream.update(frame_id_for(:index)) {
self.class.render(ExampleComponent.new(**args), layout: false)
} + turbo_stream.update(frame_id_for(:new)) {
self.class.render(ExampleComponent.new(**args), layout: false)
} |
Sorry about that, it has been moved to https://viewcomponent.org/guide/getting-started.html#rendering-viewcomponents-to-strings-inside-controller-actions |
👋 FYI, I just opened a PR in The API I proposed is this: turbo_stream.append "notifications", NotificationComponent.new From ERB, you can also pass in content <%= turbo_stream.append "notifications", NotificationComponent.new do %>
<h1>Hello World!</h1>
<% end %> |
Update: This does not actually work.
turbo_stream.append "notifications" do
render NotificationComponent.new
end |
If that's the case, it's probably worth me updating #1227 accordingly and perhaps also adding some docs. It seems like there are a lot of potential ways to go about this, so comprehensive coverage of what works and what doesn't should help. |
@boardfish sorry, I got it wrong. The technique I mentioned doesn't actually work but my PR will provide a way to do this once it's merged in turbo-rails. |
The PR got merged, so I assume support will ship in the next release. hotwired/turbo-rails#433 |
It looks like this shipped in 1.4 a few days ago. https://github.com/hotwired/turbo-rails/releases/tag/v1.4.0 Shall we close this issue? Should we do anything in the documentation first? |
I think we should update the docs. @boardfish or @joeldrapper (or anyone else here) want to draft a PR? I'm happy to review. |
something broke for me, but still trying to figure out what exactly. All my vanilla GET
Just basic resources index -> show navigation. Probably user error, but timing is suspicious. Error sounds suspicious too, it shouldn't be expecting a |
OK, for sure related to release, but maybe I have something fundamentally confused and it has only been working on accident. Reverting and locking to For reference i use importmaps pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true |
@rromanchuk This doesn't seem related to ViewComponent or the renderable changes. You may want to open an issue on the turbo-rails repository. |
@joeldrapper yup, i know. This is just where i naturally ended up while debugging, and the links + discussion here gave the clues i needed. Since the links were being rendered by viewcomponent, I initially thought it might be related. Just wanted to update since future debuggers may (definitely will) end up here too. |
@rromanchuk it about this PR hotwired/turbo@1e78f3b |
Would it be appropriate to consider broadcast streams scenarios for view_components as well, a la hotwired/turbo-rails#270? |
Closing as stale. |
Feature request
Provide an interface for rendering view components outside the context of a request-response cycle (e.g without depending on a controller or view context).
Motivation
Rails 7 ships with Hotwire by default. Turbo Streams are a component of Hotwire that (among other things) allow applications to broadcast HTML "over the wire" based on model changes.
Like Rails, Turbo Streams lean heavily on partials - although you can now broadcast HTML from the model. Since ViewComponents replace partials in lots of scenarios, I think it would be beneficial for the library to integrate seamlessly with Turbo Streams in the same way partials do.
Currently there are few ways to integrate ViewComponents with Turbo Streams, but I think it would be nice to offer an "approved" approach.
#render_in(view_context)
to broadcast HTML from a controller.#call
method we can invoke it to return HTML and broadcast from a model (or any other class).Context
Turbo broadcasting discussion
Hotwire by default in Rails 7
The text was updated successfully, but these errors were encountered: