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.

Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE +27 -0
  3. data/.github/PULL_REQUEST_TEMPLATE +17 -0
  4. data/.github/workflows/ruby_on_rails.yml +31 -0
  5. data/.gitignore +53 -0
  6. data/.rubocop.yml +8 -0
  7. data/CHANGELOG.md +373 -0
  8. data/CODE_OF_CONDUCT.md +76 -0
  9. data/CONTRIBUTING.md +46 -0
  10. data/Gemfile +8 -0
  11. data/Gemfile.lock +202 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +460 -0
  14. data/Rakefile +12 -0
  15. data/app/controllers/rails/components_controller.rb +65 -0
  16. data/docs/case-studies/jellyswitch.md +76 -0
  17. data/lib/action_view/component.rb +4 -0
  18. data/lib/action_view/component/base.rb +13 -0
  19. data/lib/action_view/component/preview.rb +8 -0
  20. data/lib/action_view/component/railtie.rb +3 -0
  21. data/lib/action_view/component/test_case.rb +9 -0
  22. data/lib/action_view/component/test_helpers.rb +17 -0
  23. data/lib/rails/generators/component/USAGE +13 -0
  24. data/lib/rails/generators/component/component_generator.rb +44 -0
  25. data/lib/rails/generators/component/templates/component.rb.tt +5 -0
  26. data/lib/rails/generators/erb/component_generator.rb +21 -0
  27. data/lib/rails/generators/erb/templates/component.html.erb.tt +1 -0
  28. data/lib/rails/generators/haml/component_generator.rb +21 -0
  29. data/lib/rails/generators/haml/templates/component.html.haml.tt +1 -0
  30. data/lib/rails/generators/rspec/component_generator.rb +19 -0
  31. data/lib/rails/generators/rspec/templates/component_spec.rb.tt +13 -0
  32. data/lib/rails/generators/slim/component_generator.rb +21 -0
  33. data/lib/rails/generators/slim/templates/component.html.slim.tt +1 -0
  34. data/lib/rails/generators/test_unit/component_generator.rb +20 -0
  35. data/lib/rails/generators/test_unit/templates/component_test.rb.tt +10 -0
  36. data/lib/railties/lib/rails.rb +5 -0
  37. data/lib/railties/lib/rails/templates/rails/components/index.html.erb +8 -0
  38. data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +1 -0
  39. data/lib/railties/lib/rails/templates/rails/components/previews.html.erb +6 -0
  40. data/lib/view_component.rb +30 -0
  41. data/lib/view_component/base.rb +279 -0
  42. data/lib/view_component/conversion.rb +9 -0
  43. data/lib/view_component/engine.rb +65 -0
  44. data/lib/view_component/preview.rb +78 -0
  45. data/lib/view_component/previewable.rb +25 -0
  46. data/lib/view_component/render_monkey_patch.rb +31 -0
  47. data/lib/view_component/rendering_monkey_patch.rb +13 -0
  48. data/lib/view_component/template_error.rb +9 -0
  49. data/lib/view_component/test_case.rb +9 -0
  50. data/lib/view_component/test_helpers.rb +39 -0
  51. data/lib/view_component/version.rb +11 -0
  52. data/script/bootstrap +6 -0
  53. data/script/console +8 -0
  54. data/script/install +6 -0
  55. data/script/release +6 -0
  56. data/script/test +6 -0
  57. data/view_component.gemspec +46 -0
  58. metadata +226 -0
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -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
+ &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.
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "view_component"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Component
5
+ class Base < ViewComponent::Base
6
+ include ActiveModel::Validations
7
+
8
+ def before_render_check
9
+ validate!
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Component # :nodoc:
5
+ class Preview < ViewComponent::Preview
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "view_component/engine"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Component
5
+ class TestCase
6
+ include ActionView::Component::TestHelpers
7
+ end
8
+ end
9
+ end
@@ -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,5 @@
1
+ class <%= class_name %>Component < <%= parent_class %>
2
+ def initialize(<%= initialize_signature %>)
3
+ <%= initialize_body %>
4
+ end
5
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ autoload :ComponentsController
5
+ end