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

Add support for multiple formats #2079

Merged
merged 26 commits into from
Sep 6, 2024
Merged

Add support for multiple formats #2079

merged 26 commits into from
Sep 6, 2024

Conversation

joelhawksley
Copy link
Member

@joelhawksley joelhawksley commented Aug 23, 2024

What are you trying to accomplish?

I'm looking to more holistically address #1990 by having ViewComponent properly handle formats instead of hardcoding to HTML.

What approach did you choose and why?

I passed the format from render_in down to the template selection logic, which now allows for multiple formats in a single component ❤️

I've also added a few test helpers to make working with formats more enjoyable.

Anything you want to highlight for special attention from reviewers?

Nothing in particular. There is no way I could have pulled this off without our massively thorough test suite ❤️

cc @fredboyle

Closes #1990 #1783

@joelhawksley joelhawksley changed the title Add support for mutliple formats Add support for multiple formats Aug 23, 2024
Copy link
Contributor

3 file(s) had their final line ending fixed:

  • test/sandbox/app/components/multiple_formats_component.css.erb
    ,- test/sandbox/app/components/multiple_formats_component.html.erb
    ,- test/sandbox/app/components/multiple_formats_component.json.erb

Copy link
Contributor

1 file(s) had their final line ending fixed:

  • test/sandbox/app/components/multiple_formats_component.json.jbuilder

@joelhawksley joelhawksley marked this pull request as ready for review August 26, 2024 17:21
@reeganviljoen
Copy link
Collaborator

@joelhawksley does this now mean that a component could have a multiple format templates, i.e the same component has a json and an html template?

@joelhawksley
Copy link
Member Author

@reeganviljoen correct, see the test case: https://github.com/ViewComponent/view_component/pull/2079/files#diff-0553d15257b3cb95586c15820fe4dbe55ce1d4e3bae03f0f37d88fafcc50709b

Copy link
Contributor

@BlakeWilliams BlakeWilliams left a comment

Choose a reason for hiding this comment

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

I left a few comments, but I think my comment about considering a separate class for handling some of the template logic is the one I'm curious about most. I think that could help reduce the complexity of the compiler a good bit, and would be happy to pair or chat more about it!

lib/view_component/base.rb Outdated Show resolved Hide resolved
@@ -330,11 +330,11 @@ def maybe_escape_html(text)
end
end

def safe_render_template_for(variant)
def safe_render_template_for(variant, format = nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
def safe_render_template_for(variant, format = nil)
def safe_render_template_for(variant, format: nil)

extremely minor, but thoughts on using a kwarg for nice readability?

Copy link
Member Author

Choose a reason for hiding this comment

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

My Personal Ruby Style Guide ™️ says to only use kwargs for > 2 arguments 🤷🏻

Regardless, this method is only used once, so I'm just going to inline it. No point in having default values for a single callsite anyways.

@@ -106,28 +106,71 @@ def renders_template_for_variant?(variant)
attr_reader :component_class, :redefinition_lock

def define_render_template_for
variant_elsifs = variants.compact.uniq.map do |variant|
safe_name = "_call_variant_#{normalized_variant_name(variant)}_#{safe_class_name}"
branches = []
Copy link
Contributor

@BlakeWilliams BlakeWilliams Aug 27, 2024

Choose a reason for hiding this comment

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

This is getting pretty complex, is this something we could break into more methods, or perhaps a separate class that's responsible for returning template objects that define the relevant methods? Like name, method_name, format, etc.?

It might also make the testing story a bit easier and more comprehensive since we don't have to couple the compiler and template logic as heavily.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh man, all I wanted to do while writing this PR was refactor this entire file, but I held back. I'm happy to go down that route, but maybe it would be best to do as a follow-up?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hah, the compiler has gotten a bit gnarly! Part of the reasoning was that I had a hard to following the code in here (but I also didn't have a ton of time to dedicate to reading it) so I thought refactoring might make it easier to follow the changes.

One strategy I've done in the past for changes like this is refactoring the PR on-top of the new behavior, backporting it while removing the new functionality as a separate PR (so 1-to-1 functionality wise), then re-adding those initial refactor changes on-top. It's a bit roundabout, but I found it helps reduce churn and makes the changes easy-to-follow.

I'm also fine with a follow-up, but I'd also say I haven't reviewed this code in depth yet if we wanted to go that route. 🙂

@@ -147,7 +190,7 @@ def template_errors
errors << "Couldn't find a template file or inline render method for #{component_class}."
end

if templates.count { |template| template[:variant].nil? } > 1
if templates.map { |template| "#{template[:variant].inspect}_#{template[:format]}" }.tally.any? { |_, count| count > 1 }
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we use inspect here?

@@ -769,4 +769,22 @@ def test_unsafe_postamble_component
"Rendering UnsafePostambleComponent did not emit an HTML safety warning"
)
end

def test_renders_multiple_format_component_as_html
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to Rails default behavior do we want a test that validates we fall back to HTML in cases where the component is missing a format specific template?

Should we also have a test that validates that ViewComponent respects the formats option passed to render if we don't have one already?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm. My read of the docs is the opposite, that we should _not _ fall back for an unknown format:

If a template with the specified format does not exist an ActionView::MissingTemplate error is raised.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, I wrote a test for the formats option and realized that we don't pass any of the render arguments into components: https://github.com/rails/rails/blob/4bb2227640531c877e30cc96f10df0c298dc3331/actionview/lib/action_view/template/renderable.rb#L16.

IMO we should look at passing render options to renderables, as it would be nice to handle format overrides.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ohhhh, you're right. I read that wrong. Good catch.

I was reading:

Rails uses the format specified in the request (or :html by default)

Which I think means instead of passing format = nil in the compiler, it should be format = :html? 🤔

Copy link
Contributor

@camertron camertron Aug 28, 2024

Choose a reason for hiding this comment

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

It looks like we're passing :html by default in the compiler: https://github.com/ViewComponent/view_component/pull/2079/files#diff-fa28fa6e2d5f267384e7793b855281451a46b8437431867871a298fc52fe4940R316

I believe request&.format&.to_sym will also be :html by default, which gets passed to render_template_for.

Copy link
Contributor

@camertron camertron left a comment

Choose a reason for hiding this comment

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

This all makes sense to me 😄

@joelhawksley joelhawksley enabled auto-merge (squash) September 6, 2024 20:37
@joelhawksley joelhawksley merged commit 451543a into main Sep 6, 2024
19 checks passed
@joelhawksley joelhawksley deleted the multi-format-component branch September 6, 2024 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants