diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0e94f1df..668381c58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,18 +27,6 @@ jobs: fail-fast: false matrix: include: - - ruby_version: "3.0" - rails_version: "6.1" - mode: "capture_patch_enabled" - - ruby_version: "3.0" - rails_version: "6.1" - mode: "capture_patch_disabled" - - ruby_version: "3.1" - rails_version: "7.0" - mode: "capture_patch_enabled" - - ruby_version: "3.1" - rails_version: "7.0" - mode: "capture_patch_disabled" - ruby_version: "3.2" rails_version: "7.1" mode: "capture_patch_enabled" diff --git a/.gitignore b/.gitignore index 340f093f5..dfe6728fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.gem *.rbc .ruby-version +.DS_Store /.config /coverage/assets /coverage/index.html diff --git a/Appraisals b/Appraisals index 97a25f8e0..295bcd949 100644 --- a/Appraisals +++ b/Appraisals @@ -1,22 +1,5 @@ # frozen_string_literal: true -appraise "rails-6.1" do - gem "rails", "~> 6.1" - gem "tailwindcss-rails", "~> 2.0" - - # Required for Ruby 3.1.0 - gem "net-smtp", require: false - gem "net-imap", require: false - gem "net-pop", require: false - gem "turbo-rails", "~> 1" -end - -appraise "rails-7.0" do - gem "rails", "~> 7.0" - gem "tailwindcss-rails", "~> 2.0" - gem "turbo-rails", "~> 1" -end - appraise "rails-7.1" do gem "rails", "~> 7.1" gem "tailwindcss-rails", "~> 2.0" diff --git a/Gemfile b/Gemfile index 0a20ee868..0ed4f7ae3 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gemspec -rails_version = (ENV["RAILS_VERSION"] || "~> 7.0.0").to_s +rails_version = (ENV["RAILS_VERSION"] || "~> 7.1.0").to_s gem "rails", (rails_version == "main") ? {git: "https://github.com/rails/rails", ref: "main"} : rails_version ruby_version = (ENV["RUBY_VERSION"] || "~> 3.3").to_s diff --git a/Gemfile.lock b/Gemfile.lock index b70346e73..dbd9018b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,77 +2,86 @@ PATH remote: . specs: view_component (3.20.0) - activesupport (>= 5.2.0, < 8.1) + activesupport (>= 7.1.0, < 8.1) concurrent-ruby (~> 1.0) method_source (~> 1.0) GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.6) - actionpack (= 7.0.8.6) - activesupport (= 7.0.8.6) + actioncable (7.1.4) + actionpack (= 7.1.4) + activesupport (= 7.1.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.6) - actionpack (= 7.0.8.6) - activejob (= 7.0.8.6) - activerecord (= 7.0.8.6) - activestorage (= 7.0.8.6) - activesupport (= 7.0.8.6) + zeitwerk (~> 2.6) + actionmailbox (7.1.4) + actionpack (= 7.1.4) + activejob (= 7.1.4) + activerecord (= 7.1.4) + activestorage (= 7.1.4) + activesupport (= 7.1.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.6) - actionpack (= 7.0.8.6) - actionview (= 7.0.8.6) - activejob (= 7.0.8.6) - activesupport (= 7.0.8.6) + actionmailer (7.1.4) + actionpack (= 7.1.4) + actionview (= 7.1.4) + activejob (= 7.1.4) + activesupport (= 7.1.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.8.6) - actionview (= 7.0.8.6) - activesupport (= 7.0.8.6) - rack (~> 2.0, >= 2.2.4) + rails-dom-testing (~> 2.2) + actionpack (7.1.4) + actionview (= 7.1.4) + activesupport (= 7.1.4) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.6) - actionpack (= 7.0.8.6) - activerecord (= 7.0.8.6) - activestorage (= 7.0.8.6) - activesupport (= 7.0.8.6) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.4) + actionpack (= 7.1.4) + activerecord (= 7.1.4) + activestorage (= 7.1.4) + activesupport (= 7.1.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.6) - activesupport (= 7.0.8.6) + actionview (7.1.4) + activesupport (= 7.1.4) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8.6) - activesupport (= 7.0.8.6) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.4) + activesupport (= 7.1.4) globalid (>= 0.3.6) - activemodel (7.0.8.6) - activesupport (= 7.0.8.6) - activerecord (7.0.8.6) - activemodel (= 7.0.8.6) - activesupport (= 7.0.8.6) - activestorage (7.0.8.6) - actionpack (= 7.0.8.6) - activejob (= 7.0.8.6) - activerecord (= 7.0.8.6) - activesupport (= 7.0.8.6) + activemodel (7.1.4) + activesupport (= 7.1.4) + activerecord (7.1.4) + activemodel (= 7.1.4) + activesupport (= 7.1.4) + timeout (>= 0.4.0) + activestorage (7.1.4) + actionpack (= 7.1.4) + activejob (= 7.1.4) + activerecord (= 7.1.4) + activesupport (= 7.1.4) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.8.6) + activesupport (7.1.4) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) @@ -105,6 +114,7 @@ GEM xpath (~> 3.2) coderay (1.1.3) concurrent-ruby (1.3.4) + connection_pool (2.4.1) crass (1.0.6) cuprite (0.15.1) capybara (~> 3.0) @@ -191,23 +201,28 @@ GEM puma (6.4.3) nio4r (~> 2.0) racc (1.8.1) - rack (2.2.10) + rack (2.2.9) + rack-session (1.0.2) + rack (< 3) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8.6) - actioncable (= 7.0.8.6) - actionmailbox (= 7.0.8.6) - actionmailer (= 7.0.8.6) - actionpack (= 7.0.8.6) - actiontext (= 7.0.8.6) - actionview (= 7.0.8.6) - activejob (= 7.0.8.6) - activemodel (= 7.0.8.6) - activerecord (= 7.0.8.6) - activestorage (= 7.0.8.6) - activesupport (= 7.0.8.6) + rackup (1.0.0) + rack (< 3) + webrick + rails (7.1.4) + actioncable (= 7.1.4) + actionmailbox (= 7.1.4) + actionmailer (= 7.1.4) + actionpack (= 7.1.4) + actiontext (= 7.1.4) + actionview (= 7.1.4) + activejob (= 7.1.4) + activemodel (= 7.1.4) + activerecord (= 7.1.4) + activestorage (= 7.1.4) + activesupport (= 7.1.4) bundler (>= 1.15.0) - railties (= 7.0.8.6) + railties (= 7.1.4) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -215,13 +230,14 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.8.6) - actionpack (= 7.0.8.6) - activesupport (= 7.0.8.6) - method_source + railties (7.1.4) + actionpack (= 7.1.4) + activesupport (= 7.1.4) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) rdoc (6.7.0) @@ -356,7 +372,7 @@ DEPENDENCIES net-smtp pry (~> 0.13) puma (~> 6) - rails (~> 7.0.0) + rails (~> 7.1.0) rake (~> 13.0) rspec-rails (~> 5) rubocop-md (~> 1) diff --git a/app/helpers/preview_helper.rb b/app/helpers/preview_helper.rb index 18016ff76..a679e3318 100644 --- a/app/helpers/preview_helper.rb +++ b/app/helpers/preview_helper.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true module PreviewHelper - # :nocov: - include ActionView::Helpers::AssetUrlHelper if Rails.version.to_f < 6.1 - # :nocov: - AVAILABLE_PRISM_LANGUAGES = %w[ruby erb haml] FALLBACK_LANGUAGE = "ruby" @@ -25,38 +21,10 @@ def prism_js_source_url def find_template_data(lookup_context:, template_identifier:) template = lookup_context.find_template(template_identifier) - if Rails.version.to_f >= 6.1 || template.source.present? - { - source: template.source, - prism_language_name: prism_language_name_by_template(template: template) - } - # :nocov: - else - # Fetch template source via finding it through preview paths - # to accomodate source view when exclusively using templates - # for previews for Rails < 6.1. - all_template_paths = ViewComponent::Base.config.preview_paths.map do |preview_path| - Dir.glob("#{preview_path}/**/*") - end.flatten - - # Search for templates the contain `html`. - matching_templates = all_template_paths.find_all do |path| - path =~ /#{template_identifier}*.(html)/ - end - - raise ViewComponent::NoMatchingTemplatesForPreviewError.new(template_identifier) if matching_templates.empty? - raise ViewComponent::MultipleMatchingTemplatesForPreviewError.new(template_identifier) if matching_templates.size > 1 - - template_file_path = matching_templates.first - template_source = File.read(template_file_path) - prism_language_name = prism_language_name_by_template_path(template_file_path: template_file_path) - - { - source: template_source, - prism_language_name: prism_language_name - } - end - # :nocov: + { + source: template.source, + prism_language_name: prism_language_name_by_template(template: template) + } end private diff --git a/app/views/view_components/preview.html.erb b/app/views/view_components/preview.html.erb index f364fd725..c12aa6eb0 100644 --- a/app/views/view_components/preview.html.erb +++ b/app/views/view_components/preview.html.erb @@ -1,9 +1,5 @@ <% if @render_args[:component] %> - <% if ViewComponent::Base.config.render_monkey_patch_enabled || Rails.version.to_f >= 6.1 %> - <%= render(@render_args[:component], @render_args[:args], &@render_args[:block]) %> - <% else %> - <%= render_component(@render_args[:component], &@render_args[:block]) %> - <% end %> + <%= render(@render_args[:component], @render_args[:args], &@render_args[:block]) %> <% else %> <%= render template: @render_args[:template], locals: @render_args[:locals] || {} %> <% end %> diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c106fb61d..df46266a8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,35 @@ nav_order: 5 ## main +## 4.0.0 + +* BREAKING: Require [non-EOL](https://endoflife.date/rails) Rails (`>= 7.1.0`). + + *Joel Hawksley* + +* BREAKING: Require [non-EOL](https://www.ruby-lang.org/en/downloads/branches/) Ruby (`>= 3.2.0`). + + *Joel Hawksley* + +* BREAKING: Remove `render_component` and `render` monkey patch configured with `render_monkey_patch_enabled`. + + *Joel Hawksley* + +* BREAKING: Remove support for variant names containing `.` to be consistent with Rails. + + *Stephen Nelson* + +* BREAKING: Use ActionView's `lookup_context` for picking templates instead of the request format. + + 3.15 added support for using templates that match the request format, i.e. if `/resource.csv` is requested then + ViewComponents would pick `_component.csv.erb` over `_component.html.erb`. + + With this release, the request format is no longer considered and instead ViewComponent will use the Rails logic + for picking the most appropriate template type, i.e. the csv template will be used if it matches the `Accept` header + or because the controller uses a `respond_to` block to pick the response format. + + *Stephen Nelson* + * Ensure HTML output safety wrapper is used for all inline templates. *Joel Hawksley* diff --git a/docs/api.md b/docs/api.md index 3e18fbe58..0e4778345 100644 --- a/docs/api.md +++ b/docs/api.md @@ -243,12 +243,6 @@ Defaults to `['test/components/previews']` relative to your Rails root. The entry route for component previews. Defaults to `"/rails/view_components"`. -### `.render_monkey_patch_enabled` - -If this is disabled, use `#render_component` or -`#render_component_to_string` instead. -Defaults to `true`. - ### `.show_previews` Whether component previews are enabled. diff --git a/docs/compatibility.md b/docs/compatibility.md index ded7dfd13..19f243267 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -8,29 +8,12 @@ nav_order: 6 ## Ruby & Rails -ViewComponent supports all actively supported versions of Ruby (3.0+) and Ruby on Rails (6.1+) and is tested against a combination of these versions of Ruby on Rails. - -While EOL (end-of-life) versions of Ruby and Ruby on Rails may still work with ViewComponent, they're not actively supported and no longer tested. We will still accept patches on a case-by-case basis to support older Ruby & Rails versions based on the complexity and maintenance burden. Please open an issue before submitting such a Pull Request. +ViewComponent supports all actively supported versions of [Ruby](https://endoflife.date/ruby) (>= 3.2) and [Ruby on Rails](https://endoflife.date/rails) (>= 7.1). Changes to the minimum Ruby and Rails versions supported will only be made in major releases. ## Template languages ViewComponent is tested against ERB, Haml, and Slim, but it should support most Rails template handlers. -## Disabling the render monkey patch (Rails < 6.1) - -Since 2.13.0 -{: .label } - -To [avoid conflicts](https://github.com/viewcomponent/view_component/issues/288) between ViewComponent and other gems that also monkey patch the `render` method, it's possible to configure ViewComponent to not include the render monkey patch: - -`config.view_component.render_monkey_patch_enabled = false # defaults to true` - -With the monkey patch disabled, use `render_component` (or `render_component_to_string`) instead: - -```erb -<%= render_component Component.new(message: "bar") %> -``` - ## Bridgetown (Static Site Generator) [Bridgetown](https://www.bridgetownrb.com/) supports ViewComponent via an experimental shim provided by the [bridgetown-view-component gem](https://github.com/bridgetownrb/bridgetown-view-component). More information available [here](https://www.bridgetownrb.com/docs/components/ruby#need-compatibility-with-rails-try-viewcomponent-experimental). diff --git a/docs/index.md b/docs/index.md index c810d6471..659efa588 100644 --- a/docs/index.md +++ b/docs/index.md @@ -193,6 +193,7 @@ ViewComponent is built by over a hundred members of the community, including: sammyhenningsson sampart seanpdoyle +sfnelson simonrand skryukov smashwilson diff --git a/docs/known_issues.md b/docs/known_issues.md index ce20d6abf..ff8197168 100644 --- a/docs/known_issues.md +++ b/docs/known_issues.md @@ -55,7 +55,3 @@ Calls to form helpers such as `form_with` in ViewComponents [don't use the defau <%= f.text_field :name %> <% end %> ``` - -## Inconsistent controller rendering behavior between Rails versions - -In versions of Rails < 6.1, rendering a ViewComponent from a controller doesn't include the layout. diff --git a/gemfiles/rails_6.1.gemfile b/gemfiles/rails_6.1.gemfile deleted file mode 100644 index d78c28030..000000000 --- a/gemfiles/rails_6.1.gemfile +++ /dev/null @@ -1,12 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 6.1" -gem "tailwindcss-rails", "~> 2.0" -gem "net-smtp", require: false -gem "net-imap", require: false -gem "net-pop", require: false -gem "turbo-rails", "~> 1" - -gemspec path: "../" diff --git a/gemfiles/rails_7.0.gemfile b/gemfiles/rails_7.0.gemfile deleted file mode 100644 index a703d2cc6..000000000 --- a/gemfiles/rails_7.0.gemfile +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 7.0" -gem "tailwindcss-rails", "~> 2.0" -gem "turbo-rails", "~> 1" - -gemspec path: "../" diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index 2cb42a545..5099901a9 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -9,6 +9,7 @@ require "view_component/errors" require "view_component/inline_template" require "view_component/preview" +require "view_component/request_details" require "view_component/slotable" require "view_component/slotable_default" require "view_component/template" @@ -63,6 +64,8 @@ def set_original_view_context(view_context) self.__vc_original_view_context = view_context end + using RequestDetails + # Entrypoint for rendering components. # # - `view_context`: ActionView context from calling view @@ -90,13 +93,12 @@ def render_in(view_context, &block) # For i18n @virtual_path ||= virtual_path - # For template variants (+phone, +desktop, etc.) - @__vc_variant ||= @lookup_context.variants.first + # Describes the inferred request constraints (locales, formats, variants) + @__vc_requested_details ||= @lookup_context.vc_requested_details # For caching, such as #cache_if @current_template = nil unless defined?(@current_template) old_current_template = @current_template - @current_template = self if block && defined?(@__vc_content_set_by_with_content) raise DuplicateContentError.new(self.class.name) @@ -108,7 +110,7 @@ def render_in(view_context, &block) before_render if render? - rendered_template = render_template_for(@__vc_variant, __vc_request&.format&.to_sym).to_s + rendered_template = render_template_for(@__vc_requested_details).to_s # Avoid allocating new string when output_preamble and output_postamble are blank if output_preamble.blank? && output_postamble.blank? @@ -156,7 +158,7 @@ def render_parent_to_string target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level] @__vc_parent_render_level += 1 - target_render.bind_call(self, @__vc_variant) + target_render.bind_call(self, @__vc_requested_details) ensure @__vc_parent_render_level -= 1 end @@ -267,11 +269,10 @@ def view_cache_dependencies [] end - # For caching, such as #cache_if - # - # @private + # Rails expects us to define `format` on all renderables, + # but we do not know the `format` of a ViewComponent until runtime. def format - @__vc_variant if defined?(@__vc_variant) + nil end # The current request. Use sparingly as doing so introduces coupling that @@ -328,7 +329,7 @@ def content_evaluated? end def maybe_escape_html(text) - return text if __vc_request && !__vc_request.format.html? + return text if @current_template && !@current_template.html? return text if text.blank? if text.html_safe? @@ -361,13 +362,6 @@ def safe_output_postamble # configured on a per-test basis using `with_controller_class`. # - # Set if render monkey patches should be included or not in Rails <6.1: - # - # ```ruby - # config.view_component.render_monkey_patch_enabled = false - # ``` - # - # Path for component files # # ```ruby @@ -524,12 +518,12 @@ def inherited(child) # meaning it will not be called for any children and thus not compile their templates. if !child.instance_methods(false).include?(:render_template_for) && !child.compiled? child.class_eval <<~RUBY, __FILE__, __LINE__ + 1 - def render_template_for(variant = nil, format = nil) + def render_template_for(requested_details) # Force compilation here so the compiler always redefines render_template_for. # This is mostly a safeguard to prevent infinite recursion. self.class.compile(raise_errors: true, force: true) # .compile replaces this method; call the new one - render_template_for(variant, format) + render_template_for(requested_details) end RUBY end @@ -686,8 +680,6 @@ def splatted_keyword_argument_present? def initialize_parameter_names return attribute_names.map(&:to_sym) if respond_to?(:attribute_names) - return attribute_types.keys.map(&:to_sym) if Rails::VERSION::MAJOR <= 5 && respond_to?(:attribute_types) - initialize_parameters.map(&:last) end diff --git a/lib/view_component/collection.rb b/lib/view_component/collection.rb index 798e38c25..d3c13e56a 100644 --- a/lib/view_component/collection.rb +++ b/lib/view_component/collection.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "action_view/renderer/collection_renderer" if Rails.version.to_f >= 6.1 +require "action_view/renderer/collection_renderer" module ViewComponent class Collection diff --git a/lib/view_component/compiler.rb b/lib/view_component/compiler.rb index c4ab88df4..9bad61349 100644 --- a/lib/view_component/compiler.rb +++ b/lib/view_component/compiler.rb @@ -55,6 +55,23 @@ def compile(raise_errors: false, force: false) end end + # @return all matching compiled templates, in priority order based on the requested details from LookupContext + # + # @param [ActionView::TemplateDetails::Requested] requested_details i.e. locales, formats, variants + def find_templates_for(requested_details) + filtered_templates = @templates.select do |template| + template.details.matches?(requested_details) + end + + if filtered_templates.count > 1 + filtered_templates.sort_by! do |template| + template.details.sort_key_for(requested_details) + end + end + + filtered_templates + end + private attr_reader :templates @@ -64,40 +81,25 @@ def define_render_template_for template.compile_to_component end - method_body = - if @templates.one? - @templates.first.safe_method_name_call - elsif (template = @templates.find(&:inline?)) - template.safe_method_name_call - else - branches = [] - - @templates.each do |template| - conditional = - if template.inline_call? - "variant&.to_sym == #{template.variant.inspect}" - else - [ - template.default_format? ? "(format == #{ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT.inspect} || format.nil?)" : "format == #{template.format.inspect}", - template.variant.nil? ? "variant.nil?" : "variant&.to_sym == #{template.variant.inspect}" - ].join(" && ") - end - - branches << [conditional, template.safe_method_name_call] - end + @component.silence_redefinition_of_method(:render_template_for) - out = branches.each_with_object(+"") do |(conditional, branch_body), memo| - memo << "#{(!memo.present?) ? "if" : "elsif"} #{conditional}\n #{branch_body}\n" + if @templates.one? + template = @templates.first + safe_call = template.safe_method_name_call + @component.define_method(:render_template_for) do |_| + @current_template = template + instance_exec(&safe_call) + end + else + compiler = self + @component.define_method(:render_template_for) do |details| + if (@current_template = compiler.find_templates_for(details).first) + instance_exec(&@current_template.safe_method_name_call) + else + raise MissingTemplateError.new(self.class.name, details) end - out << "else\n #{templates.find { _1.variant.nil? && _1.default_format? }.safe_method_name_call}\nend" end - - @component.silence_redefinition_of_method(:render_template_for) - @component.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def render_template_for(variant = nil, format = nil) - #{method_body} end - RUBY end def template_errors @@ -168,30 +170,18 @@ def template_errors def gather_templates @templates ||= - begin + if @component.inline_template.present? + [Template::Inline.new( + component: @component, + inline_template: @component.inline_template + )] + else + path_parser = ActionView::Resolver::PathParser.new templates = @component.sidecar_files( ActionView::Template.template_handler_extensions ).map do |path| - # Extract format and variant from template filename - this_format, variant = - File - .basename(path) # "variants_component.html+mini.watch.erb" - .split(".")[1..-2] # ["html+mini", "watch"] - .join(".") # "html+mini.watch" - .split("+") # ["html", "mini.watch"] - .map(&:to_sym) # [:html, :"mini.watch"] - - out = Template.new( - component: @component, - type: :file, - path: path, - lineno: 0, - extension: path.split(".").last, - this_format: this_format.to_s.split(".").last&.to_sym, # strip locale from this_format, see #2113 - variant: variant - ) - - out + details = path_parser.parse(path).details + Template::File.new(component: @component, path: path, details: details) end component_instance_methods_on_self = @component.instance_methods(false) @@ -201,24 +191,10 @@ def gather_templates ).flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) } .uniq .each do |method_name| - templates << Template.new( - component: @component, - type: :inline_call, - this_format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT, - variant: method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil, - method_name: method_name, - defined_on_self: component_instance_methods_on_self.include?(method_name) - ) - end - - if @component.inline_template.present? - templates << Template.new( + templates << Template::InlineCall.new( component: @component, - type: :inline, - path: @component.inline_template.path, - lineno: @component.inline_template.lineno, - source: @component.inline_template.source.dup, - extension: @component.inline_template.language + method_name: method_name, + defined_on_self: component_instance_methods_on_self.include?(method_name) ) end diff --git a/lib/view_component/config.rb b/lib/view_component/config.rb index 3edcd22d2..c2c8d6620 100644 --- a/lib/view_component/config.rb +++ b/lib/view_component/config.rb @@ -18,7 +18,6 @@ def defaults show_previews_source: false, instrumentation_enabled: false, use_deprecated_instrumentation_name: true, - render_monkey_patch_enabled: true, view_component_path: "app/components", component_parent_class: nil, show_previews: Rails.env.development? || Rails.env.test?, @@ -126,12 +125,6 @@ def defaults # Will default to `false` in next major version. # Defaults to `true`. - # @!attribute render_monkey_patch_enabled - # @return [Boolean] Whether the #render method should be monkey patched. - # If this is disabled, use `#render_component` or - # `#render_component_to_string` instead. - # Defaults to `true`. - # @!attribute view_component_path # @return [String] # The path in which components, their templates, and their sidecars should diff --git a/lib/view_component/engine.rb b/lib/view_component/engine.rb index b920eb5d8..40b9d305c 100644 --- a/lib/view_component/engine.rb +++ b/lib/view_component/engine.rb @@ -33,7 +33,6 @@ class Engine < Rails::Engine # :nodoc: options[config_option] ||= ViewComponent::Base.public_send(config_option) end options.instrumentation_enabled = false if options.instrumentation_enabled.nil? - options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil? options.show_previews = (Rails.env.development? || Rails.env.test?) if options.show_previews.nil? if options.show_previews @@ -91,46 +90,6 @@ class Engine < Rails::Engine # :nodoc: end end - initializer "view_component.monkey_patch_render" do |app| - next if Rails.version.to_f >= 6.1 || !app.config.view_component.render_monkey_patch_enabled - - # :nocov: - ViewComponent::Deprecation.deprecation_warning("Monkey patching `render`", "ViewComponent 4.0 will remove the `render` monkey patch") - - ActiveSupport.on_load(:action_view) do - require "view_component/render_monkey_patch" - ActionView::Base.prepend ViewComponent::RenderMonkeyPatch - end - - ActiveSupport.on_load(:action_controller) do - require "view_component/rendering_monkey_patch" - require "view_component/render_to_string_monkey_patch" - ActionController::Base.prepend ViewComponent::RenderingMonkeyPatch - ActionController::Base.prepend ViewComponent::RenderToStringMonkeyPatch - end - # :nocov: - end - - initializer "view_component.include_render_component" do |_app| - next if Rails.version.to_f >= 6.1 - - # :nocov: - ViewComponent::Deprecation.deprecation_warning("using `render_component`", "ViewComponent 4.0 will remove `render_component`") - - ActiveSupport.on_load(:action_view) do - require "view_component/render_component_helper" - ActionView::Base.include ViewComponent::RenderComponentHelper - end - - ActiveSupport.on_load(:action_controller) do - require "view_component/rendering_component_helper" - require "view_component/render_component_to_string_helper" - ActionController::Base.include ViewComponent::RenderingComponentHelper - ActionController::Base.include ViewComponent::RenderComponentToStringHelper - end - # :nocov: - end - initializer "static assets" do |app| if serve_static_preview_assets?(app.config) app.middleware.use(::ActionDispatch::Static, "#{root}/app/assets/vendor") @@ -174,16 +133,6 @@ def serve_static_preview_assets?(app_config) end end - # :nocov: - if RUBY_VERSION < "3.2.0" - ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 3.2.0", "ViewComponent v4 will remove support for Ruby versions < 3.2.0 no earlier than April 1, 2025") - end - - if Rails.version.to_f < 7.1 - ViewComponent::Deprecation.deprecation_warning("Support for Rails versions < 7.1", "ViewComponent v4 will remove support for Rails versions < 7.1 no earlier than April 1, 2025") - end - # :nocov: - app.executor.to_run :before do CompileCache.invalidate! unless ActionView::Base.cache_template_loading end diff --git a/lib/view_component/errors.rb b/lib/view_component/errors.rb index e8cb8b10e..22f445569 100644 --- a/lib/view_component/errors.rb +++ b/lib/view_component/errors.rb @@ -38,6 +38,22 @@ def initialize(example) end end + class MissingTemplateError < StandardError + MESSAGE = + "No templates for COMPONENT match the request DETAIL.\n\n" \ + "To fix this issue, provide a suitable template." + + def initialize(component, request_detail) + detail = { + locale: request_detail.locale, + formats: request_detail.formats, + variants: request_detail.variants, + handlers: request_detail.handlers + } + super(MESSAGE.gsub("COMPONENT", component).gsub("DETAIL", detail.inspect)) + end + end + class DuplicateContentError < StandardError MESSAGE = "It looks like a block was provided after calling `with_content` on COMPONENT, " \ diff --git a/lib/view_component/preview.rb b/lib/view_component/preview.rb index 2d3c84465..737b3601e 100644 --- a/lib/view_component/preview.rb +++ b/lib/view_component/preview.rb @@ -30,8 +30,6 @@ def render_with_template(template: nil, locals: {}) } end - alias_method :render_component, :render - class << self # Returns all component preview classes. def all diff --git a/lib/view_component/render_component_helper.rb b/lib/view_component/render_component_helper.rb deleted file mode 100644 index 945cd539d..000000000 --- a/lib/view_component/render_component_helper.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderComponentHelper # :nodoc: - def render_component(component, &block) - component.set_original_view_context(__vc_original_view_context) if is_a?(ViewComponent::Base) - component.render_in(self, &block) - end - end -end diff --git a/lib/view_component/render_component_to_string_helper.rb b/lib/view_component/render_component_to_string_helper.rb deleted file mode 100644 index dff55587c..000000000 --- a/lib/view_component/render_component_to_string_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderComponentToStringHelper # :nodoc: - def render_component_to_string(component) - component.render_in(view_context) - end - end -end diff --git a/lib/view_component/render_monkey_patch.rb b/lib/view_component/render_monkey_patch.rb deleted file mode 100644 index 0d05ac394..000000000 --- a/lib/view_component/render_monkey_patch.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderMonkeyPatch # :nodoc: - def render(options = {}, args = {}, &block) - if options.respond_to?(:render_in) - options.render_in(self, &block) - else - super - end - end - end -end diff --git a/lib/view_component/render_to_string_monkey_patch.rb b/lib/view_component/render_to_string_monkey_patch.rb deleted file mode 100644 index 325646f07..000000000 --- a/lib/view_component/render_to_string_monkey_patch.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderToStringMonkeyPatch # :nodoc: - def render_to_string(options = {}, args = {}) - if options.respond_to?(:render_in) - options.render_in(view_context) - else - super - end - end - end -end diff --git a/lib/view_component/rendering_component_helper.rb b/lib/view_component/rendering_component_helper.rb deleted file mode 100644 index dc7777efb..000000000 --- a/lib/view_component/rendering_component_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderingComponentHelper # :nodoc: - def render_component(component) - self.response_body = component.render_in(view_context) - end - end -end diff --git a/lib/view_component/rendering_monkey_patch.rb b/lib/view_component/rendering_monkey_patch.rb deleted file mode 100644 index ac7df3380..000000000 --- a/lib/view_component/rendering_monkey_patch.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderingMonkeyPatch # :nodoc: - def render(options = {}, args = {}) - if options.respond_to?(:render_in) - self.response_body = options.render_in(view_context) - else - super - end - end - end -end diff --git a/lib/view_component/request_details.rb b/lib/view_component/request_details.rb new file mode 100644 index 000000000..c502ce1eb --- /dev/null +++ b/lib/view_component/request_details.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module ViewComponent + # LookupContext computes and encapsulates @details for each request + # so that it doesn't need to be recomputed on each partial render. + # This data is wrapped in ActionView::TemplateDetails::Requested and + # used by instances of ActionView::Resolver to choose which template + # best matches the request. + # + # ActionView considers this logic internal to template/partial resolution. + # We're exposing it to the compiler via `refine` so that ViewComponent + # can match Rails' template picking logic. + module RequestDetails + refine ActionView::LookupContext do + # Return an abstraction for matching and sorting available templates + # based on the current lookup context details. + # + # @return ActionView::TemplateDetails::Requested + # @see ActionView::LookupContext#detail_args_for + # @see ActionView::FileSystemResolver#_find_all + def vc_requested_details(user_details = {}) + # The hash `user_details` would normally be the standard arguments that + # `render` accepts, but there's currently no mechanism for users to + # provide these when calling render on a ViewComponent. + details, cached = detail_args_for(user_details) + cached || ActionView::TemplateDetails::Requested.new(**details) + end + end + end +end diff --git a/lib/view_component/template.rb b/lib/view_component/template.rb index f01fd9368..969a2e8ee 100644 --- a/lib/view_component/template.rb +++ b/lib/view_component/template.rb @@ -5,66 +5,110 @@ class Template DataWithSource = Struct.new(:format, :identifier, :short_identifier, :type, keyword_init: true) DataNoSource = Struct.new(:source, :identifier, :type, keyword_init: true) - attr_reader :variant, :this_format, :type - - def initialize( - component:, - type:, - this_format: nil, - variant: nil, - lineno: nil, - path: nil, - extension: nil, - source: nil, - method_name: nil, - defined_on_self: true - ) + attr_reader :details + + delegate :virtual_path, to: :@component + delegate :format, :variant, to: :@details + + def initialize(component:, details:, lineno: nil, path: nil) @component = component - @type = type - @this_format = this_format - @variant = variant&.to_sym + @details = details @lineno = lineno @path = path - @extension = extension - @source = source - @method_name = method_name - @defined_on_self = defined_on_self - - @source_originally_nil = @source.nil? - - @call_method_name = - if @method_name - @method_name - else - out = +"call" - out << "_#{normalized_variant_name}" if @variant.present? - out << "_#{@this_format}" if @this_format.present? && @this_format != ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT - out + end + + class File < Template + def initialize(component:, details:, path:) + super( + component: component, + details: details, + path: path, + lineno: 0 + ) + end + + def type + :file + end + + # Load file each time we look up #source in case the file has been modified + def source + ::File.read(@path) + end + end + + class Inline < Template + attr_reader :source + + def initialize(component:, inline_template:) + details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, nil, nil) + + super( + component: component, + details: details, + path: inline_template.path, + lineno: inline_template.lineno, + ) + + @source = inline_template.source.dup + end + + def type + :inline + end + end + + class InlineCall < Template + def initialize(component:, method_name:, defined_on_self:) + variant = method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil + details = ActionView::TemplateDetails.new(nil, nil, nil, variant) + + super(component: component, details: details) + + @call_method_name = method_name + @defined_on_self = defined_on_self + end + + def type + :inline_call + end + + def compile_to_component + @component.define_method(safe_method_name, @component.instance_method(@call_method_name)) + end + + def safe_method_name_call + m = safe_method_name + proc do + maybe_escape_html(send(m)) do + Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. " \ + "The output will be automatically escaped, but you may want to investigate.") + end end + end + + def defined_on_self? + @defined_on_self + end end def compile_to_component - if !inline_call? - @component.silence_redefinition_of_method(@call_method_name) + @component.silence_redefinition_of_method(call_method_name) - # rubocop:disable Style/EvalWithLocation - @component.class_eval <<-RUBY, @path, @lineno - def #{@call_method_name} + # rubocop:disable Style/EvalWithLocation + @component.class_eval <<~RUBY, @path, @lineno + def #{call_method_name} #{compiled_source} end - RUBY - # rubocop:enable Style/EvalWithLocation - end + RUBY + # rubocop:enable Style/EvalWithLocation @component.define_method(safe_method_name, @component.instance_method(@call_method_name)) end def safe_method_name_call - return safe_method_name unless inline_call? - - "maybe_escape_html(#{safe_method_name}) " \ - "{ Kernel.warn('WARNING: The #{@component} component rendered HTML-unsafe output. " \ - "The output will be automatically escaped, but you may want to investigate.') } " + m = safe_method_name + proc { send(m) } end def requires_compiled_superclass? @@ -72,55 +116,42 @@ def requires_compiled_superclass? end def inline_call? - @type == :inline_call - end - - def inline? - @type == :inline + type == :inline_call end def default_format? - @this_format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT + format.nil? || format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT end + alias_method :html?, :default_format? - def format - @this_format + def call_method_name + @call_method_name ||= + ["call", (normalized_variant_name if variant.present?), (format unless default_format?)] + .compact.join("_").to_sym end def safe_method_name - "_#{@call_method_name}_#{@component.name.underscore.gsub("/", "__")}" + "_#{call_method_name}_#{@component.name.underscore.gsub("/", "__")}" end def normalized_variant_name - @variant.to_s.gsub("-", "__").gsub(".", "___") - end - - def defined_on_self? - @defined_on_self + variant.to_s.gsub("-", "__") end private - def source - if @source_originally_nil - # Load file each time we look up #source in case the file has been modified - File.read(@path) - else - @source - end - end - def compiled_source - handler = ActionView::Template.handler_for_extension(@extension) + handler = details.handler_class this_source = source this_source.rstrip! if @component.strip_trailing_whitespace? short_identifier = defined?(Rails.root) ? @path.sub("#{Rails.root}/", "") : @path - type = ActionView::Template::Types[@this_format] + format = self.format || ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT + type = ActionView::Template::Types[format] if handler.method(:call).parameters.length > 1 handler.call( - DataWithSource.new(format: @this_format, identifier: @path, short_identifier: short_identifier, type: type), + DataWithSource.new(format: format, identifier: @path, short_identifier: short_identifier, type: type), this_source ) # :nocov: diff --git a/lib/view_component/test_helpers.rb b/lib/view_component/test_helpers.rb index 3677763fb..6b6ace7b2 100644 --- a/lib/view_component/test_helpers.rb +++ b/lib/view_component/test_helpers.rb @@ -49,16 +49,7 @@ def assert_component_rendered # @return [Nokogiri::HTML] def render_inline(component, **args, &block) @page = nil - @rendered_content = - if Rails.version.to_f >= 6.1 - vc_test_controller.view_context.render(component, args, &block) - - # :nocov: - else - vc_test_controller.view_context.render_component(component, &block) - end - - # :nocov: + @rendered_content = vc_test_controller.view_context.render(component, args, &block) Nokogiri::HTML.fragment(@rendered_content) end @@ -136,11 +127,11 @@ def render_in_view_context(*args, &block) # end # ``` # - # @param variant [Symbol] The variant to be set for the provided block. - def with_variant(variant) + # @param variants [Symbol[]] The variants to be set for the provided block. + def with_variant(*variants) old_variants = vc_test_controller.view_context.lookup_context.variants - vc_test_controller.view_context.lookup_context.variants = variant + vc_test_controller.view_context.lookup_context.variants += variants yield ensure vc_test_controller.view_context.lookup_context.variants = old_variants @@ -173,9 +164,14 @@ def with_controller_class(klass) # end # ``` # - # @param format [Symbol] The format to be set for the provided block. - def with_format(format) - with_request_url("/", format: format) { yield } + # @param formats [Symbol[]] The format(s) to be set for the provided block. + def with_format(*formats) + old_formats = vc_test_controller.view_context.lookup_context.formats + + vc_test_controller.view_context.lookup_context.formats = formats + yield + ensure + vc_test_controller.view_context.lookup_context.formats = old_formats end # Set the URL of the current request (such as when using request-dependent path helpers): @@ -205,7 +201,7 @@ def with_format(format) # @param full_path [String] The path to set for the current request. # @param host [String] The host to set for the current request. # @param method [String] The request method to set for the current request. - def with_request_url(full_path, host: nil, method: nil, format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT) + def with_request_url(full_path, host: nil, method: nil) old_request_host = vc_test_request.host old_request_method = vc_test_request.request_method old_request_path_info = vc_test_request.path_info @@ -225,7 +221,6 @@ def with_request_url(full_path, host: nil, method: nil, format: ViewComponent::B vc_test_request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query).with_indifferent_access) vc_test_request.set_header(Rack::QUERY_STRING, query) - vc_test_request.format = format yield ensure vc_test_request.host = old_request_host diff --git a/test/sandbox/app/components/container_component.rb b/test/sandbox/app/components/container_component.rb index 471502918..f06f9428d 100644 --- a/test/sandbox/app/components/container_component.rb +++ b/test/sandbox/app/components/container_component.rb @@ -2,10 +2,6 @@ class ContainerComponent < ViewComponent::Base def call - if Rails.application.config.view_component.render_monkey_patch_enabled || Rails.version.to_f >= 6.1 - render HelpersProxyComponent.new - else - render_component HelpersProxyComponent.new - end + render HelpersProxyComponent.new end end diff --git a/test/sandbox/app/components/monkey_patch_disabled_component.html.erb b/test/sandbox/app/components/monkey_patch_disabled_component.html.erb deleted file mode 100644 index d2d68fe70..000000000 --- a/test/sandbox/app/components/monkey_patch_disabled_component.html.erb +++ /dev/null @@ -1 +0,0 @@ -
hello,world!
diff --git a/test/sandbox/app/components/monkey_patch_disabled_component.rb b/test/sandbox/app/components/monkey_patch_disabled_component.rb deleted file mode 100644 index f01f73f31..000000000 --- a/test/sandbox/app/components/monkey_patch_disabled_component.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -class MonkeyPatchDisabledComponent < ViewComponent::Base -end diff --git a/test/sandbox/app/components/turbo_stream_format_component.html+custom.erb b/test/sandbox/app/components/turbo_stream_format_component.html+custom.erb new file mode 100644 index 000000000..2a019d410 --- /dev/null +++ b/test/sandbox/app/components/turbo_stream_format_component.html+custom.erb @@ -0,0 +1 @@ +Hi turbo stream custom! diff --git a/test/sandbox/app/components/turbo_stream_format_component.html.erb b/test/sandbox/app/components/turbo_stream_format_component.html.erb new file mode 100644 index 000000000..f6e0d0b4f --- /dev/null +++ b/test/sandbox/app/components/turbo_stream_format_component.html.erb @@ -0,0 +1 @@ +Hi turbo stream! diff --git a/test/sandbox/app/components/turbo_stream_format_component.rb b/test/sandbox/app/components/turbo_stream_format_component.rb new file mode 100644 index 000000000..ea4a3d0fc --- /dev/null +++ b/test/sandbox/app/components/turbo_stream_format_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class TurboStreamFormatComponent < ViewComponent::Base +end diff --git a/test/sandbox/app/components/variants_component.html+mini.watch.erb b/test/sandbox/app/components/variants_component.html+mini.watch.erb deleted file mode 100644 index b44d8505e..000000000 --- a/test/sandbox/app/components/variants_component.html+mini.watch.erb +++ /dev/null @@ -1 +0,0 @@ -Mini Watch with dot diff --git a/test/sandbox/app/controllers/integration_examples_controller.rb b/test/sandbox/app/controllers/integration_examples_controller.rb index 54c7c4068..b5e8dd505 100644 --- a/test/sandbox/app/controllers/integration_examples_controller.rb +++ b/test/sandbox/app/controllers/integration_examples_controller.rb @@ -23,25 +23,15 @@ def controller_inline_baseline end def controller_to_string - # Ensures render_to_string_monkey_patch.rb correctly calls `super` when - # not rendering a component: render_to_string("integration_examples/_controller_inline", locals: {message: "bar"}) render(plain: render_to_string(ControllerInlineComponent.new(message: "bar"))) end - def controller_inline_render_component - render_component(ControllerInlineComponent.new(message: "bar")) - end - def helpers_proxy_component render(plain: render_to_string(HelpersProxyComponent.new)) end - def controller_to_string_render_component - render(plain: render_component_to_string(ControllerInlineComponent.new(message: "bar"))) - end - def products @products = [Product.new(name: "Radio clock"), Product.new(name: "Mints")] end diff --git a/test/sandbox/app/views/integration_examples/render_component.html.erb b/test/sandbox/app/views/integration_examples/render_component.html.erb deleted file mode 100644 index 29a63eab4..000000000 --- a/test/sandbox/app/views/integration_examples/render_component.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render_component ErbComponent.new(message: "bar") %> diff --git a/test/sandbox/config/environments/test.rb b/test/sandbox/config/environments/test.rb index 43cd3b2d8..5fab7ad61 100644 --- a/test/sandbox/config/environments/test.rb +++ b/test/sandbox/config/environments/test.rb @@ -25,7 +25,7 @@ config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates - config.action_dispatch.show_exceptions = (Rails::VERSION::STRING < "7.1") ? false : :none + config.action_dispatch.show_exceptions = :none # Disable request forgery protection in test environment config.action_controller.allow_forgery_protection = false @@ -33,7 +33,6 @@ config.view_component.show_previews = true config.view_component.preview_paths << "#{Rails.root}/lib/component_previews" - config.view_component.render_monkey_patch_enabled = true config.view_component.show_previews_source = true config.view_component.test_controller = "IntegrationExamplesController" config.view_component.capture_compatibility_patch_enabled = ENV["CAPTURE_PATCH_ENABLED"] == "true" @@ -51,7 +50,7 @@ # Print deprecation notices to the stderr config.active_support.deprecation = :stderr - config.action_view.annotate_rendered_view_with_filenames = true if Rails.version.to_f >= 6.1 + config.action_view.annotate_rendered_view_with_filenames = true config.eager_load = true end diff --git a/test/sandbox/config/routes.rb b/test/sandbox/config/routes.rb index 6bdb4525d..f5b7a43a3 100644 --- a/test/sandbox/config/routes.rb +++ b/test/sandbox/config/routes.rb @@ -15,9 +15,6 @@ get :controller_inline_with_block, to: "integration_examples#controller_inline_with_block" get :controller_inline_baseline, to: "integration_examples#controller_inline_baseline" get :controller_to_string, to: "integration_examples#controller_to_string" - get :render_component, to: "integration_examples#render_component" - get :controller_inline_render_component, to: "integration_examples#controller_inline_render_component" - get :controller_to_string_render_component, to: "integration_examples#controller_to_string_render_component" get :layout_default, to: "layouts#default" get :layout_global_for_action, to: "layouts#global_for_action" get :layout_explicit_in_action, to: "layouts#explicit_in_action" diff --git a/test/sandbox/test/base_test.rb b/test/sandbox/test/base_test.rb index 587dfde4f..ac84a5a79 100644 --- a/test/sandbox/test/base_test.rb +++ b/test/sandbox/test/base_test.rb @@ -88,7 +88,6 @@ def test_sidecar_files end def test_does_not_render_additional_newline_with_render_in - skip unless Rails::VERSION::MAJOR >= 7 without_template_annotations do ActionView::Template::Handlers::ERB.strip_trailing_newlines = true rendered_output = Array.new(2) { @@ -97,7 +96,7 @@ def test_does_not_render_additional_newline_with_render_in assert_includes rendered_output, "Hello, world!Hello, world!" end ensure - ActionView::Template::Handlers::ERB.strip_trailing_newlines = false if Rails::VERSION::MAJOR >= 7 + ActionView::Template::Handlers::ERB.strip_trailing_newlines = false end def test_evaled_component diff --git a/test/sandbox/test/components/previews/monkey_patch_disabled_component_preview.rb b/test/sandbox/test/components/previews/monkey_patch_disabled_component_preview.rb deleted file mode 100644 index 54f4269b7..000000000 --- a/test/sandbox/test/components/previews/monkey_patch_disabled_component_preview.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class MonkeyPatchDisabledComponentPreview < ViewComponent::Preview - def default - render_component(MonkeyPatchDisabledComponent.new(title: "Lorem Ipsum")) - end -end diff --git a/test/sandbox/test/config_test.rb b/test/sandbox/test/config_test.rb index 86c662a02..0a74018b4 100644 --- a/test/sandbox/test/config_test.rb +++ b/test/sandbox/test/config_test.rb @@ -15,7 +15,6 @@ def test_defaults_are_correct assert_equal @config.show_previews_source, false assert_equal @config.instrumentation_enabled, false assert_equal @config.use_deprecated_instrumentation_name, true - assert_equal @config.render_monkey_patch_enabled, true assert_equal @config.show_previews, true assert_equal @config.preview_paths, ["#{Rails.root}/test/components/previews"] end diff --git a/test/sandbox/test/integration_test.rb b/test/sandbox/test/integration_test.rb index 76e65cf4d..bf567e25b 100644 --- a/test/sandbox/test/integration_test.rb +++ b/test/sandbox/test/integration_test.rb @@ -14,15 +14,13 @@ def test_rendering_component_in_a_view assert_select("div", "Foo\n bar") end - if Rails.version.to_f >= 6.1 - def test_rendering_component_with_template_annotations_enabled - get "/" - assert_response :success + def test_rendering_component_with_template_annotations_enabled + get "/" + assert_response :success - assert_includes response.body, "BEGIN app/components/erb_component.html.erb" + assert_includes response.body, "BEGIN app/components/erb_component.html.erb" - assert_select("div", "Foo\n bar") - end + assert_select("div", "Foo\n bar") end def test_rendering_component_in_a_controller @@ -493,42 +491,6 @@ def test_renders_empty_slot_without_error assert_response :success end - if Rails.version.to_f >= 6.1 - def test_rendering_component_using_the_render_component_helper_raises_an_error - error = - assert_raises ActionView::Template::Error do - get "/render_component" - end - - matcher = (RUBY_VERSION >= "3.4") ? /undefined method 'render_component'/ : /undefined method `render_component' for/ - assert_match(matcher, error.message) - end - end - - if Rails.version.to_f < 6.1 - def test_rendering_component_using_render_component - get "/render_component" - assert_includes response.body, "bar" - end - - def test_rendering_component_in_a_controller_using_render_component - get "/controller_inline_render_component" - assert_includes response.body, "bar" - end - - def test_rendering_component_in_a_controller_using_render_component_to_string - get "/controller_to_string_render_component" - assert_includes response.body, "bar" - end - - def test_rendering_component_in_preview_using_render_component_and_monkey_patch_disabled - with_render_monkey_patch_config(false) do - get "/rails/view_components/monkey_patch_disabled_component/default" - assert_includes response.body, "
hello,world!
" - end - end - end - def test_renders_the_inline_component_preview_examples_with_default_behaviour_and_with_their_own_templates get "/rails/view_components/inline_component/default" assert_select "input" do @@ -549,25 +511,23 @@ def test_renders_the_inline_component_preview_examples_with_default_behaviour_an end def test_does_not_render_additional_newline - skip unless Rails::VERSION::MAJOR >= 7 without_template_annotations do ActionView::Template::Handlers::ERB.strip_trailing_newlines = true get "/rails/view_components/display_inline_component/with_newline" assert_includes response.body, "Hello, world!Hello, world!" end ensure - ActionView::Template::Handlers::ERB.strip_trailing_newlines = false if Rails::VERSION::MAJOR >= 7 + ActionView::Template::Handlers::ERB.strip_trailing_newlines = false end def test_does_not_render_additional_newline_with_render_in - skip unless Rails::VERSION::MAJOR >= 7 without_template_annotations do ActionView::Template::Handlers::ERB.strip_trailing_newlines = true get "/rails/view_components/display_inline_component/with_newline_render_in" assert_includes response.body, "Hello, world!Hello, world!" end ensure - ActionView::Template::Handlers::ERB.strip_trailing_newlines = false if Rails::VERSION::MAJOR >= 7 + ActionView::Template::Handlers::ERB.strip_trailing_newlines = false end # This test documents a bug that reports an incompatibility with the turbo-rails gem's `turbo_stream` helper. @@ -617,8 +577,6 @@ def test_renders_the_inline_component_using_a_non_standard_located_template end def test_renders_an_inline_component_preview_using_a_haml_template - skip if Rails::VERSION::STRING < "6.1" - get "/rails/view_components/inline_component/with_haml" assert_select "h1", "Some HAML here" assert_select "input[name=?]", "name" @@ -631,8 +589,6 @@ def test_returns_404_when_preview_does_not_exist end def test_renders_a_mix_of_haml_and_erb - skip if Rails::VERSION::STRING < "6.1" - get "/nested_haml" assert_response :success assert_select ".foo > .bar > .baz > .quux > .haml-div" @@ -647,8 +603,6 @@ def test_raises_an_error_if_the_template_is_not_present_and_the_render_with_temp end def test_renders_a_preview_template_using_haml_params_from_url_custom_template_and_locals - skip if Rails::VERSION::STRING < "6.1" - get "/rails/view_components/inline_component/with_several_options?form_title=Title from params" assert_select "form" do diff --git a/test/sandbox/test/layouts_test.rb b/test/sandbox/test/layouts_test.rb index 7bba7b10a..9cf7a62f9 100644 --- a/test/sandbox/test/layouts_test.rb +++ b/test/sandbox/test/layouts_test.rb @@ -3,35 +3,33 @@ require "test_helper" class LayoutsTest < ActionDispatch::IntegrationTest - if Rails.version.to_f >= 6.1 - test "rendering default layout" do - get "/layout_default" - assert_response :success - assert_select 'body[data-layout="application"]' - end + test "rendering default layout" do + get "/layout_default" + assert_response :success + assert_select 'body[data-layout="application"]' + end - test "rendering global_for_action" do - get "/layout_global_for_action" - assert_response :success - assert_select 'body[data-layout="global_for_action"]' - end + test "rendering global_for_action" do + get "/layout_global_for_action" + assert_response :success + assert_select 'body[data-layout="global_for_action"]' + end - test "rendering explicit_in_action" do - get "/layout_explicit_in_action" - assert_response :success - assert_select 'body[data-layout="explicit_in_action"]' - end + test "rendering explicit_in_action" do + get "/layout_explicit_in_action" + assert_response :success + assert_select 'body[data-layout="explicit_in_action"]' + end - test "rendering disabled_in_action" do - get "/layout_disabled_in_action" - assert_response :success - assert_select "body", 0 - end + test "rendering disabled_in_action" do + get "/layout_disabled_in_action" + assert_response :success + assert_select "body", 0 + end - test "rendering with_content_for" do - get "/layout_with_content_for" - assert_response :success - assert_select 'body[data-layout="with_content_for"]', "Hello content for\n\n Foo: bar" - end + test "rendering with_content_for" do + get "/layout_with_content_for" + assert_response :success + assert_select 'body[data-layout="with_content_for"]', "Hello content for\n\n Foo: bar" end end diff --git a/test/sandbox/test/preview_helper_test.rb b/test/sandbox/test/preview_helper_test.rb index 5921a525d..b979db27f 100644 --- a/test/sandbox/test/preview_helper_test.rb +++ b/test/sandbox/test/preview_helper_test.rb @@ -49,143 +49,4 @@ def test_returns_template_data_with_template_of_different_languages assert_equal(template_data[:prism_language_name], language) end end - - if Rails.version.to_f < 6.1 - def test_returns_template_data_without_dedicated_template - template_identifier = "preview/template" - expected_source = "<%= PreviewTest %>" - - PreviewHelper::AVAILABLE_PRISM_LANGUAGES.each do |language| - mock_template = Minitest::Mock.new - mock_template.expect(:source, expected_source) - mock_template.expect(:source, expected_source) - mock_template.expect(:identifier, "html.#{language}") - - lookup_context = Minitest::Mock.new - expected_template_path = "some/path/#{template_identifier}.html.haml" - lookup_context.expect(:find_template, mock_template, [template_identifier]) - - mock = Minitest::Mock.new - mock.expect :map, [expected_template_path] - ViewComponent::Base.stub(:preview_paths, mock) do - template_data = PreviewHelper.find_template_data( - lookup_context: lookup_context, - template_identifier: template_identifier - ) - - assert_equal(template_data[:source], expected_source) - assert_equal(template_data[:prism_language_name], language) - end - end - end - - def test_returns_template_data_with_dedicated_template - template_identifier = "preview/template" - expected_source = "<%= PreviewTest %>" - - PreviewHelper::AVAILABLE_PRISM_LANGUAGES.each do |language| - mock_template = Minitest::Mock.new - mock_template.expect(:source, "") - mock_template.expect(:source, "") - - lookup_context = Minitest::Mock.new - expected_template_path = "some/path/#{template_identifier}.html.#{language}" - lookup_context.expect(:find_template, mock_template, [template_identifier]) - - mock = Minitest::Mock.new - mock.expect :map, [expected_template_path] - Rails.application.config.view_component.stub(:preview_paths, mock) do - File.stub(:read, expected_source, [expected_template_path]) do - template_data = PreviewHelper.find_template_data( - lookup_context: lookup_context, - template_identifier: template_identifier - ) - - assert_equal(template_data[:source], expected_source) - assert_equal(template_data[:prism_language_name], language) - end - end - end - end - - def test_raises_with_no_matching_template - template_identifier = "preview/template" - - mock_template = Minitest::Mock.new - mock_template.expect(:source, "") - mock_template.expect(:source, "") - - lookup_context = Minitest::Mock.new - lookup_context.expect(:find_template, mock_template, [template_identifier]) - - mock = Minitest::Mock.new - mock.expect :map, [] - Rails.application.config.view_component.stub :preview_paths, mock do - exception = assert_raises ViewComponent::NoMatchingTemplatesForPreviewError do - PreviewHelper.find_template_data( - lookup_context: lookup_context, - template_identifier: template_identifier - ) - end - - assert_equal("Found 0 matches for templates for #{template_identifier}.", exception.message) - end - end - - def test_raises_with_conflict_in_template_resolution - template_identifier = "preview/template" - - mock_template = Minitest::Mock.new - mock_template.expect(:source, "") - mock_template.expect(:source, "") - - lookup_context = Minitest::Mock.new - lookup_context.expect(:find_template, mock_template, [template_identifier]) - - mock = Minitest::Mock.new - mock.expect :map, [template_identifier + ".html.haml", template_identifier + ".html.erb"] - Rails.application.config.view_component.stub :preview_paths, mock do - exception = assert_raises ViewComponent::MultipleMatchingTemplatesForPreviewError do - PreviewHelper.find_template_data( - lookup_context: lookup_context, - template_identifier: template_identifier - ) - end - - assert_equal("Found multiple templates for #{template_identifier}.", exception.message) - end - end - - def test_prism_css_source_url_with_asset_pipeline - Rails.application.config.public_file_server.stub(:enabled, true) do - if Rails.version.to_f >= 6.1 - assert_equal "/assets/prism.css", PreviewHelper.prism_css_source_url - else - assert_equal "/prism.css", PreviewHelper.prism_css_source_url - end - end - end - - def test_prism_css_source_url_with_no_asset_pipeline - Rails.application.config.public_file_server.stub(:enabled, false) do - assert_equal "https://cdn.jsdelivr.net/npm/prismjs@1.28.0/themes/prism.min.css", PreviewHelper.prism_css_source_url - end - end - - def test_prism_js_source_with_asset_pipeline - Rails.application.config.public_file_server.stub(:enabled, true) do - if Rails.version.to_f >= 6.1 - assert_equal "/assets/prism.min.js", PreviewHelper.prism_js_source_url - else - assert_equal "/prism.min.js", PreviewHelper.prism_js_source_url - end - end - end - - def test_prism_js_source_url_with_no_asset_pipeline - Rails.application.config.public_file_server.stub(:enabled, false) do - assert_equal "https://cdn.jsdelivr.net/npm/prismjs@1.28.0/prism.min.js", PreviewHelper.prism_js_source_url - end - end - end end diff --git a/test/sandbox/test/rendering_test.rb b/test/sandbox/test/rendering_test.rb index bd39865b7..bdd120f0c 100644 --- a/test/sandbox/test/rendering_test.rb +++ b/test/sandbox/test/rendering_test.rb @@ -15,7 +15,7 @@ def test_render_inline_allocations ViewComponent::CompileCache.cache.delete(MyComponent) MyComponent.ensure_compiled - assert_allocations("3.4.0" => 109, "3.3.6" => 115, "3.3.0" => 127, "3.2.6" => 114, "3.1.6" => 114, "3.0.7" => 123) do + assert_allocations("3.5.0" => 104, "3.4.1" => 107, "3.3.6" => 107, "3.2.6" => 105) do render_inline(MyComponent.new) end @@ -117,8 +117,6 @@ def test_renders_slim_template end def test_renders_haml_with_html_formatted_slot - skip if Rails::VERSION::STRING < "6.1" - render_inline(HamlHtmlFormattedSlotComponent.new) assert_selector("p", text: "HTML Formatted one") @@ -192,19 +190,19 @@ def test_renders_component_with_variant end end - def test_renders_component_with_variant_containing_a_dash - with_variant :"mini-watch" do + def test_renders_component_with_multiple_variants + with_variant :app, :phone do render_inline(VariantsComponent.new) - assert_text("Mini Watch with dash") + assert_text("Phone") end end - def test_renders_component_with_variant_containing_a_dot - with_variant :"mini.watch" do + def test_renders_component_with_variant_containing_a_dash + with_variant :"mini-watch" do render_inline(VariantsComponent.new) - assert_text("Mini Watch with dot") + assert_text("Mini Watch with dash") end end @@ -269,17 +267,9 @@ def test_renders_helper_method_through_proxy def test_renders_helper_method_within_nested_component render_inline(ContainerComponent.new) - assert_text("Hello helper method") end - def test_renders_helper_method_within_nested_component_with_disabled_monkey_patch - with_render_monkey_patch_config(false) do - render_inline(ContainerComponent.new) - assert_text("Hello helper method") - end - end - def test_renders_path_helper render_inline(PathComponent.new) @@ -1216,6 +1206,20 @@ def test_with_format end end + def test_with_format_missing + with_format(:xml) do + exception = + assert_raises ViewComponent::MissingTemplateError do + render_inline(MultipleFormatsComponent.new) + end + + assert_includes( + exception.message, + "No templates for MultipleFormatsComponent match the request" + ) + end + end + def test_localised_component render_inline(LocalisedComponent.new) @@ -1227,4 +1231,14 @@ def test_request_param assert_text("foo") end + + def test_turbo_stream_format_custom_variant + with_format(:turbo_stream, :html) do + with_variant(:custom) do + render_inline(TurboStreamFormatComponent.new) + + assert_text("Hi turbo stream custom!") + end + end + end end diff --git a/test/test_engine/test/config_test.rb b/test/test_engine/test/config_test.rb index 16258d003..5300cadcd 100644 --- a/test/test_engine/test/config_test.rb +++ b/test/test_engine/test/config_test.rb @@ -15,7 +15,6 @@ def test_defaults_are_correct assert_equal @config.show_previews_source, false assert_equal @config.instrumentation_enabled, false assert_equal @config.use_deprecated_instrumentation_name, true - assert_equal @config.render_monkey_patch_enabled, true assert_equal @config.show_previews, true assert_equal @config.preview_paths, ["#{TestEngine::Engine.root}/test/components/previews"] end diff --git a/test/test_helper.rb b/test/test_helper.rb index 61e9ff33e..5ea189c8d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -169,10 +169,6 @@ def with_default_preview_layout(layout, &block) with_config_option(:default_preview_layout, layout, &block) end -def with_render_monkey_patch_config(enabled, &block) - with_config_option(:render_monkey_patch_enabled, enabled, &block) -end - def with_compiler_development_mode(mode) previous_mode = ViewComponent::Compiler.development_mode ViewComponent::Compiler.development_mode = mode diff --git a/view_component.gemspec b/view_component.gemspec index 711abf550..51c353c89 100644 --- a/view_component.gemspec +++ b/view_component.gemspec @@ -27,9 +27,9 @@ Gem::Specification.new do |spec| spec.files = Dir["LICENSE.txt", "README.md", "app/**/*", "docs/CHANGELOG.md", "lib/**/*"] spec.require_paths = ["lib"] - spec.required_ruby_version = ">= 2.7.0" + spec.required_ruby_version = ">= 3.2.0" - spec.add_runtime_dependency "activesupport", [">= 5.2.0", "< 8.1"] + spec.add_runtime_dependency "activesupport", [">= 7.1.0", "< 8.1"] spec.add_runtime_dependency "method_source", "~> 1.0" spec.add_runtime_dependency "concurrent-ruby", "~> 1.0" spec.add_development_dependency "allocation_stats", "~> 0.1.5" @@ -61,11 +61,9 @@ Gem::Specification.new do |spec| spec.add_development_dependency "yard", "~> 0.9.34" spec.add_development_dependency "yard-activesupport-concern", "~> 0.0.1" - if RUBY_VERSION >= "3.1" - spec.add_development_dependency "net-imap" - spec.add_development_dependency "net-pop" - spec.add_development_dependency "net-smtp" - end + spec.add_development_dependency "net-imap" + spec.add_development_dependency "net-pop" + spec.add_development_dependency "net-smtp" if RUBY_VERSION >= "3.3" spec.add_development_dependency "base64"