view_component 2.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.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/CHANGELOG.md +582 -0
- data/LICENSE.txt +21 -0
- data/README.md +1000 -0
- data/app/controllers/view_components_controller.rb +71 -0
- data/app/views/view_components/index.html.erb +8 -0
- data/app/views/view_components/preview.html.erb +1 -0
- data/app/views/view_components/previews.html.erb +6 -0
- data/lib/rails/generators/component/USAGE +13 -0
- data/lib/rails/generators/component/component_generator.rb +42 -0
- data/lib/rails/generators/component/templates/component.rb.tt +7 -0
- data/lib/rails/generators/erb/component_generator.rb +30 -0
- data/lib/rails/generators/erb/templates/component.html.erb.tt +1 -0
- data/lib/rails/generators/haml/component_generator.rb +30 -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 +30 -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/view_component.rb +14 -0
- data/lib/view_component/base.rb +458 -0
- data/lib/view_component/collection.rb +41 -0
- data/lib/view_component/compile_cache.rb +24 -0
- data/lib/view_component/engine.rb +101 -0
- data/lib/view_component/preview.rb +111 -0
- data/lib/view_component/preview_template_error.rb +6 -0
- data/lib/view_component/previewable.rb +38 -0
- data/lib/view_component/render_component_helper.rb +9 -0
- data/lib/view_component/render_component_to_string_helper.rb +9 -0
- data/lib/view_component/render_monkey_patch.rb +13 -0
- data/lib/view_component/render_to_string_monkey_patch.rb +13 -0
- data/lib/view_component/rendering_component_helper.rb +9 -0
- data/lib/view_component/rendering_monkey_patch.rb +13 -0
- data/lib/view_component/slot.rb +7 -0
- data/lib/view_component/slotable.rb +121 -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 +49 -0
- data/lib/view_component/version.rb +11 -0
- metadata +244 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
class Collection
|
5
|
+
def render_in(view_context, &block)
|
6
|
+
iterator = ActionView::PartialIteration.new(@collection.size)
|
7
|
+
|
8
|
+
@component.compile(raise_errors: true)
|
9
|
+
@component.validate_collection_parameter!(validate_default: true)
|
10
|
+
|
11
|
+
@collection.map do |item|
|
12
|
+
content = @component.new(**component_options(item, iterator)).render_in(view_context, &block)
|
13
|
+
iterator.iterate!
|
14
|
+
content
|
15
|
+
end.join.html_safe
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def initialize(component, object, **options)
|
21
|
+
@component = component
|
22
|
+
@collection = collection_variable(object || [])
|
23
|
+
@options = options
|
24
|
+
end
|
25
|
+
|
26
|
+
def collection_variable(object)
|
27
|
+
if object.respond_to?(:to_ary)
|
28
|
+
object.to_ary
|
29
|
+
else
|
30
|
+
raise ArgumentError.new("The value of the argument isn't a valid collection. Make sure it responds to to_ary: #{object.inspect}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def component_options(item, iterator)
|
35
|
+
item_options = { @component.collection_parameter => item }
|
36
|
+
item_options[@component.collection_counter_parameter] = iterator.index + 1 if @component.counter_argument_present?
|
37
|
+
|
38
|
+
@options.merge(item_options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
# Keeps track of which templates have already been compiled
|
5
|
+
# This is not part of the public API
|
6
|
+
module CompileCache
|
7
|
+
mattr_accessor :cache, instance_reader: false, instance_accessor: false do
|
8
|
+
Set.new
|
9
|
+
end
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def register(klass)
|
13
|
+
cache << klass
|
14
|
+
end
|
15
|
+
|
16
|
+
def compiled?(klass)
|
17
|
+
cache.include? klass
|
18
|
+
end
|
19
|
+
|
20
|
+
def invalidate!
|
21
|
+
cache.clear
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
require "view_component"
|
5
|
+
|
6
|
+
module ViewComponent
|
7
|
+
class Engine < Rails::Engine # :nodoc:
|
8
|
+
config.view_component = ActiveSupport::OrderedOptions.new
|
9
|
+
config.view_component.preview_paths ||= []
|
10
|
+
|
11
|
+
initializer "view_component.set_configs" do |app|
|
12
|
+
options = app.config.view_component
|
13
|
+
|
14
|
+
options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
|
15
|
+
options.show_previews = Rails.env.development? if options.show_previews.nil?
|
16
|
+
options.preview_route ||= ViewComponent::Base.preview_route
|
17
|
+
|
18
|
+
if options.show_previews
|
19
|
+
options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root)
|
20
|
+
|
21
|
+
if options.preview_path.present?
|
22
|
+
ActiveSupport::Deprecation.warn(
|
23
|
+
"`preview_path` will be removed in v3.0.0. Use `preview_paths` instead."
|
24
|
+
)
|
25
|
+
options.preview_paths << options.preview_path
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
ActiveSupport.on_load(:view_component) do
|
30
|
+
options.each { |k, v| send("#{k}=", v) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
initializer "view_component.set_autoload_paths" do |app|
|
35
|
+
options = app.config.view_component
|
36
|
+
|
37
|
+
if options.show_previews && options.preview_path
|
38
|
+
ActiveSupport::Dependencies.autoload_paths << options.preview_path
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
initializer "view_component.eager_load_actions" do
|
43
|
+
ActiveSupport.on_load(:after_initialize) do
|
44
|
+
ViewComponent::Base.descendants.each(&:compile)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
initializer "view_component.compile_config_methods" do
|
49
|
+
ActiveSupport.on_load(:view_component) do
|
50
|
+
config.compile_methods! if config.respond_to?(:compile_methods!)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
initializer "view_component.monkey_patch_render" do |app|
|
55
|
+
next if Rails.version.to_f >= 6.1 || !app.config.view_component.render_monkey_patch_enabled
|
56
|
+
|
57
|
+
ActiveSupport.on_load(:action_view) do
|
58
|
+
require "view_component/render_monkey_patch"
|
59
|
+
ActionView::Base.prepend ViewComponent::RenderMonkeyPatch
|
60
|
+
end
|
61
|
+
|
62
|
+
ActiveSupport.on_load(:action_controller) do
|
63
|
+
require "view_component/rendering_monkey_patch"
|
64
|
+
require "view_component/render_to_string_monkey_patch"
|
65
|
+
ActionController::Base.prepend ViewComponent::RenderingMonkeyPatch
|
66
|
+
ActionController::Base.prepend ViewComponent::RenderToStringMonkeyPatch
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
initializer "view_component.include_render_component" do |app|
|
71
|
+
next if Rails.version.to_f >= 6.1
|
72
|
+
|
73
|
+
ActiveSupport.on_load(:action_view) do
|
74
|
+
require "view_component/render_component_helper"
|
75
|
+
ActionView::Base.include ViewComponent::RenderComponentHelper
|
76
|
+
end
|
77
|
+
|
78
|
+
ActiveSupport.on_load(:action_controller) do
|
79
|
+
require "view_component/rendering_component_helper"
|
80
|
+
require "view_component/render_component_to_string_helper"
|
81
|
+
ActionController::Base.include ViewComponent::RenderingComponentHelper
|
82
|
+
ActionController::Base.include ViewComponent::RenderComponentToStringHelper
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
config.after_initialize do |app|
|
87
|
+
options = app.config.view_component
|
88
|
+
|
89
|
+
if options.show_previews
|
90
|
+
app.routes.prepend do
|
91
|
+
get options.preview_route, to: "view_components#index", as: :preview_view_components, internal: true
|
92
|
+
get "#{options.preview_route}/*path", to: "view_components#previews", as: :preview_view_component, internal: true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
app.executor.to_run :before do
|
97
|
+
CompileCache.invalidate! unless ActionView::Base.cache_template_loading
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/descendants_tracker"
|
4
|
+
|
5
|
+
module ViewComponent # :nodoc:
|
6
|
+
class Preview
|
7
|
+
include ActionView::Helpers::TagHelper
|
8
|
+
extend ActiveSupport::DescendantsTracker
|
9
|
+
|
10
|
+
def render(component, **args, &block)
|
11
|
+
{
|
12
|
+
args: args,
|
13
|
+
block: block,
|
14
|
+
component: component,
|
15
|
+
locals: {},
|
16
|
+
template: "view_components/preview",
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def render_with_template(template: nil, locals: {})
|
21
|
+
{
|
22
|
+
template: template,
|
23
|
+
locals: locals
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
# Returns all component preview classes.
|
29
|
+
def all
|
30
|
+
load_previews if descendants.empty?
|
31
|
+
descendants
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the arguments for rendering of the component in its layout
|
35
|
+
def render_args(example, params: {})
|
36
|
+
example_params_names = instance_method(example).parameters.map(&:last)
|
37
|
+
provided_params = params.slice(*example_params_names).to_h.symbolize_keys
|
38
|
+
result = provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params)
|
39
|
+
result ||= {}
|
40
|
+
result[:template] = preview_example_template_path(example) if result[:template].nil?
|
41
|
+
@layout = nil unless defined?(@layout)
|
42
|
+
result.merge(layout: @layout)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the component object class associated to the preview.
|
46
|
+
def component
|
47
|
+
name.chomp("Preview").constantize
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns all of the available examples for the component preview.
|
51
|
+
def examples
|
52
|
+
public_instance_methods(false).map(&:to_s).sort
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns +true+ if the example of the component preview exists.
|
56
|
+
def example_exists?(example)
|
57
|
+
examples.include?(example)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns +true+ if the preview exists.
|
61
|
+
def exists?(preview)
|
62
|
+
all.any? { |p| p.preview_name == preview }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Find a component preview by its underscored class name.
|
66
|
+
def find(preview)
|
67
|
+
all.find { |p| p.preview_name == preview }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the underscored name of the component preview without the suffix.
|
71
|
+
def preview_name
|
72
|
+
name.chomp("Preview").underscore
|
73
|
+
end
|
74
|
+
|
75
|
+
# Setter for layout name.
|
76
|
+
def layout(layout_name)
|
77
|
+
@layout = layout_name
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the relative path (from preview_path) to the preview example template if the template exists
|
81
|
+
def preview_example_template_path(example)
|
82
|
+
preview_path = Array(preview_paths).detect do |preview_path|
|
83
|
+
Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
|
84
|
+
end
|
85
|
+
|
86
|
+
if preview_path.nil?
|
87
|
+
raise PreviewTemplateError, "preview template for example #{example} does not exist"
|
88
|
+
end
|
89
|
+
|
90
|
+
path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
|
91
|
+
Pathname.new(path).relative_path_from(Pathname.new(preview_path)).to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def load_previews
|
97
|
+
Array(preview_paths).each do |preview_path|
|
98
|
+
Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def preview_paths
|
103
|
+
Base.preview_paths
|
104
|
+
end
|
105
|
+
|
106
|
+
def show_previews
|
107
|
+
Base.show_previews
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module ViewComponent # :nodoc:
|
6
|
+
module Previewable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
# Set the location of component previews through app configuration:
|
11
|
+
#
|
12
|
+
# config.view_component.preview_paths << "#{Rails.root}/lib/component_previews"
|
13
|
+
#
|
14
|
+
mattr_accessor :preview_paths, instance_writer: false
|
15
|
+
|
16
|
+
# TODO: deprecated, remove in v3.0.0
|
17
|
+
mattr_accessor :preview_path, instance_writer: false
|
18
|
+
|
19
|
+
# Enable or disable component previews through app configuration:
|
20
|
+
#
|
21
|
+
# config.view_component.show_previews = true
|
22
|
+
#
|
23
|
+
# Defaults to +true+ for development environment
|
24
|
+
#
|
25
|
+
mattr_accessor :show_previews, instance_writer: false
|
26
|
+
|
27
|
+
# Set the entry route for component previews through app configuration:
|
28
|
+
#
|
29
|
+
# config.view_component.preview_route = "/previews"
|
30
|
+
#
|
31
|
+
# Defaults to +/rails/view_components+ when `show_previews' is enabled
|
32
|
+
#
|
33
|
+
mattr_accessor :preview_route, instance_writer: false do
|
34
|
+
"/rails/view_components"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
module RenderToStringMonkeyPatch # :nodoc:
|
5
|
+
def render_to_string(options = {}, args = {})
|
6
|
+
if options.respond_to?(:render_in)
|
7
|
+
options.render_in(self.view_context)
|
8
|
+
else
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
module RenderingMonkeyPatch # :nodoc:
|
5
|
+
def render(options = {}, args = {})
|
6
|
+
if options.respond_to?(:render_in)
|
7
|
+
self.response_body = options.render_in(self.view_context)
|
8
|
+
else
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
require "view_component/slot"
|
6
|
+
|
7
|
+
module ViewComponent
|
8
|
+
module Slotable
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
# support initializing slots as:
|
13
|
+
#
|
14
|
+
# with_slot(
|
15
|
+
# :header,
|
16
|
+
# collection: true|false,
|
17
|
+
# class_name: "Header" # class name string, used to instantiate Slot
|
18
|
+
# )
|
19
|
+
def with_slot(*slot_names, collection: false, class_name: nil)
|
20
|
+
slot_names.each do |slot_name|
|
21
|
+
# Ensure slot_name is not already declared
|
22
|
+
if self.slots.key?(slot_name)
|
23
|
+
raise ArgumentError.new("#{slot_name} slot declared multiple times")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Ensure slot name is not :content
|
27
|
+
if slot_name == :content
|
28
|
+
raise ArgumentError.new ":content is a reserved slot name. Please use another name, such as ':body'"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set the name of the method used to access the Slot(s)
|
32
|
+
accessor_name =
|
33
|
+
if collection
|
34
|
+
# If Slot is a collection, set the accessor
|
35
|
+
# name to the pluralized form of the slot name
|
36
|
+
# For example: :tab => :tabs
|
37
|
+
ActiveSupport::Inflector.pluralize(slot_name)
|
38
|
+
else
|
39
|
+
slot_name
|
40
|
+
end
|
41
|
+
|
42
|
+
instance_variable_name = "@#{accessor_name}"
|
43
|
+
|
44
|
+
# If the slot is a collection, define an accesor that defaults to an empty array
|
45
|
+
if collection
|
46
|
+
class_eval <<-RUBY
|
47
|
+
def #{accessor_name}
|
48
|
+
#{instance_variable_name} ||= []
|
49
|
+
end
|
50
|
+
RUBY
|
51
|
+
else
|
52
|
+
attr_reader accessor_name
|
53
|
+
end
|
54
|
+
|
55
|
+
# Default class_name to ViewComponent::Slot
|
56
|
+
class_name = "ViewComponent::Slot" unless class_name.present?
|
57
|
+
|
58
|
+
# Register the slot on the component
|
59
|
+
self.slots[slot_name] = {
|
60
|
+
class_name: class_name,
|
61
|
+
instance_variable_name: instance_variable_name,
|
62
|
+
collection: collection
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Build a Slot instance on a component,
|
69
|
+
# exposing it for use inside the
|
70
|
+
# component template.
|
71
|
+
#
|
72
|
+
# slot: Name of Slot, in symbol form
|
73
|
+
# **args: Arguments to be passed to Slot initializer
|
74
|
+
#
|
75
|
+
# For example:
|
76
|
+
# <%= render(SlotsComponent.new) do |component| %>
|
77
|
+
# <% component.slot(:footer, class_names: "footer-class") do %>
|
78
|
+
# <p>This is my footer!</p>
|
79
|
+
# <% end %>
|
80
|
+
# <% end %>
|
81
|
+
#
|
82
|
+
def slot(slot_name, **args, &block)
|
83
|
+
# Raise ArgumentError if `slot` does not exist
|
84
|
+
unless slots.keys.include?(slot_name)
|
85
|
+
raise ArgumentError.new "Unknown slot '#{slot_name}' - expected one of '#{slots.keys}'"
|
86
|
+
end
|
87
|
+
|
88
|
+
slot = slots[slot_name]
|
89
|
+
|
90
|
+
# The class name of the Slot, such as Header
|
91
|
+
slot_class = self.class.const_get(slot[:class_name])
|
92
|
+
|
93
|
+
unless slot_class <= ViewComponent::Slot
|
94
|
+
raise ArgumentError.new "#{slot[:class_name]} must inherit from ViewComponent::Slot"
|
95
|
+
end
|
96
|
+
|
97
|
+
# Instantiate Slot class, accommodating Slots that don't accept arguments
|
98
|
+
slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
|
99
|
+
|
100
|
+
# Capture block and assign to slot_instance#content
|
101
|
+
slot_instance.content = view_context.capture(&block).strip.html_safe if block_given?
|
102
|
+
|
103
|
+
if slot[:collection]
|
104
|
+
# Initialize instance variable as an empty array
|
105
|
+
# if slot is a collection and has yet to be initialized
|
106
|
+
unless instance_variable_defined?(slot[:instance_variable_name])
|
107
|
+
instance_variable_set(slot[:instance_variable_name], [])
|
108
|
+
end
|
109
|
+
|
110
|
+
# Append Slot instance to collection accessor Array
|
111
|
+
instance_variable_get(slot[:instance_variable_name]) << slot_instance
|
112
|
+
else
|
113
|
+
# Assign the Slot instance to the slot accessor
|
114
|
+
instance_variable_set(slot[:instance_variable_name], slot_instance)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Return nil, as this method should not output anything to the view itself.
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|