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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE +5 -8
- data/.github/PULL_REQUEST_TEMPLATE +2 -4
- data/.github/workflows/ruby_on_rails.yml +6 -2
- data/.gitignore +1 -0
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +150 -0
- data/CONTRIBUTING.md +10 -19
- data/Gemfile.lock +20 -5
- data/README.md +204 -248
- data/actionview-component.gemspec +10 -8
- data/{lib/railties/lib → app/controllers}/rails/components_controller.rb +18 -8
- data/docs/case-studies/jellyswitch.md +76 -0
- data/lib/action_view/component.rb +1 -20
- data/lib/action_view/component/base.rb +2 -246
- data/lib/action_view/component/preview.rb +1 -80
- data/lib/action_view/component/railtie.rb +1 -64
- data/lib/action_view/component/test_case.rb +1 -3
- data/lib/action_view/component/test_helpers.rb +1 -19
- data/lib/rails/generators/component/component_generator.rb +6 -12
- data/lib/rails/generators/component/templates/component.rb.tt +0 -4
- data/lib/rails/generators/erb/component_generator.rb +21 -0
- data/lib/rails/generators/erb/templates/component.html.erb.tt +1 -0
- data/lib/rails/generators/haml/component_generator.rb +21 -0
- data/lib/rails/generators/haml/templates/component.html.haml.tt +1 -0
- data/lib/rails/generators/rspec/component_generator.rb +1 -1
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -1
- data/lib/rails/generators/slim/component_generator.rb +21 -0
- data/lib/rails/generators/slim/templates/component.html.slim.tt +1 -0
- data/lib/rails/generators/test_unit/component_generator.rb +1 -1
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +3 -3
- data/lib/railties/lib/rails.rb +0 -1
- data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +1 -1
- data/lib/view_component.rb +30 -0
- data/lib/view_component/base.rb +279 -0
- data/lib/view_component/conversion.rb +9 -0
- data/lib/view_component/engine.rb +65 -0
- data/lib/view_component/preview.rb +78 -0
- data/lib/view_component/previewable.rb +25 -0
- data/lib/view_component/render_monkey_patch.rb +31 -0
- data/lib/view_component/rendering_monkey_patch.rb +13 -0
- data/lib/view_component/template_error.rb +9 -0
- data/lib/view_component/test_case.rb +9 -0
- data/lib/view_component/test_helpers.rb +39 -0
- data/lib/view_component/version.rb +11 -0
- data/script/console +1 -1
- metadata +48 -21
- data/lib/action_view/component/conversion.rb +0 -11
- data/lib/action_view/component/previewable.rb +0 -27
- data/lib/action_view/component/render_monkey_patch.rb +0 -29
- data/lib/action_view/component/template_error.rb +0 -11
- data/lib/action_view/component/version.rb +0 -13
- data/lib/rails/generators/component/templates/component.html.erb.tt +0 -5
- data/lib/railties/lib/rails/component_examples_controller.rb +0 -9
- 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 "
|
6
|
+
require "view_component/version"
|
7
7
|
|
8
8
|
Gem::Specification.new do |spec|
|
9
9
|
spec.name = "actionview-component"
|
10
|
-
spec.version =
|
10
|
+
spec.version = ViewComponent::VERSION::STRING
|
11
11
|
spec.authors = ["GitHub Open Source"]
|
12
|
-
spec.email = ["opensource+
|
12
|
+
spec.email = ["opensource+view_component@github.com"]
|
13
13
|
|
14
|
-
spec.summary = %q{
|
15
|
-
spec.
|
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.
|
38
|
-
spec.add_development_dependency "
|
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 =
|
18
|
+
@previews = ViewComponent::Preview.all
|
18
19
|
@page_title = "Component Previews"
|
19
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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|
|
51
|
+
preview = candidates.detect { |candidate| ViewComponent::Preview.exists?(candidate) }
|
42
52
|
|
43
53
|
if preview
|
44
|
-
@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
|
+
|
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 "
|
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 <
|
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
|