actionview-component 1.5.3 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE +25 -0
- data/.github/PULL_REQUEST_TEMPLATE +17 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +106 -0
- data/CONTRIBUTING.md +4 -13
- data/Gemfile.lock +13 -2
- data/README.md +282 -9
- data/actionview-component.gemspec +1 -0
- data/lib/action_view/component.rb +20 -2
- data/lib/action_view/component/base.rb +95 -39
- data/lib/action_view/component/conversion.rb +11 -0
- data/lib/action_view/component/preview.rb +8 -35
- data/lib/action_view/component/previewable.rb +27 -0
- data/lib/action_view/component/railtie.rb +15 -1
- data/lib/action_view/component/render_monkey_patch.rb +29 -0
- data/lib/action_view/component/template_error.rb +11 -0
- data/lib/action_view/component/test_case.rb +11 -0
- data/lib/action_view/component/test_helpers.rb +2 -2
- data/lib/action_view/component/version.rb +2 -2
- data/lib/rails/generators/component/component_generator.rb +2 -2
- data/lib/rails/generators/rspec/component_generator.rb +1 -1
- data/lib/rails/generators/test_unit/component_generator.rb +1 -1
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +2 -4
- data/lib/railties/lib/rails.rb +0 -1
- data/lib/railties/lib/rails/components_controller.rb +5 -1
- data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +1 -1
- metadata +23 -5
- data/lib/action_view/component/monkey_patch.rb +0 -27
- data/lib/railties/lib/rails/component_examples_controller.rb +0 -9
- data/lib/railties/lib/rails/templates/rails/examples/show.html.erb +0 -1
@@ -39,6 +39,7 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_development_dependency "minitest", "= 5.1.0"
|
40
40
|
spec.add_development_dependency "haml", "~> 5"
|
41
41
|
spec.add_development_dependency "slim", "~> 4.0"
|
42
|
+
spec.add_development_dependency "better_html", "~> 1"
|
42
43
|
spec.add_development_dependency "rubocop", "= 0.74"
|
43
44
|
spec.add_development_dependency "rubocop-github", "~> 0.13.0"
|
44
45
|
end
|
@@ -1,5 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "action_view
|
3
|
+
require "active_model"
|
4
|
+
require "action_view"
|
5
|
+
require "active_support/dependencies/autoload"
|
5
6
|
require "action_view/component/railtie"
|
7
|
+
|
8
|
+
module ActionView
|
9
|
+
module Component
|
10
|
+
extend ActiveSupport::Autoload
|
11
|
+
|
12
|
+
autoload :Base
|
13
|
+
autoload :Conversion
|
14
|
+
autoload :Preview
|
15
|
+
autoload :Previewable
|
16
|
+
autoload :TestHelpers
|
17
|
+
autoload :TestCase
|
18
|
+
autoload :RenderMonkeyPatch
|
19
|
+
autoload :TemplateError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
ActiveModel::Conversion.include ActionView::Component::Conversion
|
@@ -1,20 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_model"
|
4
|
-
require "action_view"
|
5
3
|
require "active_support/configurable"
|
6
|
-
require_relative "preview"
|
7
4
|
|
8
5
|
module ActionView
|
9
6
|
module Component
|
10
7
|
class Base < ActionView::Base
|
11
8
|
include ActiveModel::Validations
|
12
9
|
include ActiveSupport::Configurable
|
13
|
-
include ActionView::Component::
|
10
|
+
include ActionView::Component::Previewable
|
14
11
|
|
15
12
|
delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
|
16
13
|
|
17
|
-
|
14
|
+
class_attribute :content_areas, default: []
|
15
|
+
self.content_areas = [] # default doesn't work until Rails 5.2
|
18
16
|
|
19
17
|
# Entrypoint for rendering components. Called by ActionView::Base#render.
|
20
18
|
#
|
@@ -42,17 +40,21 @@ module ActionView
|
|
42
40
|
# <span title="greeting">Hello, world!</span>
|
43
41
|
#
|
44
42
|
def render_in(view_context, *args, &block)
|
45
|
-
self.class.compile
|
43
|
+
self.class.compile!
|
46
44
|
@view_context = view_context
|
47
45
|
@view_renderer ||= view_context.view_renderer
|
48
46
|
@lookup_context ||= view_context.lookup_context
|
49
47
|
@view_flow ||= view_context.view_flow
|
50
48
|
@virtual_path ||= virtual_path
|
51
49
|
@variant = @lookup_context.variants.first
|
50
|
+
|
51
|
+
return "" unless render?
|
52
|
+
|
52
53
|
old_current_template = @current_template
|
53
54
|
@current_template = self
|
54
55
|
|
55
|
-
@content = view_context.capture(&block) if block_given?
|
56
|
+
@content = view_context.capture(self, &block) if block_given?
|
57
|
+
|
56
58
|
validate!
|
57
59
|
|
58
60
|
send(self.class.call_method_name(@variant))
|
@@ -60,6 +62,10 @@ module ActionView
|
|
60
62
|
@current_template = old_current_template
|
61
63
|
end
|
62
64
|
|
65
|
+
def render?
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
63
69
|
def initialize(*); end
|
64
70
|
|
65
71
|
def render(options = {}, args = {}, &block)
|
@@ -92,20 +98,33 @@ module ActionView
|
|
92
98
|
@variant
|
93
99
|
end
|
94
100
|
|
95
|
-
|
101
|
+
def with(area, content = nil, &block)
|
102
|
+
unless content_areas.include?(area)
|
103
|
+
raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
|
104
|
+
end
|
96
105
|
|
97
|
-
|
98
|
-
|
106
|
+
if block_given?
|
107
|
+
content = view_context.capture(&block)
|
108
|
+
end
|
99
109
|
|
100
|
-
|
110
|
+
instance_variable_set("@#{area}".to_sym, content)
|
111
|
+
nil
|
101
112
|
end
|
102
113
|
|
114
|
+
private
|
115
|
+
|
103
116
|
def request
|
104
117
|
@request ||= controller.request
|
105
118
|
end
|
106
119
|
|
107
120
|
attr_reader :content, :view_context
|
108
121
|
|
122
|
+
# The controller used for testing components.
|
123
|
+
# Defaults to ApplicationController. This should be set early
|
124
|
+
# in the initialization process and should be set to a string.
|
125
|
+
mattr_accessor :test_controller
|
126
|
+
@@test_controller = "ApplicationController"
|
127
|
+
|
109
128
|
class << self
|
110
129
|
def inherited(child)
|
111
130
|
child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
|
@@ -114,7 +133,7 @@ module ActionView
|
|
114
133
|
end
|
115
134
|
|
116
135
|
def call_method_name(variant)
|
117
|
-
if variant.present?
|
136
|
+
if variant.present? && variants.include?(variant)
|
118
137
|
"call_#{variant}"
|
119
138
|
else
|
120
139
|
"call"
|
@@ -122,25 +141,39 @@ module ActionView
|
|
122
141
|
end
|
123
142
|
|
124
143
|
def source_location
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
144
|
+
@source_location ||=
|
145
|
+
begin
|
146
|
+
# Require #initialize to be defined so that we can use
|
147
|
+
# method#source_location to look up the file name
|
148
|
+
# of the component.
|
149
|
+
#
|
150
|
+
# If we were able to only support Ruby 2.7+,
|
151
|
+
# We could just use Module#const_source_location,
|
152
|
+
# rendering this unnecessary.
|
153
|
+
#
|
154
|
+
initialize_method = instance_method(:initialize)
|
155
|
+
initialize_method.source_location[0] if initialize_method.owner == self
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def compiled?
|
160
|
+
@compiled && ActionView::Base.cache_template_loading
|
161
|
+
end
|
133
162
|
|
134
|
-
|
163
|
+
def compile!
|
164
|
+
compile(validate: true)
|
135
165
|
end
|
136
166
|
|
137
167
|
# Compile templates to instance methods, assuming they haven't been compiled already.
|
138
168
|
# We could in theory do this on app boot, at least in production environments.
|
139
169
|
# Right now this just compiles the first time the component is rendered.
|
140
|
-
def compile
|
141
|
-
return if
|
170
|
+
def compile(validate: false)
|
171
|
+
return if compiled?
|
142
172
|
|
143
|
-
|
173
|
+
if template_errors.present?
|
174
|
+
raise ActionView::Component::TemplateError.new(template_errors) if validate
|
175
|
+
return false
|
176
|
+
end
|
144
177
|
|
145
178
|
templates.each do |template|
|
146
179
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
@@ -164,36 +197,59 @@ module ActionView
|
|
164
197
|
end
|
165
198
|
|
166
199
|
def identifier
|
167
|
-
|
200
|
+
source_location
|
201
|
+
end
|
202
|
+
|
203
|
+
def with_content_areas(*areas)
|
204
|
+
if areas.include?(:content)
|
205
|
+
raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
|
206
|
+
end
|
207
|
+
attr_reader *areas
|
208
|
+
self.content_areas = areas
|
168
209
|
end
|
169
210
|
|
170
211
|
private
|
171
212
|
|
213
|
+
def matching_views_in_source_location
|
214
|
+
return [] unless source_location
|
215
|
+
(Dir["#{source_location.chomp(File.extname(source_location))}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location])
|
216
|
+
end
|
217
|
+
|
172
218
|
def templates
|
173
219
|
@templates ||=
|
174
|
-
|
220
|
+
matching_views_in_source_location.each_with_object([]) do |path, memo|
|
221
|
+
pieces = File.basename(path).split(".")
|
222
|
+
|
175
223
|
memo << {
|
176
224
|
path: path,
|
177
|
-
variant:
|
178
|
-
handler:
|
225
|
+
variant: pieces.second.split("+").second&.to_sym,
|
226
|
+
handler: pieces.last
|
179
227
|
}
|
180
228
|
end
|
181
229
|
end
|
182
230
|
|
183
|
-
def
|
184
|
-
|
185
|
-
|
186
|
-
|
231
|
+
def template_errors
|
232
|
+
@template_errors ||=
|
233
|
+
begin
|
234
|
+
errors = []
|
235
|
+
errors << "#{self} must implement #initialize." if source_location.nil?
|
236
|
+
errors << "Could not find a template file for #{self}." if templates.empty?
|
187
237
|
|
188
|
-
|
189
|
-
|
190
|
-
|
238
|
+
if templates.count { |template| template[:variant].nil? } > 1
|
239
|
+
errors << "More than one template found for #{self}. There can only be one default template file per component."
|
240
|
+
end
|
191
241
|
|
192
|
-
|
193
|
-
|
242
|
+
invalid_variants = templates
|
243
|
+
.group_by { |template| template[:variant] }
|
244
|
+
.map { |variant, grouped| variant if grouped.length > 1 }
|
245
|
+
.compact
|
246
|
+
.sort
|
194
247
|
|
195
|
-
|
196
|
-
|
248
|
+
unless invalid_variants.empty?
|
249
|
+
errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be one template file per variant."
|
250
|
+
end
|
251
|
+
errors
|
252
|
+
end
|
197
253
|
end
|
198
254
|
|
199
255
|
def compiled_template(file_path)
|
@@ -1,37 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/concern"
|
4
3
|
require "active_support/descendants_tracker"
|
5
|
-
require_relative "test_helpers"
|
6
4
|
|
7
5
|
module ActionView
|
8
|
-
module Component
|
9
|
-
module Previews
|
10
|
-
extend ActiveSupport::Concern
|
11
|
-
|
12
|
-
included do
|
13
|
-
# Set the location of component previews through app configuration:
|
14
|
-
#
|
15
|
-
# config.action_view_component.preview_path = "#{Rails.root}/lib/component_previews"
|
16
|
-
#
|
17
|
-
mattr_accessor :preview_path, instance_writer: false
|
18
|
-
|
19
|
-
# Enable or disable component previews through app configuration:
|
20
|
-
#
|
21
|
-
# config.action_view_component.show_previews = true
|
22
|
-
#
|
23
|
-
# Defaults to +true+ for development environment
|
24
|
-
#
|
25
|
-
mattr_accessor :show_previews, instance_writer: false
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
6
|
+
module Component # :nodoc:
|
29
7
|
class Preview
|
30
8
|
extend ActiveSupport::DescendantsTracker
|
31
|
-
include ActionView::Component::TestHelpers
|
32
9
|
|
33
|
-
def render(component,
|
34
|
-
|
10
|
+
def render(component, **args, &block)
|
11
|
+
{ component: component, args: args, block: block }
|
35
12
|
end
|
36
13
|
|
37
14
|
class << self
|
@@ -41,18 +18,14 @@ module ActionView
|
|
41
18
|
descendants
|
42
19
|
end
|
43
20
|
|
44
|
-
# Returns the
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
Rails::ComponentExamplesController.render(template: "examples/show",
|
49
|
-
layout: @layout || "layouts/application",
|
50
|
-
assigns: { example: example_html })
|
21
|
+
# Returns the arguments for rendering of the component in its layout
|
22
|
+
def render_args(example)
|
23
|
+
new.public_send(example).merge(layout: @layout)
|
51
24
|
end
|
52
25
|
|
53
26
|
# Returns the component object class associated to the preview.
|
54
27
|
def component
|
55
|
-
|
28
|
+
name.chomp("Preview").constantize
|
56
29
|
end
|
57
30
|
|
58
31
|
# Returns all of the available examples for the component preview.
|
@@ -77,7 +50,7 @@ module ActionView
|
|
77
50
|
|
78
51
|
# Returns the underscored name of the component preview without the suffix.
|
79
52
|
def preview_name
|
80
|
-
name.
|
53
|
+
name.chomp("Preview").underscore
|
81
54
|
end
|
82
55
|
|
83
56
|
# Setter for layout name.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module ActionView
|
6
|
+
module Component # :nodoc:
|
7
|
+
module Previewable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
# Set the location of component previews through app configuration:
|
12
|
+
#
|
13
|
+
# config.action_view_component.preview_path = "#{Rails.root}/lib/component_previews"
|
14
|
+
#
|
15
|
+
mattr_accessor :preview_path, instance_writer: false
|
16
|
+
|
17
|
+
# Enable or disable component previews through app configuration:
|
18
|
+
#
|
19
|
+
# config.action_view_component.show_previews = true
|
20
|
+
#
|
21
|
+
# Defaults to +true+ for development environment
|
22
|
+
#
|
23
|
+
mattr_accessor :show_previews, instance_writer: false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "rails"
|
4
|
+
require "action_view/component"
|
5
|
+
|
3
6
|
module ActionView
|
4
7
|
module Component
|
5
8
|
class Railtie < Rails::Railtie # :nodoc:
|
@@ -21,7 +24,6 @@ module ActionView
|
|
21
24
|
|
22
25
|
initializer "action_view_component.set_autoload_paths" do |app|
|
23
26
|
require "railties/lib/rails/components_controller"
|
24
|
-
require "railties/lib/rails/component_examples_controller"
|
25
27
|
|
26
28
|
options = app.config.action_view_component
|
27
29
|
|
@@ -30,12 +32,24 @@ module ActionView
|
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
35
|
+
initializer "action_view_component.eager_load_actions" do
|
36
|
+
ActiveSupport.on_load(:after_initialize) do
|
37
|
+
ActionView::Component::Base.descendants.each(&:compile)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
33
41
|
initializer "action_view_component.compile_config_methods" do
|
34
42
|
ActiveSupport.on_load(:action_view_component) do
|
35
43
|
config.compile_methods! if config.respond_to?(:compile_methods!)
|
36
44
|
end
|
37
45
|
end
|
38
46
|
|
47
|
+
initializer "action_view_component.monkey_patch_render" do
|
48
|
+
ActiveSupport.on_load(:action_view) do
|
49
|
+
ActionView::Base.prepend ActionView::Component::RenderMonkeyPatch
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
39
53
|
config.after_initialize do |app|
|
40
54
|
options = app.config.action_view_component
|
41
55
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Monkey patch ActionView::Base#render to support ActionView::Component
|
4
|
+
#
|
5
|
+
# A version of this monkey patch was upstreamed in https://github.com/rails/rails/pull/36388
|
6
|
+
# We'll need to upstream an updated version of this eventually.
|
7
|
+
module ActionView
|
8
|
+
module Component
|
9
|
+
module RenderMonkeyPatch # :nodoc:
|
10
|
+
def render(options = {}, args = {}, &block)
|
11
|
+
if options.respond_to?(:render_in)
|
12
|
+
ActiveSupport::Deprecation.warn(
|
13
|
+
"passing component instances (`render MyComponent.new(foo: :bar)`) has been deprecated and will be removed in v2.0.0. Use `render MyComponent, foo: :bar` instead."
|
14
|
+
)
|
15
|
+
|
16
|
+
options.render_in(self, &block)
|
17
|
+
elsif options.is_a?(Class) && options < ActionView::Component::Base
|
18
|
+
options.new(args).render_in(self, &block)
|
19
|
+
elsif options.is_a?(Hash) && options.has_key?(:component)
|
20
|
+
options[:component].new(options[:locals]).render_in(self, &block)
|
21
|
+
elsif options.respond_to?(:to_component_class) && !options.to_component_class.nil?
|
22
|
+
options.to_component_class.new(options).render_in(self, &block)
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|