actionview-component 1.7.0 → 1.17.0

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