actionview-component 1.3.4 → 1.5.1

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: 03a3c04aa78f3fcc16c350b49955870f60e712ad0383eb924e7053d7208431d0
4
- data.tar.gz: 299741730bcfe4eb6640e5899e3973d4fd303dc28aadc79103eb72bdcd2382ec
3
+ metadata.gz: 84c85d88f1ceefc7d3a0c97affcaa2465120a4513f0f445863a068715cb55f88
4
+ data.tar.gz: ad3ae8d04886ee3c22f79b73998e6d3d0156f9ef8e84cd64468139a6bd9b282a
5
5
  SHA512:
6
- metadata.gz: '01091f9da0436fcbfcbdfd602bcf7595c9c061738f49b9fb70305474218b6b928382daed58c4a3ebc1d08991815ecc965cc6a19b8c4384c35338d672ee5b456e'
7
- data.tar.gz: 32fae5a27cb1f1f939ce5b59947b3785f1b4ee73a8e9b3b1a95eb1351800aa005518b61b6f24d8570790c1e45d128bf87ab28e4f14cecac1479d33a4ed30a524
6
+ metadata.gz: fd41865d44d1b450f9772ff3da1dac80b5873c809bb4cc598c93be37e15c074fb5fdbf5186350af13a8da65d7c8502dcccfd069e7e71ef86e4b87b19891f4bcd
7
+ data.tar.gz: c44f9d1129c3597553abf20510ec50a9f490b707bc121fd0bee8f7046ae257a6cf0a6517e91bdfa14391e1028b88565f14eda017224c79577700741ef85560c1
@@ -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
@@ -1,3 +1,75 @@
1
+ # v1.5.1
2
+
3
+ * Update railties class to work with Rails 6.
4
+
5
+ *Juan Manuel Ramallo*
6
+
7
+ # v1.5.0
8
+
9
+ Note: `actionview-component` is now loaded by requiring `actionview/component`, not `actionview/component/base`.
10
+
11
+ * Fix issue with generating component method signatures.
12
+
13
+ *Ryan Workman, Dylan Clark*
14
+
15
+ * Create component generator.
16
+
17
+ *Vinicius Stock*
18
+
19
+ * Add helpers proxy.
20
+
21
+ *Kasper Meyer*
22
+
23
+ * Introduce ActionView::Component::Previews.
24
+
25
+ *Juan Manuel Ramallo*
26
+
27
+ # v1.4.0
28
+
29
+ * Fix bug where components broke in application paths with periods.
30
+
31
+ *Anton, Joel Hawksley*
32
+
33
+ * Add support for `cache_if` in component templates.
34
+
35
+ *Aaron Patterson, Joel Hawksley*
36
+
37
+ * Add support for variants.
38
+
39
+ *Juan Manuel Ramallo*
40
+
41
+ * Fix bug in virtual path lookup.
42
+
43
+ *Juan Manuel Ramallo*
44
+
45
+ * Preselect the rendered component in render_inline.
46
+
47
+ *Elia Schito*
48
+
49
+ # v1.3.6
50
+
51
+ * Allow template file names without format.
52
+
53
+ *Joel Hawksley*
54
+
55
+ * Add support for translations.
56
+
57
+ *Juan Manuel Ramallo*
58
+
59
+ # v1.3.5
60
+
61
+ * Re-expose `controller` method.
62
+
63
+ *Michael Emhofer, Joel Hawksley*
64
+
65
+ * Gem version numbers are now accessible through `ActionView::Component::VERSION`
66
+
67
+ *Richard Macklin*
68
+
69
+ * Fix typo in README
70
+
71
+ *ars moriendi*
72
+
1
73
  # v1.3.4
2
74
 
3
75
  * Template errors surface correct file and line number.
@@ -32,7 +32,8 @@ Here are a few things you can do that will increase the likelihood of your pull
32
32
  If you are the current maintainer of this gem:
33
33
 
34
34
  1. Create a branch for the release: `git checkout -b release-vxx.xx.xx`
35
- 1. Bump gem version in `actionview-component.gemspec`.
35
+ 1. Bump gem version in `lib/action_view/component/version.rb`.
36
+ 1. Add version heading/entries to `CHANGELOG.md`.
36
37
  1. Make sure your local dependencies are up to date: `bundle`
37
38
  1. Ensure that tests are green: `bundle exec rake`
38
39
  1. Build a test gem `GEM_VERSION=$(git describe --tags 2>/dev/null | sed 's/-/./g' | sed 's/v//') gem build actionview-component.gemspec`
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- actionview-component (1.3.4)
4
+ actionview-component (1.5.1)
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:
@@ -149,7 +165,7 @@ Components can be rendered via:
149
165
 
150
166
  The following syntax has been deprecated and will be removed in v2.0.0:
151
167
 
152
- `render(TestComponent.new(foo: :bar)`
168
+ `render(TestComponent.new(foo: :bar))`
153
169
 
154
170
  #### Error case
155
171
 
@@ -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?
@@ -3,10 +3,11 @@
3
3
 
4
4
  lib = File.expand_path("../lib", __FILE__)
5
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require "action_view/component/version"
6
7
 
7
8
  Gem::Specification.new do |spec|
8
9
  spec.name = "actionview-component"
9
- spec.version = "1.3.4"
10
+ spec.version = ActionView::Component::VERSION::STRING
10
11
  spec.authors = ["GitHub Open Source"]
11
12
  spec.email = ["opensource+actionview-component@github.com"]
12
13
 
@@ -38,6 +39,6 @@ Gem::Specification.new do |spec|
38
39
  spec.add_development_dependency "minitest", "= 5.1.0"
39
40
  spec.add_development_dependency "haml", "~> 5"
40
41
  spec.add_development_dependency "slim", "~> 4.0"
41
- spec.add_development_dependency "rubocop", "~> 0.59"
42
+ spec.add_development_dependency "rubocop", "= 0.74"
42
43
  spec.add_development_dependency "rubocop-github", "~> 0.13.0"
43
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
@@ -80,91 +70,145 @@ module ActionView
80
70
  end
81
71
  end
82
72
 
83
- class << self
84
- def inherited(child)
85
- child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
73
+ def controller
74
+ @controller ||= view_context.controller
75
+ end
86
76
 
87
- super
88
- end
77
+ # Provides a proxy to access helper methods through
78
+ def helpers
79
+ @helpers ||= view_context
80
+ end
89
81
 
90
- # Compile template to #call instance method, assuming it hasn't been compiled already.
91
- # We could in theory do this on app boot, at least in production environments.
92
- # Right now this just compiles the template the first time the component is rendered.
93
- def compile
94
- return if @compiled && ActionView::Base.cache_template_loading
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
95
86
 
96
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
97
- def call
98
- @output_buffer = ActionView::OutputBuffer.new
99
- #{compiled_template}
100
- end
101
- RUBY
87
+ def view_cache_dependencies
88
+ []
89
+ end
102
90
 
103
- @compiled = true
104
- end
91
+ def format # :nodoc:
92
+ @variant
93
+ end
105
94
 
106
- private
95
+ private
107
96
 
108
- def compiled_template
109
- handler = ActionView::Template.handler_for_extension(File.extname(template_file_path).gsub(".", ""))
110
- template = File.read(template_file_path)
97
+ def variant_exists
98
+ return if self.class.variants.include?(@variant) || @variant.nil?
111
99
 
112
- if handler.method(:call).parameters.length > 1
113
- handler.call(DummyTemplate.new, template)
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
+
109
+ class << self
110
+ def inherited(child)
111
+ child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
112
+
113
+ super
114
+ end
115
+
116
+ def call_method_name(variant)
117
+ if variant.present?
118
+ "call_#{variant}"
114
119
  else
115
- handler.call(DummyTemplate.new(template))
120
+ "call"
116
121
  end
117
122
  end
118
123
 
119
- def template_file_path
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.
120
132
  raise NotImplementedError.new("#{self} must implement #initialize.") unless self.instance_method(:initialize).owner == self
121
133
 
122
- filename = self.instance_method(:initialize).source_location[0]
123
- filename_without_extension = filename[0..-(File.extname(filename).length + 1)]
124
- sibling_template_files = Dir["#{filename_without_extension}.????.{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [filename]
134
+ instance_method(:initialize).source_location[0]
135
+ end
136
+
137
+ # Compile templates to instance methods, assuming they haven't been compiled already.
138
+ # We could in theory do this on app boot, at least in production environments.
139
+ # Right now this just compiles the first time the component is rendered.
140
+ def compile
141
+ return if @compiled && ActionView::Base.cache_template_loading
125
142
 
126
- if sibling_template_files.length > 1
127
- raise StandardError.new("More than one template found for #{self}. There can only be one sidecar template file per component.")
128
- end
143
+ validate_templates
129
144
 
130
- if sibling_template_files.length == 0
131
- raise NotImplementedError.new(
132
- "Could not find a template file for #{self}."
133
- )
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
134
152
  end
135
153
 
136
- sibling_template_files[0]
154
+ @compiled = true
137
155
  end
138
- end
139
156
 
140
- class DummyTemplate
141
- attr_reader :source
157
+ def variants
158
+ templates.map { |template| template[:variant] }
159
+ end
142
160
 
143
- def initialize(source = nil)
144
- @source = source
161
+ # we'll eventually want to update this to support other types
162
+ def type
163
+ "text/html"
145
164
  end
146
165
 
147
166
  def identifier
148
167
  ""
149
168
  end
150
169
 
151
- # we'll eventually want to update this to support other types
152
- def type
153
- "text/html"
170
+ private
171
+
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
154
181
  end
155
- end
156
182
 
157
- private
183
+ def validate_templates
184
+ if templates.empty?
185
+ raise NotImplementedError.new("Could not find a template file for #{self}.")
186
+ end
158
187
 
159
- def controller
160
- @controller ||= view_context.controller
161
- end
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
162
191
 
163
- def request
164
- @request ||= controller.request
192
+ variants.each_with_object(Hash.new(0)) { |variant, counts| counts[variant] += 1 }.each do |variant, count|
193
+ next unless count > 1
194
+
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
197
+ end
198
+
199
+ def compiled_template(file_path)
200
+ handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
201
+ template = File.read(file_path)
202
+
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
208
+ end
165
209
  end
166
210
 
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,62 @@
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
+ initializer "action_view_component.eager_load_actions" do
45
+ ActiveSupport.on_load(:after_initialize) do
46
+ ActionView::Component::Base.descendants.each(&:action_methods) if config.eager_load
47
+ end
48
+ end
49
+
50
+ config.after_initialize do |app|
51
+ options = app.config.action_view_component
52
+
53
+ if options.show_previews
54
+ app.routes.prepend do
55
+ get "/rails/components" => "rails/components#index", :internal => true
56
+ get "/rails/components/*path" => "rails/components#previews", :internal => true
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ 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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Component
5
+ module VERSION
6
+ MAJOR = 1
7
+ MINOR = 5
8
+ PATCH = 1
9
+
10
+ STRING = [MAJOR, MINOR, PATCH].join(".")
11
+ end
12
+ end
13
+ 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>
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.4
4
+ version: 1.5.1
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-07 00:00:00.000000000 Z
11
+ date: 2019-11-21 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,8 +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
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
132
150
  - script/bootstrap
133
151
  - script/console
134
152
  - script/install