Skip to content

Commit

Permalink
Implement slotable v2
Browse files Browse the repository at this point in the history
* `with_slot` is now `renders_one`
* `with_slot collection: true` is now `renders_many`
* Slots are no longer stand-alone classes, but are lambdas that return
  an object that responds to `render_in` or a string
* Add support for positional arguments in slots v2
* Abstract away `slot.content`, preferring `to_s` (implicit or explicit)
* Replace `#slot` API with an API using the slots name. e.g.
  `c.slot(:header, 1)` is now `c.header(1)`
  • Loading branch information
BlakeWilliams committed Nov 30, 2020
1 parent bbe48fc commit 2491135
Show file tree
Hide file tree
Showing 32 changed files with 1,039 additions and 187 deletions.
123 changes: 66 additions & 57 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,84 +1,84 @@
GIT
remote: git://github.com/rails/rails.git
revision: 26fd55e02a19111d9b7785ca262e95dde6645f1a
revision: 64b1c815323c17d3259e823c53d6a547152e61e3
specs:
actioncable (6.1.0.rc1)
actionpack (= 6.1.0.rc1)
activesupport (= 6.1.0.rc1)
actioncable (6.1.0.alpha)
actionpack (= 6.1.0.alpha)
activesupport (= 6.1.0.alpha)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.0.rc1)
actionpack (= 6.1.0.rc1)
activejob (= 6.1.0.rc1)
activerecord (= 6.1.0.rc1)
activestorage (= 6.1.0.rc1)
activesupport (= 6.1.0.rc1)
actionmailbox (6.1.0.alpha)
actionpack (= 6.1.0.alpha)
activejob (= 6.1.0.alpha)
activerecord (= 6.1.0.alpha)
activestorage (= 6.1.0.alpha)
activesupport (= 6.1.0.alpha)
mail (>= 2.7.1)
actionmailer (6.1.0.rc1)
actionpack (= 6.1.0.rc1)
actionview (= 6.1.0.rc1)
activejob (= 6.1.0.rc1)
activesupport (= 6.1.0.rc1)
actionmailer (6.1.0.alpha)
actionpack (= 6.1.0.alpha)
actionview (= 6.1.0.alpha)
activejob (= 6.1.0.alpha)
activesupport (= 6.1.0.alpha)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.0.rc1)
actionview (= 6.1.0.rc1)
activesupport (= 6.1.0.rc1)
actionpack (6.1.0.alpha)
actionview (= 6.1.0.alpha)
activesupport (= 6.1.0.alpha)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.0.rc1)
actionpack (= 6.1.0.rc1)
activerecord (= 6.1.0.rc1)
activestorage (= 6.1.0.rc1)
activesupport (= 6.1.0.rc1)
actiontext (6.1.0.alpha)
actionpack (= 6.1.0.alpha)
activerecord (= 6.1.0.alpha)
activestorage (= 6.1.0.alpha)
activesupport (= 6.1.0.alpha)
nokogiri (>= 1.8.5)
actionview (6.1.0.rc1)
activesupport (= 6.1.0.rc1)
actionview (6.1.0.alpha)
activesupport (= 6.1.0.alpha)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.0.rc1)
activesupport (= 6.1.0.rc1)
activejob (6.1.0.alpha)
activesupport (= 6.1.0.alpha)
globalid (>= 0.3.6)
activemodel (6.1.0.rc1)
activesupport (= 6.1.0.rc1)
activerecord (6.1.0.rc1)
activemodel (= 6.1.0.rc1)
activesupport (= 6.1.0.rc1)
activestorage (6.1.0.rc1)
actionpack (= 6.1.0.rc1)
activejob (= 6.1.0.rc1)
activerecord (= 6.1.0.rc1)
activesupport (= 6.1.0.rc1)
activemodel (6.1.0.alpha)
activesupport (= 6.1.0.alpha)
activerecord (6.1.0.alpha)
activemodel (= 6.1.0.alpha)
activesupport (= 6.1.0.alpha)
activestorage (6.1.0.alpha)
actionpack (= 6.1.0.alpha)
activejob (= 6.1.0.alpha)
activerecord (= 6.1.0.alpha)
activesupport (= 6.1.0.alpha)
marcel (~> 0.3.1)
mimemagic (~> 0.3.2)
activesupport (6.1.0.rc1)
activesupport (6.1.0.alpha)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
rails (6.1.0.rc1)
actioncable (= 6.1.0.rc1)
actionmailbox (= 6.1.0.rc1)
actionmailer (= 6.1.0.rc1)
actionpack (= 6.1.0.rc1)
actiontext (= 6.1.0.rc1)
actionview (= 6.1.0.rc1)
activejob (= 6.1.0.rc1)
activemodel (= 6.1.0.rc1)
activerecord (= 6.1.0.rc1)
activestorage (= 6.1.0.rc1)
activesupport (= 6.1.0.rc1)
rails (6.1.0.alpha)
actioncable (= 6.1.0.alpha)
actionmailbox (= 6.1.0.alpha)
actionmailer (= 6.1.0.alpha)
actionpack (= 6.1.0.alpha)
actiontext (= 6.1.0.alpha)
actionview (= 6.1.0.alpha)
activejob (= 6.1.0.alpha)
activemodel (= 6.1.0.alpha)
activerecord (= 6.1.0.alpha)
activestorage (= 6.1.0.alpha)
activesupport (= 6.1.0.alpha)
bundler (>= 1.15.0)
railties (= 6.1.0.rc1)
railties (= 6.1.0.alpha)
sprockets-rails (>= 2.0.0)
railties (6.1.0.rc1)
actionpack (= 6.1.0.rc1)
activesupport (= 6.1.0.rc1)
railties (6.1.0.alpha)
actionpack (= 6.1.0.alpha)
activesupport (= 6.1.0.alpha)
method_source
rake (>= 0.8.7)
thor (~> 1.0)
Expand All @@ -97,6 +97,8 @@ GEM
ansi (1.5.0)
ast (2.4.1)
benchmark-ips (2.8.2)
benchmark-memory (0.1.2)
memory_profiler (~> 0.9)
better_html (1.0.15)
actionview (>= 4.0)
activesupport (>= 4.0)
Expand All @@ -114,10 +116,11 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (~> 1.5)
xpath (~> 3.2)
coderay (1.1.3)
concurrent-ruby (1.1.7)
crass (1.0.6)
docile (1.3.2)
erubi (1.10.0)
erubi (1.9.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
haml (5.1.2)
Expand All @@ -134,6 +137,7 @@ GEM
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
memory_profiler (0.9.14)
method_source (1.0.0)
mimemagic (0.3.5)
mini_mime (1.0.2)
Expand All @@ -145,6 +149,9 @@ GEM
parallel (1.19.2)
parser (2.7.1.4)
ast (~> 2.4.1)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (4.0.5)
rack (2.2.3)
rack-test (1.1.0)
Expand Down Expand Up @@ -194,26 +201,28 @@ GEM
unicode-display_width (~> 1.1, >= 1.1.1)
thor (1.0.1)
tilt (2.0.10)
tzinfo (2.0.3)
tzinfo (2.0.2)
concurrent-ruby (~> 1.0)
unicode-display_width (1.6.1)
websocket-driver (0.7.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.4.1)
zeitwerk (2.4.0)

PLATFORMS
ruby

DEPENDENCIES
benchmark-ips (~> 2.8.2)
benchmark-memory (~> 0.1.2)
better_html (~> 1)
bundler (~> 1.14)
capybara (~> 3)
haml (~> 5)
minitest (= 5.6.0)
pry (~> 0.13)
rails!
rake (~> 13.0)
rubocop (= 0.74)
Expand Down
16 changes: 6 additions & 10 deletions lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
require "view_component/compile_cache"
require "view_component/previewable"
require "view_component/slotable"
require "view_component/sub_components"
require "view_component/sub_component_wrapper"

module ViewComponent
class Base < ActionView::Base
Expand All @@ -20,10 +22,6 @@ class Base < ActionView::Base
class_attribute :content_areas
self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2

# Hash of registered Slots
class_attribute :slots
self.slots = {}

# Entrypoint for rendering components.
#
# view_context: ActionView context from calling view
Expand Down Expand Up @@ -132,7 +130,10 @@ def view_cache_dependencies

# For caching, such as #cache_if
def format
@variant
# Ruby 2.6 throws a warning without this
if defined?(@variant)
@variant
end
end

# Assign the provided content to the content area accessor
Expand Down Expand Up @@ -207,10 +208,6 @@ def inherited(child)
# Removes the first part of the path and the extension.
child.virtual_path = child.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")

# Clone slot configuration into child class
# see #test_slots_pollution
child.slots = self.slots.clone

super
end

Expand Down Expand Up @@ -291,7 +288,6 @@ def initialize_parameters
def provided_collection_parameter
@provided_collection_parameter ||= nil
end

end

ActiveSupport.run_load_hooks(:view_component, self)
Expand Down
17 changes: 11 additions & 6 deletions lib/view_component/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,21 +146,26 @@ def matching_views_in_source_location
source_location = component_class.source_location
return [] unless source_location

location_without_extension = source_location.chomp(File.extname(source_location))

extensions = ActionView::Template.template_handler_extensions.join(",")

# view files in the same directory as the component
sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]

# view files in a directory named like the component
directory = File.dirname(source_location)
filename = File.basename(source_location, ".rb")
component_name = component_class.name.demodulize.underscore

sub_component_files = if component_class.name.include?("::")
subcomponent_path = component_class.name.deconstantize.underscore
Dir["#{directory}/#{subcomponent_path}/#{component_name}.*{#{extensions}}"]
else
[]
end

# view files in the same directory as the component
sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]

sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]

(sidecar_files - [source_location] + sidecar_directory_files)
(sidecar_files - [source_location] + sidecar_directory_files + sub_component_files)
end

def inline_calls
Expand Down
14 changes: 14 additions & 0 deletions lib/view_component/slotable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ module ViewComponent
module Slotable
extend ActiveSupport::Concern

included do
# Hash of registered Slots
class_attribute :slots
self.slots = {}
end

class_methods do
# support initializing slots as:
#
Expand Down Expand Up @@ -63,6 +69,14 @@ def #{accessor_name}
}
end
end

def inherited(child)
# Clone slot configuration into child class
# see #test_slots_pollution
child.slots = self.slots.clone

super
end
end

# Build a Slot instance on a component,
Expand Down
60 changes: 60 additions & 0 deletions lib/view_component/sub_component_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module ViewComponent
class SubComponentWrapper
attr_writer :_component_instance, :_content_block, :_content

# Parent must be `nil` for v1
def initialize(parent = nil)
@parent = parent
end

# Used to render the subcomponent content in the template
#
# There's currently 3 different values that may be set, that we can render.
#
# If the subcomponent renderable is a component, the string class name of a
# component, or a function that returns a component, we render that
# component instance, returning the string.
#
# If the subcomponent renderable is a function and returns a string, it is
# set as `@_content` and is returned directly.
#
# If there is no subcomponent renderable, we evaluate the block passed to
# the subcomponent and return it (content area style)
def to_s
if defined?(@_component_instance)
# render_in is faster than `parent.render`
@_component_instance.render_in(
@parent.send(:view_context),
&@_content_block
)
elsif defined?(@_content)
@_content
elsif defined?(@_content_block)
@_content_block.call
end
end

# This allows access to public component methods via the wrapper
#
# e.g.
#
# calling `header.name` (where `header` is a subcomponent) will call `name`
# on the `HeaderComponent` instance
#
# Where the component may includes:
#
# has_one :header, HeaderComponent
#
# class HeaderComponent < ViewComponent::Base
# def name
# @name
# end
# end
#
def method_missing(symbol, *args, &block)
@_component_instance.public_send(symbol, *args, &block)
end
end
end

0 comments on commit 2491135

Please sign in to comment.