actionview-component 1.13.0 → 1.14.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +43 -55
  5. data/actionview-component.gemspec +2 -2
  6. data/app/controllers/rails/components_controller.rb +4 -4
  7. data/lib/action_view/component.rb +1 -21
  8. data/lib/action_view/component/base.rb +1 -262
  9. data/lib/action_view/component/preview.rb +1 -72
  10. data/lib/action_view/component/railtie.rb +1 -1
  11. data/lib/action_view/component/test_case.rb +1 -4
  12. data/lib/rails/generators/component/component_generator.rb +1 -1
  13. data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
  14. data/lib/view_component.rb +29 -0
  15. data/lib/view_component/base.rb +273 -0
  16. data/lib/view_component/conversion.rb +9 -0
  17. data/lib/view_component/engine.rb +65 -0
  18. data/lib/view_component/preview.rb +77 -0
  19. data/lib/view_component/previewable.rb +25 -0
  20. data/lib/view_component/render_monkey_patch.rb +31 -0
  21. data/lib/view_component/rendering_monkey_patch.rb +13 -0
  22. data/lib/view_component/template_error.rb +9 -0
  23. data/lib/view_component/test_case.rb +9 -0
  24. data/lib/view_component/test_helpers.rb +43 -0
  25. data/lib/view_component/version.rb +11 -0
  26. data/script/console +1 -1
  27. metadata +14 -10
  28. data/lib/action_view/component/conversion.rb +0 -11
  29. data/lib/action_view/component/engine.rb +0 -67
  30. data/lib/action_view/component/previewable.rb +0 -27
  31. data/lib/action_view/component/render_monkey_patch.rb +0 -34
  32. data/lib/action_view/component/rendering_monkey_patch.rb +0 -15
  33. data/lib/action_view/component/template_error.rb +0 -11
  34. data/lib/action_view/component/test_helpers.rb +0 -45
  35. data/lib/action_view/component/version.rb +0 -13
@@ -1,79 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/descendants_tracker"
4
-
5
3
  module ActionView
6
4
  module Component # :nodoc:
7
- class Preview
8
- extend ActiveSupport::DescendantsTracker
9
-
10
- def render(component, **args, &block)
11
- { component: component, args: args, block: block }
12
- end
13
-
14
- class << self
15
- # Returns all component preview classes.
16
- def all
17
- load_previews if descendants.empty?
18
- descendants
19
- end
20
-
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)
24
- end
25
-
26
- # Returns the component object class associated to the preview.
27
- def component
28
- name.chomp("Preview").constantize
29
- end
30
-
31
- # Returns all of the available examples for the component preview.
32
- def examples
33
- public_instance_methods(false).map(&:to_s).sort
34
- end
35
-
36
- # Returns +true+ if the example of the component preview exists.
37
- def example_exists?(example)
38
- examples.include?(example)
39
- end
40
-
41
- # Returns +true+ if the preview exists.
42
- def exists?(preview)
43
- all.any? { |p| p.preview_name == preview }
44
- end
45
-
46
- # Find a component preview by its underscored class name.
47
- def find(preview)
48
- all.find { |p| p.preview_name == preview }
49
- end
50
-
51
- # Returns the underscored name of the component preview without the suffix.
52
- def preview_name
53
- name.chomp("Preview").underscore
54
- end
55
-
56
- # Setter for layout name.
57
- def layout(layout_name)
58
- @layout = layout_name
59
- end
60
-
61
- private
62
-
63
- def load_previews
64
- if preview_path
65
- Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
66
- end
67
- end
68
-
69
- def preview_path
70
- Base.preview_path
71
- end
72
-
73
- def show_previews
74
- Base.show_previews
75
- end
76
- end
5
+ class Preview < ViewComponent::Preview
77
6
  end
78
7
  end
79
8
  end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_view/component/engine"
3
+ require "view_component/engine"
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/test_case"
4
-
5
3
  module ActionView
6
4
  module Component
7
- class TestCase < ActiveSupport::TestCase
8
- include ActionView::Component::TestHelpers
5
+ class TestCase < ViewComponent::TestCase
9
6
  end
10
7
  end
11
8
  end
@@ -35,7 +35,7 @@ module Rails
35
35
  end
36
36
 
37
37
  def parent_class
38
- defined?(ApplicationComponent) ? "ApplicationComponent" : "ActionView::Component::Base"
38
+ defined?(ApplicationComponent) ? "ApplicationComponent" : "ViewComponent::Base"
39
39
  end
40
40
 
41
41
  def initialize_signature
@@ -1,6 +1,6 @@
1
1
  require "test_helper"
2
2
 
3
- class <%= class_name %>ComponentTest < ActionView::Component::TestCase
3
+ class <%= class_name %>ComponentTest < ViewComponent::TestCase
4
4
  test "component renders something useful" do
5
5
  # assert_equal(
6
6
  # %(<span title="my title">Hello, components!</span>),
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require "action_view"
3
+ require "active_support/dependencies/autoload"
4
+
5
+ module ViewComponent
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :Base
9
+ autoload :Conversion
10
+ autoload :Preview
11
+ autoload :Previewable
12
+ autoload :TestHelpers
13
+ autoload :TestCase
14
+ autoload :RenderMonkeyPatch
15
+ autoload :RenderingMonkeyPatch
16
+ autoload :TemplateError
17
+ end
18
+
19
+ module ActionView
20
+ module Component
21
+ extend ActiveSupport::Autoload
22
+
23
+ autoload :Base
24
+ autoload :Preview
25
+ autoload :TestCase
26
+ end
27
+ end
28
+
29
+ ActiveModel::Conversion.include ViewComponent::Conversion
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+ require "active_support/configurable"
5
+ require "view_component/previewable"
6
+
7
+ module ViewComponent
8
+ class Base < ActionView::Base
9
+ include ActiveSupport::Configurable
10
+ include ViewComponent::Previewable
11
+
12
+ delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
13
+
14
+ class_attribute :content_areas, default: []
15
+ self.content_areas = [] # default doesn't work until Rails 5.2
16
+
17
+ # Entrypoint for rendering components.
18
+ #
19
+ # view_context: ActionView context from calling view
20
+ # block: optional block to be captured within the view context
21
+ #
22
+ # returns HTML that has been escaped by the respective template handler
23
+ #
24
+ # Example subclass:
25
+ #
26
+ # app/components/my_component.rb:
27
+ # class MyComponent < ViewComponent::Base
28
+ # def initialize(title:)
29
+ # @title = title
30
+ # end
31
+ # end
32
+ #
33
+ # app/components/my_component.html.erb
34
+ # <span title="<%= @title %>">Hello, <%= content %>!</span>
35
+ #
36
+ # In use:
37
+ # <%= render MyComponent.new(title: "greeting") do %>world<% end %>
38
+ # returns:
39
+ # <span title="greeting">Hello, world!</span>
40
+ #
41
+ def render_in(view_context, &block)
42
+ self.class.compile!
43
+ @view_context = view_context
44
+ @view_renderer ||= view_context.view_renderer
45
+ @lookup_context ||= view_context.lookup_context
46
+ @view_flow ||= view_context.view_flow
47
+ @virtual_path ||= virtual_path
48
+ @variant = @lookup_context.variants.first
49
+
50
+ old_current_template = @current_template
51
+ @current_template = self
52
+
53
+ @content = view_context.capture(self, &block) if block_given?
54
+
55
+ before_render_check
56
+
57
+ if render?
58
+ send(self.class.call_method_name(@variant))
59
+ else
60
+ ""
61
+ end
62
+ ensure
63
+ @current_template = old_current_template
64
+ end
65
+
66
+ def before_render_check
67
+ # noop
68
+ end
69
+
70
+ def render?
71
+ true
72
+ end
73
+
74
+ def initialize(*); end
75
+
76
+ def render(options = {}, args = {}, &block)
77
+ if options.is_a?(String) || (options.is_a?(Hash) && options.has_key?(:partial))
78
+ view_context.render(options, args, &block)
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ def controller
85
+ @controller ||= view_context.controller
86
+ end
87
+
88
+ # Provides a proxy to access helper methods through
89
+ def helpers
90
+ @helpers ||= view_context
91
+ end
92
+
93
+ # Removes the first part of the path and the extension.
94
+ def virtual_path
95
+ self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
96
+ end
97
+
98
+ def view_cache_dependencies
99
+ []
100
+ end
101
+
102
+ def format # :nodoc:
103
+ @variant
104
+ end
105
+
106
+ def with(area, content = nil, &block)
107
+ unless content_areas.include?(area)
108
+ raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
109
+ end
110
+
111
+ if block_given?
112
+ content = view_context.capture(&block)
113
+ end
114
+
115
+ instance_variable_set("@#{area}".to_sym, content)
116
+ nil
117
+ end
118
+
119
+ private
120
+
121
+ def request
122
+ @request ||= controller.request
123
+ end
124
+
125
+ attr_reader :content, :view_context
126
+
127
+ # The controller used for testing components.
128
+ # Defaults to ApplicationController. This should be set early
129
+ # in the initialization process and should be set to a string.
130
+ mattr_accessor :test_controller
131
+ @@test_controller = "ApplicationController"
132
+
133
+ class << self
134
+ def inherited(child)
135
+ child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
136
+
137
+ super
138
+ end
139
+
140
+ def call_method_name(variant)
141
+ if variant.present? && variants.include?(variant)
142
+ "call_#{variant}"
143
+ else
144
+ "call"
145
+ end
146
+ end
147
+
148
+ def source_location
149
+ @source_location ||=
150
+ begin
151
+ # Require `#initialize` to be defined so that we can use `method#source_location`
152
+ # to look up the filename of the component.
153
+ initialize_method = instance_method(:initialize)
154
+ initialize_method.source_location[0] if initialize_method.owner == self
155
+ end
156
+ end
157
+
158
+ def compiled?
159
+ @compiled && ActionView::Base.cache_template_loading
160
+ end
161
+
162
+ def compile!
163
+ compile(raise_template_errors: true)
164
+ end
165
+
166
+ # Compile templates to instance methods, assuming they haven't been compiled already.
167
+ # We could in theory do this on app boot, at least in production environments.
168
+ # Right now this just compiles the first time the component is rendered.
169
+ def compile(raise_template_errors: false)
170
+ return if compiled?
171
+
172
+ if template_errors.present?
173
+ raise ViewComponent::TemplateError.new(template_errors) if raise_template_errors
174
+ return false
175
+ end
176
+
177
+ templates.each do |template|
178
+ class_eval <<-RUBY, template[:path], -1
179
+ def #{call_method_name(template[:variant])}
180
+ @output_buffer = ActionView::OutputBuffer.new
181
+ #{compiled_template(template[:path])}
182
+ end
183
+ RUBY
184
+ end
185
+
186
+ @compiled = true
187
+ end
188
+
189
+ def variants
190
+ templates.map { |template| template[:variant] }
191
+ end
192
+
193
+ # we'll eventually want to update this to support other types
194
+ def type
195
+ "text/html"
196
+ end
197
+
198
+ def identifier
199
+ source_location
200
+ end
201
+
202
+ def with_content_areas(*areas)
203
+ if areas.include?(:content)
204
+ raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
205
+ end
206
+ attr_reader *areas
207
+ self.content_areas = areas
208
+ end
209
+
210
+ private
211
+
212
+ def matching_views_in_source_location
213
+ return [] unless source_location
214
+ (Dir["#{source_location.chomp(File.extname(source_location))}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location])
215
+ end
216
+
217
+ def templates
218
+ @templates ||=
219
+ matching_views_in_source_location.each_with_object([]) do |path, memo|
220
+ pieces = File.basename(path).split(".")
221
+
222
+ memo << {
223
+ path: path,
224
+ variant: pieces.second.split("+").second&.to_sym,
225
+ handler: pieces.last
226
+ }
227
+ end
228
+ end
229
+
230
+ def template_errors
231
+ @template_errors ||=
232
+ begin
233
+ errors = []
234
+ if source_location.nil?
235
+ # Require `#initialize` to be defined so that we can use `method#source_location`
236
+ # to look up the filename of the component.
237
+ errors << "#{self} must implement #initialize."
238
+ end
239
+
240
+ errors << "Could not find a template file for #{self}." if templates.empty?
241
+
242
+ if templates.count { |template| template[:variant].nil? } > 1
243
+ errors << "More than one template found for #{self}. There can only be one default template file per component."
244
+ end
245
+
246
+ invalid_variants = templates
247
+ .group_by { |template| template[:variant] }
248
+ .map { |variant, grouped| variant if grouped.length > 1 }
249
+ .compact
250
+ .sort
251
+
252
+ unless invalid_variants.empty?
253
+ 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."
254
+ end
255
+ errors
256
+ end
257
+ end
258
+
259
+ def compiled_template(file_path)
260
+ handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
261
+ template = File.read(file_path)
262
+
263
+ if handler.method(:call).parameters.length > 1
264
+ handler.call(self, template)
265
+ else # remove before upstreaming into Rails
266
+ handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
267
+ end
268
+ end
269
+ end
270
+
271
+ ActiveSupport.run_load_hooks(:action_view_component, self)
272
+ end
273
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent # :nodoc:
4
+ module Conversion
5
+ def to_component_class
6
+ "#{self.class.name}Component".safe_constantize
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "view_component"
5
+
6
+ module ViewComponent
7
+ class Engine < Rails::Engine # :nodoc:
8
+ config.action_view_component = ActiveSupport::OrderedOptions.new
9
+
10
+ initializer "action_view_component.set_configs" do |app|
11
+ options = app.config.action_view_component
12
+
13
+ options.show_previews = Rails.env.development? if options.show_previews.nil?
14
+
15
+ if options.show_previews
16
+ options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/components/previews" : nil
17
+ end
18
+
19
+ ActiveSupport.on_load(:action_view_component) do
20
+ options.each { |k, v| send("#{k}=", v) }
21
+ end
22
+ end
23
+
24
+ initializer "action_view_component.set_autoload_paths" do |app|
25
+ options = app.config.action_view_component
26
+
27
+ if options.show_previews && options.preview_path
28
+ ActiveSupport::Dependencies.autoload_paths << options.preview_path
29
+ end
30
+ end
31
+
32
+ initializer "action_view_component.eager_load_actions" do
33
+ ActiveSupport.on_load(:after_initialize) do
34
+ ViewComponent::Base.descendants.each(&:compile)
35
+ end
36
+ end
37
+
38
+ initializer "action_view_component.compile_config_methods" do
39
+ ActiveSupport.on_load(:action_view_component) do
40
+ config.compile_methods! if config.respond_to?(:compile_methods!)
41
+ end
42
+ end
43
+
44
+ initializer "action_view_component.monkey_patch_render" do
45
+ ActiveSupport.on_load(:action_view) do
46
+ ActionView::Base.prepend ViewComponent::RenderMonkeyPatch
47
+ end
48
+
49
+ ActiveSupport.on_load(:action_controller) do
50
+ ActionController::Base.prepend ViewComponent::RenderingMonkeyPatch
51
+ end
52
+ end
53
+
54
+ config.after_initialize do |app|
55
+ options = app.config.action_view_component
56
+
57
+ if options.show_previews
58
+ app.routes.prepend do
59
+ get "/rails/components" => "rails/components#index", :internal => true
60
+ get "/rails/components/*path" => "rails/components#previews", :internal => true
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end