diff --git a/docs/api.md b/docs/api.md index b2b3f3304..1dda2b510 100644 --- a/docs/api.md +++ b/docs/api.md @@ -102,6 +102,8 @@ Returns HTML that has been escaped by the respective template handler. ### `#render_parent` +DEPRECATED + Subclass components that call `super` inside their template code will cause a double render if they emit the result: diff --git a/docs/guide/templates.md b/docs/guide/templates.md index 073191308..35bcb08e4 100644 --- a/docs/guide/templates.md +++ b/docs/guide/templates.md @@ -117,9 +117,53 @@ end ### Rendering parent templates +Since 3.5.0 +{: .label } + +To render a parent component's template from a subclass' template, use `yield :parent`: + +```erb +<%# my_link_component.html.erb %> +
+ <% yield :parent %> +
+``` + +If the parent supports the current variant, the variant will automatically be rendered. `yield :parent` replaces the deprecated `#render_parent` method, which does not respect variants or multiple levels of inheritance. + +`yield :parent` also works with inline templates: + +```ruby +class MyComponent < ViewComponent::Base + erb_template <<~ERB +
+ <% yield :parent %> +
+ ERB +end +``` + +To render a parent component's template from a `#call` method, call `super`. + +```ruby +class MyComponent < ViewComponent::Base + # "phone" variant + def call_phone + "
#{super}
" + end +end +``` + +`super` will attempt to call the `#call_phone` method on the parent class. If the parent class does not support the "phone" variant, Ruby will raise a `NoMethodError`. Consider using a template and `render :parent` to handle superclass variants automatically. + +### render_parent + Since 2.55.0 {: .label } +Deprecated +{: .label .label-red } + To render a parent component's template from a subclass, call `render_parent`: ```erb diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index ea11b4fa3..950b43bf1 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -115,6 +115,8 @@ def render_in(view_context, &block) @current_template = old_current_template end + # DEPRECATED + # # Subclass components that call `super` inside their template code will cause a # double render if they emit the result: # @@ -125,21 +127,10 @@ def render_in(view_context, &block) # # Calls `super`, returning `nil` to avoid rendering the result twice. def render_parent - # There are four scenarios to consider: - # - # 1. Scenario: Self responds to the variant method and so does the parent. - # Behavior: Call the parent's variant method (i.e. call super). - - # 2. Scenario: Self responds to the variant method but the parent does not. - # Behavior: Call the parent's #call method. + ViewComponent::Deprecation.deprecation_warning( + "render_parent", "Use `yield :parent` instead." + ) - # 3. Scenario: Self does not respond to the variant method but the parent does. - # Behavior: Call the child's variant method, which is also the parent's variant method - # by way of inheritance. - # - # 4. Scenario: Neither self nor the parent respond to the variant method. - # Behavior: Call the parent's #call method. - # mtd = @__vc_variant ? "call_#{@__vc_variant}" : "call" method(mtd).super_method.call nil diff --git a/lib/view_component/compiler.rb b/lib/view_component/compiler.rb index ffdf9fd7a..2afe8f257 100644 --- a/lib/view_component/compiler.rb +++ b/lib/view_component/compiler.rb @@ -43,45 +43,18 @@ def compile(raise_errors: false, force: false) component_class.validate_collection_parameter! end - unique_superclass_name = methodize(component_class.superclass.name) - if has_inline_template? template = component_class.inline_template - unique_method_name = "call__#{methodize(component_class.name)}" - redefinition_lock.synchronize do - component_class.silence_redefinition_of_method("call") - # rubocop:disable Style/EvalWithLocation - component_class.class_eval <<-RUBY, template.path, template.lineno - 1 - private def #{unique_method_name} - if block_given? - #{compiled_inline_template(template)} - else - #{unique_method_name} do |msg| - case msg - when :parent - super_method_name = if @__vc_variant - super_variant_method_name = :"call_\#{@__vc_variant}__#{unique_superclass_name}" - respond_to?(super_variant_method_name, true) ? super_variant_method_name : nil - end - - super_method_name ||= :call__#{unique_superclass_name} - send(super_method_name) - - nil - else - raise UnexpectedTemplateYield.new(msg) - end - end - end - end + template_info = { + path: template.path, + lineno: template.lineno - 1, + body: compiled_inline_template(template) + } - def call - #{unique_method_name} - end - RUBY - # rubocop:enable Style/EvalWithLocation + define_compiled_template_methods("call", template_info) + redefinition_lock.synchronize do component_class.silence_redefinition_of_method("render_template_for") component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def render_template_for(variant = nil) @@ -91,46 +64,14 @@ def render_template_for(variant = nil) end else templates.each do |template| - # Remove existing compiled template methods, - # as Ruby warns when redefining a method. method_name = call_method_name(template[:variant]) - unique_method_name = "#{method_name}__#{methodize(component_class.name)}" + template_info = { + path: template[:path], + lineno: -1, + body: compiled_template(template[:path]) + } - redefinition_lock.synchronize do - component_class.silence_redefinition_of_method(method_name) - component_class.silence_redefinition_of_method(unique_method_name) - - # rubocop:disable Style/EvalWithLocation - component_class.class_eval <<-RUBY, template[:path], -1 - private def #{unique_method_name} - if block_given? - #{compiled_template(template[:path])} - else - #{unique_method_name} do |msg| - case msg - when :parent - super_method_name = if @__vc_variant - super_variant_method_name = :"call_\#{@__vc_variant}__#{unique_superclass_name}" - respond_to?(super_variant_method_name, true) ? super_variant_method_name : nil - end - - super_method_name ||= :call__#{unique_superclass_name} - send(super_method_name) - - nil - else - raise UnexpectedTemplateYield.new(msg) - end - end - end - end - - def #{method_name} - #{unique_method_name} - end - RUBY - # rubocop:enable Style/EvalWithLocation - end + define_compiled_template_methods(method_name, template_info) end define_render_template_for @@ -145,6 +86,47 @@ def #{method_name} attr_reader :component_class, :redefinition_lock + def define_compiled_template_methods(method_name, template_info) + unique_method_name = "#{method_name}__#{methodize(component_class.name)}" + unique_superclass_name = methodize(component_class.superclass.name) + + redefinition_lock.synchronize do + # Remove existing compiled template methods, + # as Ruby warns when redefining a method. + component_class.silence_redefinition_of_method(method_name) + component_class.silence_redefinition_of_method(unique_method_name) + + component_class.class_eval <<-RUBY, template_info[:path], template_info[:lineno] + private def #{unique_method_name} + if block_given? + #{template_info[:body]} + else + #{unique_method_name} do |msg| + case msg + when :parent + super_method_name = if @__vc_variant + super_variant_method_name = :"call_\#{@__vc_variant}__#{unique_superclass_name}" + respond_to?(super_variant_method_name, true) ? super_variant_method_name : nil + end + + super_method_name ||= :call__#{unique_superclass_name} + send(super_method_name) + + nil + else + raise UnexpectedTemplateYield.new(msg) + end + end + end + end + + def #{method_name} + #{unique_method_name} + end + RUBY + end + end + def methodize(str) str.gsub("::", "_").underscore end @@ -333,11 +315,12 @@ def normalized_variant_name(variant) end def should_compile_superclass? - development? && templates.empty? && !has_inline_template? && - !( - component_class.instance_methods(false).include?(:call) || - component_class.private_instance_methods(false).include?(:call) - ) + development? && templates.empty? && !has_inline_template? && !call_defined? + end + + def call_defined? + component_class.instance_methods(false).include?(:call) || + component_class.private_instance_methods(false).include?(:call) end end end