actionview-component 1.5.3 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|