view_component 1.16.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.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE +27 -0
- data/.github/PULL_REQUEST_TEMPLATE +17 -0
- data/.github/workflows/ruby_on_rails.yml +31 -0
- data/.gitignore +53 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +373 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +46 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +202 -0
- data/LICENSE.txt +21 -0
- data/README.md +460 -0
- data/Rakefile +12 -0
- data/app/controllers/rails/components_controller.rb +65 -0
- data/docs/case-studies/jellyswitch.md +76 -0
- data/lib/action_view/component.rb +4 -0
- data/lib/action_view/component/base.rb +13 -0
- data/lib/action_view/component/preview.rb +8 -0
- data/lib/action_view/component/railtie.rb +3 -0
- data/lib/action_view/component/test_case.rb +9 -0
- data/lib/action_view/component/test_helpers.rb +17 -0
- data/lib/rails/generators/component/USAGE +13 -0
- data/lib/rails/generators/component/component_generator.rb +44 -0
- data/lib/rails/generators/component/templates/component.rb.tt +5 -0
- 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 +19 -0
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +13 -0
- 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 +20 -0
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +10 -0
- data/lib/railties/lib/rails.rb +5 -0
- data/lib/railties/lib/rails/templates/rails/components/index.html.erb +8 -0
- data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +1 -0
- data/lib/railties/lib/rails/templates/rails/components/previews.html.erb +6 -0
- 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/bootstrap +6 -0
- data/script/console +8 -0
- data/script/install +6 -0
- data/script/release +6 -0
- data/script/test +6 -0
- data/view_component.gemspec +46 -0
- metadata +226 -0
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
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("../../../lib/railties/lib/rails/templates/rails", __dir__)
|
7
|
+
prepend_view_path "#{Rails.root}/app/views/" if defined?(Rails.root)
|
8
|
+
|
9
|
+
around_action :set_locale, only: :previews
|
10
|
+
before_action :find_preview, only: :previews
|
11
|
+
before_action :require_local!, unless: :show_previews?
|
12
|
+
|
13
|
+
if respond_to?(:content_security_policy)
|
14
|
+
content_security_policy(false)
|
15
|
+
end
|
16
|
+
|
17
|
+
def index
|
18
|
+
@previews = ViewComponent::Preview.all
|
19
|
+
@page_title = "Component Previews"
|
20
|
+
# rubocop:disable GitHub/RailsControllerRenderPathsExist
|
21
|
+
render "components/index"
|
22
|
+
# rubocop:enable GitHub/RailsControllerRenderPathsExist
|
23
|
+
end
|
24
|
+
|
25
|
+
def previews
|
26
|
+
if params[:path] == @preview.preview_name
|
27
|
+
@page_title = "Component Previews for #{@preview.preview_name}"
|
28
|
+
# rubocop:disable GitHub/RailsControllerRenderPathsExist
|
29
|
+
render "components/previews"
|
30
|
+
# rubocop:enable GitHub/RailsControllerRenderPathsExist
|
31
|
+
else
|
32
|
+
@example_name = File.basename(params[:path])
|
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
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def show_previews? # :doc:
|
45
|
+
ViewComponent::Base.show_previews
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_preview # :doc:
|
49
|
+
candidates = []
|
50
|
+
params[:path].to_s.scan(%r{/|$}) { candidates << $` }
|
51
|
+
preview = candidates.detect { |candidate| ViewComponent::Preview.exists?(candidate) }
|
52
|
+
|
53
|
+
if preview
|
54
|
+
@preview = ViewComponent::Preview.find(preview)
|
55
|
+
else
|
56
|
+
raise AbstractController::ActionNotFound, "Component preview '#{params[:path]}' not found"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_locale
|
61
|
+
I18n.with_locale(params[:locale] || I18n.default_locale) do
|
62
|
+
yield
|
63
|
+
end
|
64
|
+
end
|
65
|
+
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.
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
module Component
|
5
|
+
module TestHelpers
|
6
|
+
include ViewComponent::TestHelpers
|
7
|
+
|
8
|
+
def render_component(component, **args, &block)
|
9
|
+
ActiveSupport::Deprecation.warn(
|
10
|
+
"`render_component` has been deprecated in favor of `render_inline`, and will be removed in v2.0.0."
|
11
|
+
)
|
12
|
+
|
13
|
+
render_inline(component, args, &block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
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,44 @@
|
|
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
|
+
check_class_collision suffix: "Component"
|
10
|
+
|
11
|
+
def create_component_file
|
12
|
+
template "component.rb", File.join("app/components", class_path, "#{file_name}_component.rb")
|
13
|
+
end
|
14
|
+
|
15
|
+
hook_for :test_framework
|
16
|
+
|
17
|
+
hook_for :template_engine do |instance, template_engine|
|
18
|
+
instance.invoke template_engine, [instance.name]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def file_name
|
24
|
+
@_file_name ||= super.sub(/_component\z/i, "")
|
25
|
+
end
|
26
|
+
|
27
|
+
def parent_class
|
28
|
+
defined?(ApplicationComponent) ? "ApplicationComponent" : "ViewComponent::Base"
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize_signature
|
32
|
+
if attributes.present?
|
33
|
+
attributes.map { |attr| "#{attr.name}:" }.join(", ")
|
34
|
+
else
|
35
|
+
"*"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize_body
|
40
|
+
attributes.map { |attr| "@#{attr.name} = #{attr.name}" }.join("\n ")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/erb"
|
4
|
+
|
5
|
+
module Erb
|
6
|
+
module Generators
|
7
|
+
class ComponentGenerator < Base
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def copy_view_file
|
11
|
+
template "component.html.erb", File.join("app/components", class_path, "#{file_name}_component.html.erb")
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def file_name
|
17
|
+
@_file_name ||= super.sub(/_component\z/i, "")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<div>Add <%= class_name %> template here</div>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/erb/component_generator"
|
4
|
+
|
5
|
+
module Haml
|
6
|
+
module Generators
|
7
|
+
class ComponentGenerator < Erb::Generators::ComponentGenerator
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def copy_view_file
|
11
|
+
template "component.html.haml", File.join("app/components", class_path, "#{file_name}_component.html.haml")
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def file_name
|
17
|
+
@_file_name ||= super.sub(/_component\z/i, "")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
%div Add <%= class_name %> template here
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rspec
|
4
|
+
module Generators
|
5
|
+
class ComponentGenerator < ::Rails::Generators::NamedBase
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
7
|
+
|
8
|
+
def create_test_file
|
9
|
+
template "component_spec.rb", File.join("spec/components", class_path, "#{file_name}_component_spec.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def file_name
|
15
|
+
@_file_name ||= super.sub(/_component\z/i, "")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
RSpec.describe <%= class_name %>Component, type: :component do
|
4
|
+
pending "add some examples to (or delete) #{__FILE__}"
|
5
|
+
|
6
|
+
# it "renders something useful" do
|
7
|
+
# expect(
|
8
|
+
# render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html
|
9
|
+
# ).to include(
|
10
|
+
# "Hello, components!"
|
11
|
+
# )
|
12
|
+
# end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/erb/component_generator"
|
4
|
+
|
5
|
+
module Slim
|
6
|
+
module Generators
|
7
|
+
class ComponentGenerator < Erb::Generators::ComponentGenerator
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def copy_view_file
|
11
|
+
template "component.html.slim", File.join("app/components", class_path, "#{file_name}_component.html.slim")
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def file_name
|
17
|
+
@_file_name ||= super.sub(/_component\z/i, "")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
div Add <%= class_name %> template here
|
@@ -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", class_path, "#{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,10 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class <%= class_name %>ComponentTest < ViewComponent::TestCase
|
4
|
+
test "component renders something useful" do
|
5
|
+
# assert_equal(
|
6
|
+
# %(<span>Hello, components!</span>),
|
7
|
+
# render_inline(<%= class_name %>Component.new(message: "Hello, components!")).css("span").to_html
|
8
|
+
# )
|
9
|
+
end
|
10
|
+
end
|