actionview-component 1.3.4 → 1.5.1

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