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