actionview-component 1.13.0 → 1.14.0

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