actionview-component 1.7.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE +5 -8
  3. data/.github/PULL_REQUEST_TEMPLATE +2 -4
  4. data/.github/workflows/ruby_on_rails.yml +6 -2
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.md +150 -0
  8. data/CONTRIBUTING.md +10 -19
  9. data/Gemfile.lock +20 -5
  10. data/README.md +204 -248
  11. data/actionview-component.gemspec +10 -8
  12. data/{lib/railties/lib → app/controllers}/rails/components_controller.rb +18 -8
  13. data/docs/case-studies/jellyswitch.md +76 -0
  14. data/lib/action_view/component.rb +1 -20
  15. data/lib/action_view/component/base.rb +2 -246
  16. data/lib/action_view/component/preview.rb +1 -80
  17. data/lib/action_view/component/railtie.rb +1 -64
  18. data/lib/action_view/component/test_case.rb +1 -3
  19. data/lib/action_view/component/test_helpers.rb +1 -19
  20. data/lib/rails/generators/component/component_generator.rb +6 -12
  21. data/lib/rails/generators/component/templates/component.rb.tt +0 -4
  22. data/lib/rails/generators/erb/component_generator.rb +21 -0
  23. data/lib/rails/generators/erb/templates/component.html.erb.tt +1 -0
  24. data/lib/rails/generators/haml/component_generator.rb +21 -0
  25. data/lib/rails/generators/haml/templates/component.html.haml.tt +1 -0
  26. data/lib/rails/generators/rspec/component_generator.rb +1 -1
  27. data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -1
  28. data/lib/rails/generators/slim/component_generator.rb +21 -0
  29. data/lib/rails/generators/slim/templates/component.html.slim.tt +1 -0
  30. data/lib/rails/generators/test_unit/component_generator.rb +1 -1
  31. data/lib/rails/generators/test_unit/templates/component_test.rb.tt +3 -3
  32. data/lib/railties/lib/rails.rb +0 -1
  33. data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +1 -1
  34. data/lib/view_component.rb +30 -0
  35. data/lib/view_component/base.rb +279 -0
  36. data/lib/view_component/conversion.rb +9 -0
  37. data/lib/view_component/engine.rb +65 -0
  38. data/lib/view_component/preview.rb +78 -0
  39. data/lib/view_component/previewable.rb +25 -0
  40. data/lib/view_component/render_monkey_patch.rb +31 -0
  41. data/lib/view_component/rendering_monkey_patch.rb +13 -0
  42. data/lib/view_component/template_error.rb +9 -0
  43. data/lib/view_component/test_case.rb +9 -0
  44. data/lib/view_component/test_helpers.rb +39 -0
  45. data/lib/view_component/version.rb +11 -0
  46. data/script/console +1 -1
  47. metadata +48 -21
  48. data/lib/action_view/component/conversion.rb +0 -11
  49. data/lib/action_view/component/previewable.rb +0 -27
  50. data/lib/action_view/component/render_monkey_patch.rb +0 -29
  51. data/lib/action_view/component/template_error.rb +0 -11
  52. data/lib/action_view/component/version.rb +0 -13
  53. data/lib/rails/generators/component/templates/component.html.erb.tt +0 -5
  54. data/lib/railties/lib/rails/component_examples_controller.rb +0 -9
  55. data/lib/railties/lib/rails/templates/rails/examples/show.html.erb +0 -1
@@ -3,17 +3,16 @@
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
+ require "view_component/version"
7
7
 
8
8
  Gem::Specification.new do |spec|
9
9
  spec.name = "actionview-component"
10
- spec.version = ActionView::Component::VERSION::STRING
10
+ spec.version = ViewComponent::VERSION::STRING
11
11
  spec.authors = ["GitHub Open Source"]
12
- spec.email = ["opensource+actionview-component@github.com"]
12
+ spec.email = ["opensource+view_component@github.com"]
13
13
 
14
- spec.summary = %q{View components for Rails}
15
- spec.description = %q{View components for Rails, intended for upstreaming in Rails 6.1}
16
- spec.homepage = "https://github.com/github/actionview-component"
14
+ spec.summary = %q{MOVED to view_component.}
15
+ spec.homepage = "https://github.com/github/view_component"
17
16
  spec.license = "MIT"
18
17
 
19
18
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
@@ -34,12 +33,15 @@ Gem::Specification.new do |spec|
34
33
 
35
34
  spec.required_ruby_version = ">= 2.3.0"
36
35
 
37
- spec.add_development_dependency "bundler", ">= 1.14"
38
- spec.add_development_dependency "rake", "~> 10.0"
36
+ spec.add_runtime_dependency "capybara", "~> 3"
37
+ spec.add_development_dependency "bundler", "~> 1.14"
38
+ spec.add_development_dependency "rake", "~> 13.0"
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
42
  spec.add_development_dependency "better_html", "~> 1"
43
43
  spec.add_development_dependency "rubocop", "= 0.74"
44
44
  spec.add_development_dependency "rubocop-github", "~> 0.13.0"
45
+
46
+ spec.post_install_message = "WARNING: actionview-component has been renamed to view_component, and will no longer be published in this namespace. Please update your Gemfile to use view_component."
45
47
  end
@@ -3,7 +3,8 @@
3
3
  require "rails/application_controller"
4
4
 
5
5
  class Rails::ComponentsController < Rails::ApplicationController # :nodoc:
6
- prepend_view_path File.expand_path("templates/rails", __dir__)
6
+ prepend_view_path File.expand_path("../../../lib/railties/lib/rails/templates/rails", __dir__)
7
+ prepend_view_path "#{Rails.root}/app/views/" if defined?(Rails.root)
7
8
 
8
9
  around_action :set_locale, only: :previews
9
10
  before_action :find_preview, only: :previews
@@ -14,34 +15,43 @@ class Rails::ComponentsController < Rails::ApplicationController # :nodoc:
14
15
  end
15
16
 
16
17
  def index
17
- @previews = ActionView::Component::Preview.all
18
+ @previews = ViewComponent::Preview.all
18
19
  @page_title = "Component Previews"
19
- render template: "components/index"
20
+ # rubocop:disable GitHub/RailsControllerRenderPathsExist
21
+ render "components/index"
22
+ # rubocop:enable GitHub/RailsControllerRenderPathsExist
20
23
  end
21
24
 
22
25
  def previews
23
26
  if params[:path] == @preview.preview_name
24
27
  @page_title = "Component Previews for #{@preview.preview_name}"
25
- render template: "components/previews"
28
+ # rubocop:disable GitHub/RailsControllerRenderPathsExist
29
+ render "components/previews"
30
+ # rubocop:enable GitHub/RailsControllerRenderPathsExist
26
31
  else
27
32
  @example_name = File.basename(params[:path])
28
- render template: "components/preview", layout: false
33
+ @render_args = @preview.render_args(@example_name)
34
+ layout = @render_args[:layout]
35
+ opts = layout.nil? ? {} : { layout: layout }
36
+ # rubocop:disable GitHub/RailsControllerRenderPathsExist
37
+ render "components/preview", **opts
38
+ # rubocop:enable GitHub/RailsControllerRenderPathsExist
29
39
  end
30
40
  end
31
41
 
32
42
  private
33
43
 
34
44
  def show_previews? # :doc:
35
- ActionView::Component::Base.show_previews
45
+ ViewComponent::Base.show_previews
36
46
  end
37
47
 
38
48
  def find_preview # :doc:
39
49
  candidates = []
40
50
  params[:path].to_s.scan(%r{/|$}) { candidates << $` }
41
- preview = candidates.detect { |candidate| ActionView::Component::Preview.exists?(candidate) }
51
+ preview = candidates.detect { |candidate| ViewComponent::Preview.exists?(candidate) }
42
52
 
43
53
  if preview
44
- @preview = ActionView::Component::Preview.find(preview)
54
+ @preview = ViewComponent::Preview.find(preview)
45
55
  else
46
56
  raise AbstractController::ActionNotFound, "Component preview '#{params[:path]}' not found"
47
57
  end
@@ -0,0 +1,76 @@
1
+ Name: Dave Paola
2
+
3
+ Github Handle: [@dpaola2](https://github.com/dpaola2)
4
+
5
+ [Jellyswitch](https://www.jellyswitch.com) is a coworking space management platform
6
+
7
+ In response to [this tweet](https://twitter.com/joelhawksley/status/1232674647327547394):
8
+
9
+ I recently began refactoring many of my partials into components. Along the way I discovered an interesting use case, which was to componentize the various bootstrap components I was already using.
10
+
11
+ Some examples:
12
+
13
+ - I've componentized the specific layout that I've designed using the grid system. I have defined a `FullWidthLayout` component that wraps its contents in the correct classes to give my layout a good design on both mobile and desktop. (On desktop, there is a sidebar, and on mobile, the sidebar floats on top in a collapsed fashion.)
14
+ - [Modals](https://getbootstrap.com/docs/4.4/components/modal/) are now componentized and accept arguments. I had them as partials before, but having ruby classes is an upgrade.
15
+ - [Breadcrumbs](https://getbootstrap.com/docs/4.4/components/breadcrumb/) same as above.
16
+
17
+ Here's one that I use a LOT: `OnOffSwitch`:
18
+
19
+ ```ruby
20
+ class OnOffSwitch < ApplicationComponent
21
+ def initialize(predicate:, path:, disabled: false, label: nil)
22
+ @predicate = predicate
23
+ @path = path
24
+ @disabled = disabled
25
+ @label = label
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :predicate, :path, :disabled, :label
31
+
32
+ def icon_class
33
+ if predicate
34
+ "fas fa-toggle-on"
35
+ else
36
+ "fas fa-toggle-off"
37
+ end
38
+ end
39
+ end
40
+ ```
41
+
42
+ ```erb
43
+ <div class="d-flex align-items-center">
44
+ <% if !disabled %>
45
+ <%= link_to path, class: "text-body", remote: true do %>
46
+ <span style="font-size: 20pt">
47
+ <% if predicate %>
48
+ <span class="text-success">
49
+ <i class="<%= icon_class %>"></i>
50
+ </span>
51
+ <% else %>
52
+ <span class="text-danger">
53
+ <i class="<%= icon_class %>"></i>
54
+ </span>
55
+ <% end %>
56
+ </span>
57
+ <% end %>
58
+ <% else %>
59
+ <span style="font-size: 20pt">
60
+ <span class="text-muted">
61
+ <i class="<%= icon_class %>"></i>
62
+ </span>
63
+ </span>
64
+ <% end %>
65
+ &nbsp;
66
+ <%= content %>
67
+ </div>
68
+ ```
69
+
70
+ Here is an example of how this looks:
71
+
72
+ <img width="653" alt="Screenshot 2020-02-26 08 34 07" src="https://user-images.githubusercontent.com/150509/75365920-cbfb9500-5872-11ea-8234-f1343629a462.png">
73
+
74
+ I have found that refactoring complex views is made easier and faster by first putting them into a component, extracting the conditionals and other logic into private methods and proceeding from there. And I wind up with a very nice set of well-factored components and sub-components, with argument lists and validations and so on. I think the rails community is really going to benefit from this library, and I'm hugely appreciative of y'all's efforts on it!
75
+
76
+ I plan to release an early version of the bootstrap components we've developed at some point in the near future. I would love to collaborate and learn the most appropriate way to structure that contribution.
@@ -1,23 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_model"
4
- require "action_view"
5
- require "active_support/dependencies/autoload"
6
- require "action_view/component/railtie"
7
-
8
- module ActionView
9
- module Component
10
- extend ActiveSupport::Autoload
11
-
12
- autoload :Base
13
- autoload :Conversion
14
- autoload :Preview
15
- autoload :Previewable
16
- autoload :TestHelpers
17
- autoload :TestCase
18
- autoload :RenderMonkeyPatch
19
- autoload :TemplateError
20
- end
21
- end
22
-
23
- ActiveModel::Conversion.include ActionView::Component::Conversion
4
+ require "view_component"
@@ -1,257 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/configurable"
4
-
5
3
  module ActionView
6
4
  module Component
7
- class Base < ActionView::Base
5
+ class Base < ViewComponent::Base
8
6
  include ActiveModel::Validations
9
- include ActiveSupport::Configurable
10
- include ActionView::Component::Previewable
11
-
12
- delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
13
-
14
- class_attribute :content_areas, default: []
15
- self.content_areas = [] # default doesn't work until Rails 5.2
16
-
17
- # Entrypoint for rendering components. Called by ActionView::Base#render.
18
- #
19
- # view_context: ActionView context from calling view
20
- # args(hash): params to be passed to component being rendered
21
- # block: optional block to be captured within the view context
22
- #
23
- # returns HTML that has been escaped by the respective template handler
24
- #
25
- # Example subclass:
26
- #
27
- # app/components/my_component.rb:
28
- # class MyComponent < ActionView::Component::Base
29
- # def initialize(title:)
30
- # @title = title
31
- # end
32
- # end
33
- #
34
- # app/components/my_component.html.erb
35
- # <span title="<%= @title %>">Hello, <%= content %>!</span>
36
- #
37
- # In use:
38
- # <%= render MyComponent, title: "greeting" do %>world<% end %>
39
- # returns:
40
- # <span title="greeting">Hello, world!</span>
41
- #
42
- def render_in(view_context, *args, &block)
43
- self.class.compile!
44
- @view_context = view_context
45
- @view_renderer ||= view_context.view_renderer
46
- @lookup_context ||= view_context.lookup_context
47
- @view_flow ||= view_context.view_flow
48
- @virtual_path ||= virtual_path
49
- @variant = @lookup_context.variants.first
50
- old_current_template = @current_template
51
- @current_template = self
52
-
53
- @content = view_context.capture(self, &block) if block_given?
54
7
 
8
+ def before_render_check
55
9
  validate!
56
-
57
- send(self.class.call_method_name(@variant))
58
- ensure
59
- @current_template = old_current_template
60
- end
61
-
62
- def initialize(*); end
63
-
64
- def render(options = {}, args = {}, &block)
65
- if options.is_a?(String) || (options.is_a?(Hash) && options.has_key?(:partial))
66
- view_context.render(options, args, &block)
67
- else
68
- super
69
- end
70
- end
71
-
72
- def controller
73
- @controller ||= view_context.controller
74
10
  end
75
-
76
- # Provides a proxy to access helper methods through
77
- def helpers
78
- @helpers ||= view_context
79
- end
80
-
81
- # Removes the first part of the path and the extension.
82
- def virtual_path
83
- self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
84
- end
85
-
86
- def view_cache_dependencies
87
- []
88
- end
89
-
90
- def format # :nodoc:
91
- @variant
92
- end
93
-
94
- def with(area, content = nil, &block)
95
- unless content_areas.include?(area)
96
- raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
97
- end
98
-
99
- if block_given?
100
- content = view_context.capture(&block)
101
- end
102
-
103
- instance_variable_set("@#{area}".to_sym, content)
104
- nil
105
- end
106
-
107
- private
108
-
109
- def request
110
- @request ||= controller.request
111
- end
112
-
113
- attr_reader :content, :view_context
114
-
115
- class << self
116
- def inherited(child)
117
- child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
118
-
119
- super
120
- end
121
-
122
- def call_method_name(variant)
123
- if variant.present? && variants.include?(variant)
124
- "call_#{variant}"
125
- else
126
- "call"
127
- end
128
- end
129
-
130
- def source_location
131
- @source_location ||=
132
- begin
133
- # Require #initialize to be defined so that we can use
134
- # method#source_location to look up the file name
135
- # of the component.
136
- #
137
- # If we were able to only support Ruby 2.7+,
138
- # We could just use Module#const_source_location,
139
- # rendering this unnecessary.
140
- #
141
- initialize_method = instance_method(:initialize)
142
- initialize_method.source_location[0] if initialize_method.owner == self
143
- end
144
- end
145
-
146
- def compiled?
147
- @compiled && ActionView::Base.cache_template_loading
148
- end
149
-
150
- def compile!
151
- compile(validate: true)
152
- end
153
-
154
- # Compile templates to instance methods, assuming they haven't been compiled already.
155
- # We could in theory do this on app boot, at least in production environments.
156
- # Right now this just compiles the first time the component is rendered.
157
- def compile(validate: false)
158
- return if compiled?
159
-
160
- if template_errors.present?
161
- raise ActionView::Component::TemplateError.new(template_errors) if validate
162
- return false
163
- end
164
-
165
- templates.each do |template|
166
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
167
- def #{call_method_name(template[:variant])}
168
- @output_buffer = ActionView::OutputBuffer.new
169
- #{compiled_template(template[:path])}
170
- end
171
- RUBY
172
- end
173
-
174
- @compiled = true
175
- end
176
-
177
- def variants
178
- templates.map { |template| template[:variant] }
179
- end
180
-
181
- # we'll eventually want to update this to support other types
182
- def type
183
- "text/html"
184
- end
185
-
186
- def identifier
187
- source_location
188
- end
189
-
190
- def with_content_areas(*areas)
191
- if areas.include?(:content)
192
- raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
193
- end
194
- attr_reader *areas
195
- self.content_areas = areas
196
- end
197
-
198
- private
199
-
200
- def matching_views_in_source_location
201
- return [] unless source_location
202
- (Dir["#{source_location.sub(/#{File.extname(source_location)}$/, '')}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location])
203
- end
204
-
205
- def templates
206
- @templates ||=
207
- matching_views_in_source_location.each_with_object([]) do |path, memo|
208
- pieces = File.basename(path).split(".")
209
-
210
- memo << {
211
- path: path,
212
- variant: pieces.second.split("+").second&.to_sym,
213
- handler: pieces.last
214
- }
215
- end
216
- end
217
-
218
- def template_errors
219
- @template_errors ||=
220
- begin
221
- errors = []
222
- errors << "#{self} must implement #initialize." if source_location.nil?
223
- errors << "Could not find a template file for #{self}." if templates.empty?
224
-
225
- if templates.count { |template| template[:variant].nil? } > 1
226
- errors << "More than one template found for #{self}. There can only be one default template file per component."
227
- end
228
-
229
- invalid_variants = templates
230
- .group_by { |template| template[:variant] }
231
- .map { |variant, grouped| variant if grouped.length > 1 }
232
- .compact
233
- .sort
234
-
235
- unless invalid_variants.empty?
236
- errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be one template file per variant."
237
- end
238
- errors
239
- end
240
- end
241
-
242
- def compiled_template(file_path)
243
- handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
244
- template = File.read(file_path)
245
-
246
- if handler.method(:call).parameters.length > 1
247
- handler.call(self, template)
248
- else # remove before upstreaming into Rails
249
- handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
250
- end
251
- end
252
- end
253
-
254
- ActiveSupport.run_load_hooks(:action_view_component, self)
255
11
  end
256
12
  end
257
13
  end