Skip to content
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

Allow passing a :template option to Broadcastable methods #425

Merged
merged 1 commit into from
Feb 3, 2023
Merged

Allow passing a :template option to Broadcastable methods #425

merged 1 commit into from
Feb 3, 2023

Conversation

davidalejandroaguilar
Copy link
Contributor

@davidalejandroaguilar davidalejandroaguilar commented Feb 2, 2023

Description

Summary

This PR allows passing the :template option to the **rendering options of a Broadcastable method, so we can render a whole template instead of a partial.

Rationale

Currently, we can only use :partial, :html or pass nothing (in this case Broadcastable calls the model's to_partial_path method). Meaning we're assuming that we'll always be dealing with partials or custom HTML written inline. If we rather want to render a whole template (e.g. index, show, edit, etc.), we need to resort to using ApplicationController.render, which is not ideal.

Notes

When the template option is passed, we'll pass it down with the layout: false option, in order to avoid passing any unwanted elements such as <head>.

TL;DR:

This PR allows passing the `:template` option to the `**rendering` options of a `Broadcastable` method, so we can render a whole template instead of a partial.

Rationale:

Currently, we can only use :partial, :html or pass nothing (in this case `Broadcastable` calls the model's `to_partial_path` method). Meaning we're assuming that we'll always be dealing with partials or custom HTML written inline. If we rather want to render a whole template (e.g. index, show, edit, etc.), we need to resort to using `ApplicationController.render`, which is not ideal.
assert_broadcast_on "stream", turbo_stream_action_tag("update", target: "message_1", template: render("messages/index", layout: false)) do
@message.broadcast_update_to "stream", template: "messages/index"
end
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we'd want:

  • Only one method tested with the template option, like the current commit has.
  • Separate tests for each Broadcastable method. e.g.
  test "broadcasting update to stream now with template option" do
    assert_broadcast_on "stream", turbo_stream_action_tag("update", target: "message_1", template: render("messages/index", layout: false)) do
      @message.broadcast_update_to "stream", template: "messages/index"
    end
  end

  test "broadcasting replace to stream now with template option" do
    assert_broadcast_on "stream", turbo_stream_action_tag("replace", target: "message_1", template: render("messages/index", layout: false)) do
      @message.broadcast_replace_to "stream", template: "messages/index"
    end
  end
  • Separate methods in a dynamic way, e.g.:
  [
    [:broadcast_update_to, :update],
    [:broadcast_replace_to, :replace]
  ].each do |method, action|
    test "broadcasting #{method} to stream now with template option" do
      assert_broadcast_on "stream", turbo_stream_action_tag(action, target: "message_1", template: render("messages/index", layout: false)) do
        @message.public_send method, "stream", template: "messages/index"
      end
    end
  end

  [
    [:broadcast_before_to, :before],
    [:broadcast_after_to, :after],
    [:broadcast_append_to, :append],
    [:broadcast_prepend_to, :prepend],
  ].each do |method, action|
    test "broadcasting #{method} to stream now with template option" do
      assert_broadcast_on "stream", turbo_stream_action_tag(action, target: "message_1", template: render("messages/index", layout: false)) do
        @message.public_send method, "stream", template: "messages/index", target: "message_1"
      end
    end
  end

  # ...etc.

Reason I only added one test for now is because the current tests are not thoroughly testing all possible rendering options for all methods. e.g. the partial option is only tested in the test named local variables don't get overwritten if they collide with the template name, so I don't know how to proceed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer the separate tests. Please do open a follow up PR to expand test coverage!

# def update_message
# broadcast_replace_to(user, :message, target: "message", template: "messages/show", locals: { message: self })
# end
# end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be the best example but I'm just trying to follow the existing "messages" context of the examples.

@davidalejandroaguilar davidalejandroaguilar marked this pull request as ready for review February 2, 2023 08:37
@kevinmcconnell
Copy link
Contributor

What would the use case be for broadcasting a view template?

I would normally expect the view templates to be rendered as the response to a controller action. When you want to reuse content from a view in multiple scenarios, extracting that content to a partial is a nice convention. It also means you're passing locals rather than ivars to those partials, whereas the views will typically expect state in ivars.

There's probably a good reason for this that I'm missing though!

@davidalejandroaguilar
Copy link
Contributor Author

That's a valid question @kevinmcconnell! Personally, my use case is this:

I have a turbo-frame modal that works as a stepper/wizard. It has a src attribute that, on page load, hits the show action for a specific route and displays the contents of step 1. Upon clicking "Continue" from within the modal, the show action of step 2's route is hit and the modal is updated with this page. And so on and so forth.

In this case, the show actions are rendering the whole show template, they're not using partials. So when using Broadcastable to update that modal's contents with new information, I'd have to either:

  1. Have my show.html.erb template only do a render "show" that renders a _show.html.erb partial with the actual content and have the broadcast method use partial: "show".
  2. Use ApplicationController.render to render show.html.erb and pass it to the broadcast method html: option.

Having the option to use template would prevent me from wrestling the framework in order to achieve the above.

Now, aside from my use case. I think this change is also valid for 2 reasons:

  • Principle of least surprise: If I see that the partial: and html: options are available and that they're ultimately going through ApplicationController.render. I'd think using template: would just work.
  • Ruby giving you sharp knives: Having the option to render a template is a powerful tool. But it doesn't mean it is the right one for every job. So as with everything in Ruby, you've got to use your judgement here.

Finally, regarding the use of locals rather than ivars. I personally always use locals/helper methods rather than ivars. Since if an ivar is not defined, Ruby will not complain, and so you might end up with nil values where you don't expect them. On the other hand, if you use locals/helper methods, Ruby will complain loudly when you try to use them and they're not defined.

However, since this these options are all going to be passed down to ApplicationController.render at the end (on Turbo::Streams::Broadcasts#render_format), you can either use locals: or assigns: to pass either local variables/methods or ivars. Maybe we should use assigns too in the docs example?

@davidalejandroaguilar
Copy link
Contributor Author

@dhh Failing CI tests pass on my local, e.g.

image

I've seen other PRs merged with some failing CI jobs, so not sure what the next steps are.

@dhh
Copy link
Member

dhh commented Feb 3, 2023

Yeah CI is flaky. Will fix elsewhere.

@dhh dhh merged commit 6c8126a into hotwired:main Feb 3, 2023
@kevinmcconnell
Copy link
Contributor

@davidalejandroaguilar thanks for the explanation!

I'd still use a partial for that case myself (but not called _show.html.erb; probably something more like _stage.html.erb or whatever your domain language suggests). It helps communicate that this is a shared template used in multiple ways. But I get what you're saying. Thanks for satisfying my curiosity :)

Maybe we should use assigns too in the docs example?

I think that's a great idea 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants