-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathwrapper_component.rb
More file actions
98 lines (79 loc) · 3.34 KB
/
wrapper_component.rb
File metadata and controls
98 lines (79 loc) · 3.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# frozen_string_literal: true
module ViewComponentContrib
# WrapperComponent allows to wrap any component with a custom HTML code.
# The whole wrapper is only rendered when the child component.render? returns true.
# Thus, wrapper could be used to conditionally render the outer html for components without
# conditionals in templates.
class WrapperComponent < ViewComponent::Base
include WrappedInHelper
class DoubleRenderError < StandardError
def initialize(component)
super("A child component could only be rendered once within a wrapper: #{component}")
end
end
class ComponentPresentError < StandardError
def initialize
super("A wrapper component cannot register a component if it already has a component from the constructor")
end
end
attr_reader :component_instance, :registered_components, :placeholder_block
# We need to touch `content` before the `render?` method is called,
# otherwise children calling `.wrapped_in` won't be registered.
# This overrides the default lazy evaluation of `content` in ViewComponent,
# but it's necessary for the wrapper to work properly.
def before_render
content if component_instance.blank?
end
def initialize(component = nil)
@component_instance = component
@registered_components = []
end
def render?
return component_instance.render? if component_instance.present?
return true if render_from_registered_components?
@placeholder_block.present?
end
# Simply return the contents of the block passed to #render_component.
# (Alias couldn't be used here 'cause ViewComponent check for the method presence when
# choosing between #call and a template.)
def call
return view_context.capture(&@placeholder_block) if render_placeholder?
content
end
# Returns rendered child component
# The name component is chosen for convenient usage in templates,
# so we can simply call `= wrapper.component` in the place where we're going
# to put the component
def component
raise DoubleRenderError, component_instance if @rendered
@rendered = component_instance.render_in(view_context).html_safe
end
# Register a component to be rendered within the wrapper.
# If no registered components render, the wrapper itself won't be rendered.
def register(component)
raise ComponentPresentError if component_instance.present?
raise ArgumentError, "Expected a ViewComponent" unless component.is_a?(ViewComponent::Base)
registered_components << component
end
# Register a placeholder block: `wrapper.placeholder { "Nothing to show" }`
# The block is only emitted when:
# - no component instance was supplied, AND
# - every registered component’s `render?` returns false
def placeholder(&block)
raise ArgumentError, "Block required" unless block
@placeholder_block = block
nil
end
private
# Memoize the result of registered components' render? calls as they
# could be expensive
def render_from_registered_components?
@render_from_registered_components ||= registered_components.any?(&:render?)
end
def render_placeholder?
@placeholder_block.present? &&
component_instance.blank? &&
!render_from_registered_components?
end
end
end