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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE +25 -0
  3. data/.github/PULL_REQUEST_TEMPLATE +17 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +4 -0
  6. data/CHANGELOG.md +106 -0
  7. data/CONTRIBUTING.md +4 -13
  8. data/Gemfile.lock +13 -2
  9. data/README.md +282 -9
  10. data/actionview-component.gemspec +1 -0
  11. data/lib/action_view/component.rb +20 -2
  12. data/lib/action_view/component/base.rb +95 -39
  13. data/lib/action_view/component/conversion.rb +11 -0
  14. data/lib/action_view/component/preview.rb +8 -35
  15. data/lib/action_view/component/previewable.rb +27 -0
  16. data/lib/action_view/component/railtie.rb +15 -1
  17. data/lib/action_view/component/render_monkey_patch.rb +29 -0
  18. data/lib/action_view/component/template_error.rb +11 -0
  19. data/lib/action_view/component/test_case.rb +11 -0
  20. data/lib/action_view/component/test_helpers.rb +2 -2
  21. data/lib/action_view/component/version.rb +2 -2
  22. data/lib/rails/generators/component/component_generator.rb +2 -2
  23. data/lib/rails/generators/rspec/component_generator.rb +1 -1
  24. data/lib/rails/generators/test_unit/component_generator.rb +1 -1
  25. data/lib/rails/generators/test_unit/templates/component_test.rb.tt +2 -4
  26. data/lib/railties/lib/rails.rb +0 -1
  27. data/lib/railties/lib/rails/components_controller.rb +5 -1
  28. data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +1 -1
  29. metadata +23 -5
  30. data/lib/action_view/component/monkey_patch.rb +0 -27
  31. data/lib/railties/lib/rails/component_examples_controller.rb +0 -9
  32. 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 "action_view/component/monkey_patch"
4
- require "action_view/component/base"
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::Previews
10
+ include ActionView::Component::Previewable
14
11
 
15
12
  delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
16
13
 
17
- validate :variant_exists
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
- private
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
- def variant_exists
98
- return if self.class.variants.include?(@variant) || @variant.nil?
106
+ if block_given?
107
+ content = view_context.capture(&block)
108
+ end
99
109
 
100
- errors.add(:variant, "'#{@variant}' has no template defined")
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
- # Require #initialize to be defined so that we can use
126
- # method#source_location to look up the file name
127
- # of the component.
128
- #
129
- # If we were able to only support Ruby 2.7+,
130
- # We could just use Module#const_source_location,
131
- # rendering this unnecessary.
132
- raise NotImplementedError.new("#{self} must implement #initialize.") unless self.instance_method(:initialize).owner == self
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
- instance_method(:initialize).source_location[0]
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 @compiled && ActionView::Base.cache_template_loading
170
+ def compile(validate: false)
171
+ return if compiled?
142
172
 
143
- validate_templates
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
- (Dir["#{source_location.sub(/#{File.extname(source_location)}$/, '')}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location]).each_with_object([]) do |path, memo|
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: path.split(".").second.split("+")[1]&.to_sym,
178
- handler: path.split(".").last
225
+ variant: pieces.second.split("+").second&.to_sym,
226
+ handler: pieces.last
179
227
  }
180
228
  end
181
229
  end
182
230
 
183
- def validate_templates
184
- if templates.empty?
185
- raise NotImplementedError.new("Could not find a template file for #{self}.")
186
- end
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
- if templates.select { |template| template[:variant].nil? }.length > 1
189
- raise StandardError.new("More than one template found for #{self}. There can only be one default template file per component.")
190
- end
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
- variants.each_with_object(Hash.new(0)) { |variant, counts| counts[variant] += 1 }.each do |variant, count|
193
- next unless count > 1
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
- raise StandardError.new("More than one template found for variant '#{variant}' in #{self}. There can only be one template file per variant.")
196
- end
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)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Component # :nodoc:
5
+ module Conversion
6
+ def to_component_class
7
+ "#{self.class.name}Component".safe_constantize
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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 #:nodoc:
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, *locals)
34
- render_inline(component, *locals)
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 html of the component in its layout
45
- def call(example)
46
- example_html = new.public_send(example)
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
- self.name.sub(%r{Preview$}, "").constantize
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.sub(/Preview$/, "").underscore
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