actionview-component 1.3.5 → 1.5.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dcbc0baecff7159dafaf87120583a68055e931c926679bd8b3300b54232c769
4
- data.tar.gz: b5eda121857a5dd034457e1fda306bfb1fa1e666159d20729feaee2f2da699c1
3
+ metadata.gz: 30c2d62abc3a38ba7f383a934ea3a95d6e490f81868d062d9263130e6d694d84
4
+ data.tar.gz: 8cc4375853920bd79183e9e634e00a9f4804726c2fc2a60328f3f1ad664bccef
5
5
  SHA512:
6
- metadata.gz: cf003dfb9c8dc68e7cef33e4c78fa947006b58ec076658586c95bece8a65e98516989cba81a58bcd632a1706c9b7b93b19bd51006b6f8291256dc0062ba4db1c
7
- data.tar.gz: f7707687dee6494852ad1520154d57acff346f4fa6dcdb2bfc0e2f324ed4a4683fe7f2e3b8b1f3a69a38ff4dae0e0a72b53a7f73a3f524d0f7f6e9b07a625fd3
6
+ metadata.gz: 98e2021c3308374c1ef1d92d044b18bdcbb071e422a52cbfe102f984cbf34521d6c19b4f9b60b511f3f9ffa1e3b2e0fc7b5ec31dcf22420f5d4240baad42e881
7
+ data.tar.gz: 7e68770c3ec42381c7fce1028834b95ce4c6d04e1ce71e2849ce04a99b73b3539c2246c35bd77421a63c754e66fac083adaac1ae531bc37fb306293cd7a9a61e
@@ -8,22 +8,18 @@ jobs:
8
8
  strategy:
9
9
  matrix:
10
10
  rails_version: [5.0.0, 5.2.3, 6.0.0, master]
11
- ruby_version: [2.3.x, 2.4.x, 2.5.x, 2.6.x]
11
+ ruby_version: [2.4.x, 2.5.x, 2.6.x]
12
12
  exclude:
13
13
  - rails_version: master
14
14
  ruby_version: 2.4.x
15
- - rails_version: master
16
- ruby_version: 2.3.x
17
15
  - rails_version: 6.0.0
18
16
  ruby_version: 2.4.x
19
- - rails_version: 6.0.0
20
- ruby_version: 2.3.x
21
17
  steps:
22
18
  - uses: actions/checkout@master
23
19
  - name: Setup Ruby
24
20
  uses: actions/setup-ruby@v1
25
21
  with:
26
- version: ${{ matrix.ruby_version }}
22
+ ruby-version: ${{ matrix.ruby_version }}
27
23
  - name: Build and test with Rake
28
24
  run: |
29
25
  gem install bundler:1.14.0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,67 @@
1
+ # v1.5.2
2
+
3
+ * Disable eager loading initializer.
4
+
5
+ *Kasper Meyer*
6
+
7
+ # v1.5.1
8
+
9
+ * Update railties class to work with Rails 6.
10
+
11
+ *Juan Manuel Ramallo*
12
+
13
+ # v1.5.0
14
+
15
+ Note: `actionview-component` is now loaded by requiring `actionview/component`, not `actionview/component/base`.
16
+
17
+ * Fix issue with generating component method signatures.
18
+
19
+ *Ryan Workman, Dylan Clark*
20
+
21
+ * Create component generator.
22
+
23
+ *Vinicius Stock*
24
+
25
+ * Add helpers proxy.
26
+
27
+ *Kasper Meyer*
28
+
29
+ * Introduce ActionView::Component::Previews.
30
+
31
+ *Juan Manuel Ramallo*
32
+
33
+ # v1.4.0
34
+
35
+ * Fix bug where components broke in application paths with periods.
36
+
37
+ *Anton, Joel Hawksley*
38
+
39
+ * Add support for `cache_if` in component templates.
40
+
41
+ *Aaron Patterson, Joel Hawksley*
42
+
43
+ * Add support for variants.
44
+
45
+ *Juan Manuel Ramallo*
46
+
47
+ * Fix bug in virtual path lookup.
48
+
49
+ *Juan Manuel Ramallo*
50
+
51
+ * Preselect the rendered component in render_inline.
52
+
53
+ *Elia Schito*
54
+
55
+ # v1.3.6
56
+
57
+ * Allow template file names without format.
58
+
59
+ *Joel Hawksley*
60
+
61
+ * Add support for translations.
62
+
63
+ *Juan Manuel Ramallo*
64
+
1
65
  # v1.3.5
2
66
 
3
67
  * Re-expose `controller` method.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- actionview-component (1.3.5)
4
+ actionview-component (1.5.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -64,7 +64,7 @@ GEM
64
64
  ast (2.4.0)
65
65
  builder (3.2.3)
66
66
  concurrent-ruby (1.1.5)
67
- crass (1.0.4)
67
+ crass (1.0.5)
68
68
  erubi (1.8.0)
69
69
  globalid (0.4.2)
70
70
  activesupport (>= 4.2.0)
@@ -74,7 +74,7 @@ GEM
74
74
  i18n (1.6.0)
75
75
  concurrent-ruby (~> 1.0)
76
76
  jaro_winkler (1.5.3)
77
- loofah (2.2.3)
77
+ loofah (2.3.1)
78
78
  crass (~> 1.0.2)
79
79
  nokogiri (>= 1.5.9)
80
80
  mail (2.7.1)
@@ -87,7 +87,7 @@ GEM
87
87
  mini_portile2 (2.4.0)
88
88
  minitest (5.1.0)
89
89
  nio4r (2.5.2)
90
- nokogiri (1.10.4)
90
+ nokogiri (1.10.5)
91
91
  mini_portile2 (~> 2.4.0)
92
92
  parallel (1.17.0)
93
93
  parser (2.6.3.0)
@@ -168,7 +168,7 @@ DEPENDENCIES
168
168
  minitest (= 5.1.0)
169
169
  rails (= 6.0.0)
170
170
  rake (~> 10.0)
171
- rubocop (~> 0.59)
171
+ rubocop (= 0.74)
172
172
  rubocop-github (~> 0.13.0)
173
173
  slim (~> 4.0)
174
174
 
data/README.md CHANGED
@@ -15,7 +15,7 @@ As the goal of this gem is to be upstreamed into Rails, it is designed to integr
15
15
 
16
16
  ## Compatibility
17
17
 
18
- `actionview-component` is tested for compatibility with combinations of Ruby `2.3`/`2.4`/`2.5`/`2.6` and Rails `5.0.0`/`5.2.3`/`6.0.0`/`6.1.0.alpha`.
18
+ `actionview-component` is tested for compatibility with combinations of Ruby `2.4`/`2.5`/`2.6` and Rails `5.0.0`/`5.2.3`/`6.0.0`/`6.1.0.alpha`.
19
19
 
20
20
  ## Installation
21
21
  Add this line to your application's Gemfile:
@@ -32,7 +32,7 @@ $ bundle
32
32
  In `config/application.rb`, add:
33
33
 
34
34
  ```bash
35
- require "action_view/component/base"
35
+ require "action_view/component"
36
36
  ```
37
37
 
38
38
  ## Guide
@@ -97,10 +97,26 @@ Components are subclasses of `ActionView::Component::Base` and live in `app/comp
97
97
 
98
98
  Component class names end in -`Component`.
99
99
 
100
+ Component module names are plural, as they are for controllers. (`Users::AvatarComponent`)
101
+
100
102
  Components support ActiveModel validations. Components are validated after initialization, but before rendering.
101
103
 
102
104
  Content passed to an `ActionView::Component` as a block is captured and assigned to the `content` accessor.
103
105
 
106
+ #### Quick start
107
+
108
+ Use the component generator to create a new `ActionView::Component`.
109
+
110
+ The generator accepts the component name and the list of accepted properties as arguments:
111
+
112
+ ```bash
113
+ bin/rails generate component Example title content
114
+ invoke test_unit
115
+ create test/components/example_component_test.rb
116
+ create app/components/example_component.rb
117
+ create app/components/example_component.html.erb
118
+ ```
119
+
104
120
  #### Implementation
105
121
 
106
122
  An `ActionView::Component` is a Ruby file and corresponding template file (in any format supported by Rails) with the same base name:
@@ -178,7 +194,7 @@ class MyComponentTest < Minitest::Test
178
194
  def test_render_component
179
195
  assert_equal(
180
196
  %(<span title="my title">Hello, World!</span>),
181
- render_inline(TestComponent, title: "my title") { "Hello, World!" }.css("span").to_html
197
+ render_inline(TestComponent, title: "my title") { "Hello, World!" }.to_html
182
198
  )
183
199
  end
184
200
  end
@@ -186,6 +202,64 @@ end
186
202
 
187
203
  In general, we’ve found it makes the most sense to test components based on their rendered HTML.
188
204
 
205
+ #### Action Pack Variants
206
+
207
+ To test a specific variant you can wrap your test with the `with_variant` helper method as:
208
+
209
+ ```ruby
210
+ def test_render_component_for_tablet
211
+ with_variant :tablet do
212
+ assert_equal(
213
+ %(<span title="my title">Hello, tablets!</span>),
214
+ render_inline(TestComponent, title: "my title") { "Hello, tablets!" }.css("span").to_html
215
+ )
216
+ end
217
+ end
218
+ ```
219
+
220
+ ### Previewing Components
221
+ `ActionView::Component::Preview`s provide a way to see how components look by visiting a special URL that renders them.
222
+ In the previous example, the preview class for `TestComponent` would be called `TestComponentPreview` and located in `test/components/previews/test_component_preview.rb`.
223
+ To see the preview of the component with a given title, implement a method that renders the component.
224
+ You can define as many examples as you want:
225
+
226
+ ```ruby
227
+ # test/components/previews/test_component_preview.rb
228
+
229
+ class TestComponentPreview < ActionView::Component::Preview
230
+ def with_default_title
231
+ render(TestComponent, title: "Test component default")
232
+ end
233
+
234
+ def with_long_title
235
+ render(TestComponent, title: "This is a really long title to see how the component renders this")
236
+ end
237
+ end
238
+ ```
239
+
240
+ The previews will be available in <http://localhost:3000/rails/components/test_component/with_default_title>
241
+ and <http://localhost:3000/rails/components/test_component/with_long_title>.
242
+
243
+ Previews use the application layout by default, but you can also use other layouts from your app:
244
+
245
+ ```ruby
246
+ # test/components/previews/test_component_preview.rb
247
+
248
+ class TestComponentPreview < ActionView::Component::Preview
249
+ layout "admin"
250
+
251
+ ...
252
+ end
253
+ ```
254
+
255
+ By default, the preview classes live in `test/components/previews`.
256
+ This can be configured using the `preview_path` option.
257
+ For example, if you want to use `lib/component_previews`, set the following in `config/application.rb`:
258
+
259
+ ```ruby
260
+ config.action_view_component.preview_path = "#{Rails.root}/lib/component_previews"
261
+ ```
262
+
189
263
  ## Frequently Asked Questions
190
264
 
191
265
  ### Can I use other templating languages besides ERB?
@@ -39,6 +39,6 @@ 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 "rubocop", "~> 0.59"
42
+ spec.add_development_dependency "rubocop", "= 0.74"
43
43
  spec.add_development_dependency "rubocop-github", "~> 0.13.0"
44
44
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/component/monkey_patch"
4
+ require "action_view/component/base"
5
+ require "action_view/component/railtie"
@@ -1,37 +1,20 @@
1
1
  # frozen_string_literal: true
2
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
- class ActionView::Base
8
- module RenderMonkeyPatch
9
- def render(options = {}, args = {}, &block)
10
- if options.respond_to?(:render_in)
11
- ActiveSupport::Deprecation.warn(
12
- "passing component instances to `render` has been deprecated and will be removed in v2.0.0. Use `render MyComponent, foo: :bar` instead."
13
- )
14
-
15
- options.render_in(self, &block)
16
- elsif options.is_a?(Class) && options < ActionView::Component::Base
17
- options.new(args).render_in(self, &block)
18
- elsif options.is_a?(Hash) && options.has_key?(:component)
19
- options[:component].new(options[:locals]).render_in(self, &block)
20
- else
21
- super
22
- end
23
- end
24
- end
25
-
26
- prepend RenderMonkeyPatch
27
- end
3
+ require "active_model"
4
+ require "action_view"
5
+ require "active_support/configurable"
6
+ require_relative "preview"
28
7
 
29
8
  module ActionView
30
9
  module Component
31
10
  class Base < ActionView::Base
32
11
  include ActiveModel::Validations
33
12
  include ActiveSupport::Configurable
34
- include ActionController::RequestForgeryProtection
13
+ include ActionView::Component::Previews
14
+
15
+ delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
16
+
17
+ validate :variant_exists
35
18
 
36
19
  # Entrypoint for rendering components. Called by ActionView::Base#render.
37
20
  #
@@ -64,10 +47,17 @@ module ActionView
64
47
  @view_renderer ||= view_context.view_renderer
65
48
  @lookup_context ||= view_context.lookup_context
66
49
  @view_flow ||= view_context.view_flow
50
+ @virtual_path ||= virtual_path
51
+ @variant = @lookup_context.variants.first
52
+ old_current_template = @current_template
53
+ @current_template = self
67
54
 
68
55
  @content = view_context.capture(&block) if block_given?
69
56
  validate!
70
- call
57
+
58
+ send(self.class.call_method_name(@variant))
59
+ ensure
60
+ @current_template = old_current_template
71
61
  end
72
62
 
73
63
  def initialize(*); end
@@ -84,6 +74,38 @@ module ActionView
84
74
  @controller ||= view_context.controller
85
75
  end
86
76
 
77
+ # Provides a proxy to access helper methods through
78
+ def helpers
79
+ @helpers ||= view_context
80
+ end
81
+
82
+ # Removes the first part of the path and the extension.
83
+ def virtual_path
84
+ self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
85
+ end
86
+
87
+ def view_cache_dependencies
88
+ []
89
+ end
90
+
91
+ def format # :nodoc:
92
+ @variant
93
+ end
94
+
95
+ private
96
+
97
+ def variant_exists
98
+ return if self.class.variants.include?(@variant) || @variant.nil?
99
+
100
+ errors.add(:variant, "'#{@variant}' has no template defined")
101
+ end
102
+
103
+ def request
104
+ @request ||= controller.request
105
+ end
106
+
107
+ attr_reader :content, :view_context
108
+
87
109
  class << self
88
110
  def inherited(child)
89
111
  child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
@@ -91,80 +113,102 @@ module ActionView
91
113
  super
92
114
  end
93
115
 
94
- # Compile template to #call instance method, assuming it hasn't been compiled already.
116
+ def call_method_name(variant)
117
+ if variant.present?
118
+ "call_#{variant}"
119
+ else
120
+ "call"
121
+ end
122
+ end
123
+
124
+ 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
133
+
134
+ instance_method(:initialize).source_location[0]
135
+ end
136
+
137
+ # Compile templates to instance methods, assuming they haven't been compiled already.
95
138
  # We could in theory do this on app boot, at least in production environments.
96
- # Right now this just compiles the template the first time the component is rendered.
139
+ # Right now this just compiles the first time the component is rendered.
97
140
  def compile
98
141
  return if @compiled && ActionView::Base.cache_template_loading
99
142
 
100
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
101
- def call
102
- @output_buffer = ActionView::OutputBuffer.new
103
- #{compiled_template}
104
- end
105
- RUBY
143
+ validate_templates
144
+
145
+ templates.each do |template|
146
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
147
+ def #{call_method_name(template[:variant])}
148
+ @output_buffer = ActionView::OutputBuffer.new
149
+ #{compiled_template(template[:path])}
150
+ end
151
+ RUBY
152
+ end
106
153
 
107
154
  @compiled = true
108
155
  end
109
156
 
110
- private
157
+ def variants
158
+ templates.map { |template| template[:variant] }
159
+ end
111
160
 
112
- def compiled_template
113
- handler = ActionView::Template.handler_for_extension(File.extname(template_file_path).gsub(".", ""))
114
- template = File.read(template_file_path)
161
+ # we'll eventually want to update this to support other types
162
+ def type
163
+ "text/html"
164
+ end
115
165
 
116
- if handler.method(:call).parameters.length > 1
117
- handler.call(DummyTemplate.new, template)
118
- else
119
- handler.call(DummyTemplate.new(template))
120
- end
166
+ def identifier
167
+ ""
121
168
  end
122
169
 
123
- def template_file_path
124
- raise NotImplementedError.new("#{self} must implement #initialize.") unless self.instance_method(:initialize).owner == self
170
+ private
125
171
 
126
- filename = self.instance_method(:initialize).source_location[0]
127
- filename_without_extension = filename[0..-(File.extname(filename).length + 1)]
128
- sibling_template_files = Dir["#{filename_without_extension}.????.{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [filename]
172
+ def templates
173
+ @templates ||=
174
+ (Dir["#{source_location.sub(/#{File.extname(source_location)}$/, '')}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location]).each_with_object([]) do |path, memo|
175
+ memo << {
176
+ path: path,
177
+ variant: path.split(".").second.split("+")[1]&.to_sym,
178
+ handler: path.split(".").last
179
+ }
180
+ end
181
+ end
129
182
 
130
- if sibling_template_files.length > 1
131
- raise StandardError.new("More than one template found for #{self}. There can only be one sidecar template file per component.")
183
+ def validate_templates
184
+ if templates.empty?
185
+ raise NotImplementedError.new("Could not find a template file for #{self}.")
132
186
  end
133
187
 
134
- if sibling_template_files.length == 0
135
- raise NotImplementedError.new(
136
- "Could not find a template file for #{self}."
137
- )
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.")
138
190
  end
139
191
 
140
- sibling_template_files[0]
141
- end
142
- end
143
-
144
- class DummyTemplate
145
- attr_reader :source
192
+ variants.each_with_object(Hash.new(0)) { |variant, counts| counts[variant] += 1 }.each do |variant, count|
193
+ next unless count > 1
146
194
 
147
- def initialize(source = nil)
148
- @source = source
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
149
197
  end
150
198
 
151
- def identifier
152
- ""
153
- end
199
+ def compiled_template(file_path)
200
+ handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
201
+ template = File.read(file_path)
154
202
 
155
- # we'll eventually want to update this to support other types
156
- def type
157
- "text/html"
203
+ if handler.method(:call).parameters.length > 1
204
+ handler.call(self, template)
205
+ else # remove before upstreaming into Rails
206
+ handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
207
+ end
158
208
  end
159
209
  end
160
210
 
161
- private
162
-
163
- def request
164
- @request ||= controller.request
165
- end
166
-
167
- attr_reader :content, :view_context
211
+ ActiveSupport.run_load_hooks(:action_view_component, self)
168
212
  end
169
213
  end
170
214
  end
@@ -0,0 +1,27 @@
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
+ class ActionView::Base
8
+ module RenderMonkeyPatch
9
+ def render(options = {}, args = {}, &block)
10
+ if options.respond_to?(:render_in)
11
+ ActiveSupport::Deprecation.warn(
12
+ "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."
13
+ )
14
+
15
+ options.render_in(self, &block)
16
+ elsif options.is_a?(Class) && options < ActionView::Component::Base
17
+ options.new(args).render_in(self, &block)
18
+ elsif options.is_a?(Hash) && options.has_key?(:component)
19
+ options[:component].new(options[:locals]).render_in(self, &block)
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
25
+
26
+ prepend RenderMonkeyPatch
27
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/descendants_tracker"
5
+ require_relative "test_helpers"
6
+
7
+ 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
+
29
+ class Preview
30
+ extend ActiveSupport::DescendantsTracker
31
+ include ActionView::Component::TestHelpers
32
+
33
+ def render(component, *locals)
34
+ render_inline(component, *locals)
35
+ end
36
+
37
+ class << self
38
+ # Returns all component preview classes.
39
+ def all
40
+ load_previews if descendants.empty?
41
+ descendants
42
+ end
43
+
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 })
51
+ end
52
+
53
+ # Returns the component object class associated to the preview.
54
+ def component
55
+ self.name.sub(%r{Preview$}, "").constantize
56
+ end
57
+
58
+ # Returns all of the available examples for the component preview.
59
+ def examples
60
+ public_instance_methods(false).map(&:to_s).sort
61
+ end
62
+
63
+ # Returns +true+ if the example of the component preview exists.
64
+ def example_exists?(example)
65
+ examples.include?(example)
66
+ end
67
+
68
+ # Returns +true+ if the preview exists.
69
+ def exists?(preview)
70
+ all.any? { |p| p.preview_name == preview }
71
+ end
72
+
73
+ # Find a component preview by its underscored class name.
74
+ def find(preview)
75
+ all.find { |p| p.preview_name == preview }
76
+ end
77
+
78
+ # Returns the underscored name of the component preview without the suffix.
79
+ def preview_name
80
+ name.sub(/Preview$/, "").underscore
81
+ end
82
+
83
+ # Setter for layout name.
84
+ def layout(layout_name)
85
+ @layout = layout_name
86
+ end
87
+
88
+ private
89
+
90
+ def load_previews
91
+ if preview_path
92
+ Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
93
+ end
94
+ end
95
+
96
+ def preview_path
97
+ Base.preview_path
98
+ end
99
+
100
+ def show_previews
101
+ Base.show_previews
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "railties/lib/rails/components_controller"
4
+ require "railties/lib/rails/component_examples_controller"
5
+
6
+ module ActionView
7
+ module Component
8
+ class Railtie < Rails::Railtie # :nodoc:
9
+ config.action_view_component = ActiveSupport::OrderedOptions.new
10
+
11
+ # Disabled due to issues with ActionView::Component::Base not defining .logger
12
+ # initializer "action_view_component.logger" do
13
+ # ActiveSupport.on_load(:action_view_component) { self.logger ||= Rails.logger }
14
+ # end
15
+
16
+ initializer "action_view_component.set_configs" do |app|
17
+ options = app.config.action_view_component
18
+
19
+ options.show_previews = Rails.env.development? if options.show_previews.nil?
20
+
21
+ if options.show_previews
22
+ options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/components/previews" : nil
23
+ end
24
+
25
+ ActiveSupport.on_load(:action_view_component) do
26
+ options.each { |k, v| send("#{k}=", v) }
27
+ end
28
+ end
29
+
30
+ initializer "action_view_component.set_autoload_paths" do |app|
31
+ options = app.config.action_view_component
32
+
33
+ if options.show_previews && options.preview_path
34
+ ActiveSupport::Dependencies.autoload_paths << options.preview_path
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
+ # Disabled because `ActionView::Component::Base` doesn't implement `#action_methods`
45
+ # initializer "action_view_component.eager_load_actions" do
46
+ # ActiveSupport.on_load(:after_initialize) do
47
+ # ActionView::Component::Base.descendants.each(&:action_methods) if config.eager_load
48
+ # end
49
+ # end
50
+
51
+ config.after_initialize do |app|
52
+ options = app.config.action_view_component
53
+
54
+ if options.show_previews
55
+ app.routes.prepend do
56
+ get "/rails/components" => "rails/components#index", :internal => true
57
+ get "/rails/components/*path" => "rails/components#previews", :internal => true
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -4,7 +4,7 @@ module ActionView
4
4
  module Component
5
5
  module TestHelpers
6
6
  def render_inline(component, **args, &block)
7
- Nokogiri::HTML(controller.view_context.render(component, args, &block))
7
+ Nokogiri::HTML(controller.view_context.render(component, args, &block)).css("body > *")
8
8
  end
9
9
 
10
10
  def controller
@@ -22,6 +22,14 @@ module ActionView
22
22
 
23
23
  render_inline(component, args, &block)
24
24
  end
25
+
26
+ def with_variant(variant)
27
+ old_variants = controller.view_context.lookup_context.variants
28
+
29
+ controller.view_context.lookup_context.variants = variant
30
+ yield
31
+ controller.view_context.lookup_context.variants = old_variants
32
+ end
25
33
  end
26
34
  end
27
35
  end
@@ -4,8 +4,8 @@ module ActionView
4
4
  module Component
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 3
8
- PATCH = 5
7
+ MINOR = 5
8
+ PATCH = 2
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  end
@@ -0,0 +1,13 @@
1
+ Description:
2
+ ============
3
+ Creates a new component and test.
4
+ Pass the component name, either CamelCased or under_scored, and an optional list of attributes as arguments.
5
+
6
+ Example:
7
+ ========
8
+ bin/rails generate component Profile name age
9
+
10
+ creates a Profile component and test:
11
+ Component: app/components/profile_component.rb
12
+ Template: app/components/profile_component.html.erb
13
+ Test: test/components/profile_component_test.rb
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module Generators
5
+ class ComponentGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ argument :attributes, type: :array, default: [], banner: "attribute"
9
+ hook_for :test_framework
10
+ check_class_collision suffix: "Component"
11
+
12
+ def create_component_file
13
+ template "component.rb", File.join("app/components", "#{file_name}_component.rb")
14
+ end
15
+
16
+ def create_template_file
17
+ template "component.html.erb", File.join("app/components", "#{file_name}_component.html.erb")
18
+ end
19
+
20
+ private
21
+
22
+ def file_name
23
+ @_file_name ||= super.sub(/_component\z/i, "")
24
+ end
25
+
26
+ def requires_content?
27
+ return @requires_content if @asked
28
+
29
+ @asked = true
30
+ @requires_content = ask("Would you like #{class_name} to require content? (Y/n)").downcase == "y"
31
+ end
32
+
33
+ def parent_class
34
+ defined?(ApplicationComponent) ? "ApplicationComponent" : "ActionView::Component::Base"
35
+ end
36
+
37
+ def initialize_signature
38
+ if attributes.present?
39
+ attributes.map { |attr| "#{attr.name}:" }.join(", ")
40
+ else
41
+ "*"
42
+ end
43
+ end
44
+
45
+ def initialize_body
46
+ attributes.map { |attr| "@#{attr.name} = #{attr.name}" }.join("\n ")
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,5 @@
1
+ <%- if requires_content? -%>
2
+ <%= "<%= content %%>" %>
3
+ <%- else -%>
4
+ <div>Add <%= class_name %> template here</div>
5
+ <%- end -%>
@@ -0,0 +1,9 @@
1
+ class <%= class_name %>Component < <%= parent_class %>
2
+ <%- if requires_content? -%>
3
+ validates :content, presence: true
4
+ <%- end -%>
5
+
6
+ def initialize(<%= initialize_signature %>)
7
+ <%= initialize_body %>
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestUnit
4
+ module Generators
5
+ class ComponentGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+ check_class_collision suffix: "ComponentTest"
8
+
9
+ def create_test_file
10
+ template "component_test.rb", File.join("test/components", "#{file_name}_component_test.rb")
11
+ end
12
+
13
+ private
14
+
15
+ def file_name
16
+ @_file_name ||= super.sub(/_component\z/i, "")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ require "test_helper"
2
+
3
+ class <%= class_name %>ComponentTest < ActiveSupport::TestCase
4
+ include ActionView::Component::TestHelpers
5
+
6
+ test "component renders something useful" do
7
+ # assert_equal(
8
+ # %(<span title="my title">Hello, components!</span>),
9
+ # render_inline(<%= class_name %>, attr: "value") { "Hello, components!" }.css("span").to_html
10
+ # )
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ autoload :ComponentsController
5
+ autoload :ComponentExamplesController
6
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/application_controller"
4
+ load "config/application.rb" unless Rails.root
5
+
6
+ class Rails::ComponentExamplesController < ActionController::Base # :nodoc:
7
+ prepend_view_path File.expand_path("templates/rails", __dir__)
8
+ append_view_path Rails.root.join("app/views")
9
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/application_controller"
4
+
5
+ class Rails::ComponentsController < Rails::ApplicationController # :nodoc:
6
+ prepend_view_path File.expand_path("templates/rails", __dir__)
7
+
8
+ around_action :set_locale, only: :previews
9
+ before_action :find_preview, only: :previews
10
+ before_action :require_local!, unless: :show_previews?
11
+
12
+ if respond_to?(:content_security_policy)
13
+ content_security_policy(false)
14
+ end
15
+
16
+ def index
17
+ @previews = ActionView::Component::Preview.all
18
+ @page_title = "Component Previews"
19
+ render template: "components/index"
20
+ end
21
+
22
+ def previews
23
+ if params[:path] == @preview.preview_name
24
+ @page_title = "Component Previews for #{@preview.preview_name}"
25
+ render template: "components/previews"
26
+ else
27
+ @example_name = File.basename(params[:path])
28
+ render template: "components/preview", layout: false
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def show_previews? # :doc:
35
+ ActionView::Component::Base.show_previews
36
+ end
37
+
38
+ def find_preview # :doc:
39
+ candidates = []
40
+ params[:path].to_s.scan(%r{/|$}) { candidates << $` }
41
+ preview = candidates.detect { |candidate| ActionView::Component::Preview.exists?(candidate) }
42
+
43
+ if preview
44
+ @preview = ActionView::Component::Preview.find(preview)
45
+ else
46
+ raise AbstractController::ActionNotFound, "Component preview '#{params[:path]}' not found"
47
+ end
48
+ end
49
+
50
+ def set_locale
51
+ I18n.with_locale(params[:locale] || I18n.default_locale) do
52
+ yield
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,8 @@
1
+ <% @previews.each do |preview| %>
2
+ <h3><%= link_to preview.preview_name.titleize, "/rails/components/#{preview.preview_name}" %></h3>
3
+ <ul>
4
+ <% preview.examples.each do |preview_example| %>
5
+ <li><%= link_to preview_example, "/rails/components/#{preview.preview_name}/#{preview_example}" %></li>
6
+ <% end %>
7
+ </ul>
8
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= raw @preview.call(@example_name) %>
@@ -0,0 +1,6 @@
1
+ <h3><%= @preview.preview_name.titleize %></h3>
2
+ <ul>
3
+ <% @preview.examples.each do |example| %>
4
+ <li><%= link_to example, "/rails/components/#{@preview.preview_name}/#{example}" %></li>
5
+ <% end %>
6
+ </ul>
@@ -0,0 +1 @@
1
+ <%= raw @example %>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionview-component
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.5
4
+ version: 1.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-17 00:00:00.000000000 Z
11
+ date: 2019-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -84,16 +84,16 @@ dependencies:
84
84
  name: rubocop
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - '='
88
88
  - !ruby/object:Gem::Version
89
- version: '0.59'
89
+ version: '0.74'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - '='
95
95
  - !ruby/object:Gem::Version
96
- version: '0.59'
96
+ version: '0.74'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: rubocop-github
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -127,9 +127,26 @@ files:
127
127
  - README.md
128
128
  - Rakefile
129
129
  - actionview-component.gemspec
130
+ - lib/action_view/component.rb
130
131
  - lib/action_view/component/base.rb
132
+ - lib/action_view/component/monkey_patch.rb
133
+ - lib/action_view/component/preview.rb
134
+ - lib/action_view/component/railtie.rb
131
135
  - lib/action_view/component/test_helpers.rb
132
136
  - lib/action_view/component/version.rb
137
+ - lib/rails/generators/component/USAGE
138
+ - lib/rails/generators/component/component_generator.rb
139
+ - lib/rails/generators/component/templates/component.html.erb.tt
140
+ - lib/rails/generators/component/templates/component.rb.tt
141
+ - lib/rails/generators/test_unit/component_generator.rb
142
+ - lib/rails/generators/test_unit/templates/component_test.rb.tt
143
+ - lib/railties/lib/rails.rb
144
+ - lib/railties/lib/rails/component_examples_controller.rb
145
+ - lib/railties/lib/rails/components_controller.rb
146
+ - lib/railties/lib/rails/templates/rails/components/index.html.erb
147
+ - lib/railties/lib/rails/templates/rails/components/preview.html.erb
148
+ - lib/railties/lib/rails/templates/rails/components/previews.html.erb
149
+ - lib/railties/lib/rails/templates/rails/examples/show.html.erb
133
150
  - script/bootstrap
134
151
  - script/console
135
152
  - script/install