Skip to content

Commit

Permalink
Refactor a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
camertron committed Jul 14, 2023
1 parent 7991f70 commit 0bdd60c
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 91 deletions.
2 changes: 2 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
44 changes: 44 additions & 0 deletions docs/guide/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>
<div class="base-component-template">
<% yield :parent %>
</div>
```

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.

Check failure on line 132 in docs/guide/templates.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/guide/templates.md#L132

[Microsoft.Contractions] Use 'doesn't' instead of 'does not'.
Raw output
{"message": "[Microsoft.Contractions] Use 'doesn't' instead of 'does not'.", "location": {"path": "docs/guide/templates.md", "range": {"start": {"line": 132, "column": 160}}}, "severity": "ERROR"}

`yield :parent` also works with inline templates:

```ruby
class MyComponent < ViewComponent::Base
erb_template <<~ERB
<div>
<% yield :parent %>
</div>
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
"<div>#{super}</div>"
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.

Check failure on line 157 in docs/guide/templates.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/guide/templates.md#L157

[Microsoft.Contractions] Use 'doesn't' instead of 'does not'.
Raw output
{"message": "[Microsoft.Contractions] Use 'doesn't' instead of 'does not'.", "location": {"path": "docs/guide/templates.md", "range": {"start": {"line": 157, "column": 96}}}, "severity": "ERROR"}

### 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
Expand Down
19 changes: 5 additions & 14 deletions lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:
#
Expand All @@ -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
Expand Down
137 changes: 60 additions & 77 deletions lib/view_component/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

0 comments on commit 0bdd60c

Please sign in to comment.