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.
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