Warning
Please note that Phlex::Slotable is currently under development and may undergo changes to its API before reaching the stable release (1.0.0). As a result, there may be breaking changes that affect its usage.
Phlex::Slotable
Phlex::Slotable enables slots feature to Phlex views. Inspired by ViewComponent.
- What is a slot?
- Getting started
- Generic slot
- Slot collection
- Component slot
- Lambda slot
- Polymorphic slot
- Performance
- Development
- Contributing
What is a slot?
In the context of view components, a slot serves as a placeholder inside a component that can be filled with custom content. Essentially, slots enable a component to accept external content and autonomously organize it within its structure. This abstraction allows developers to work with components without needing to understand their internals, thereby ensuring visual consistency and improving developer experience.
Getting started
Install the gem and add to the application's Gemfile by executing:
$ bundle add phlex-slotable
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install phlex-slotable
Tip
If you prefer not to add another dependency to your project, you can simply copy the Phlex::Slotable file into your project.
Afterward, simply include Phlex::Slotable
into your Phlex component and utilize slot
macro to define the component's slots. For example:
class MyComponent < Phlex::HTML
include Phlex::Slotable
slot :my_slot
end
Below, you will find a more detailed explanation of how to use the slot
API.
Generic slot
Any content can be passed to components through generic slots, also known as passthrough slots. To define a generic slot, use slot :{slot_name}
. For example:
class PageComponent < Phlex::HTML
include Phlex::Slotable
slot :title
end
To render a slot, render the {slot_name}_slot
:
class PageComponent < Phlex::HTML
include Phlex::Slotable
slot :title
def template
header { render title_slot }
end
end
To pass content to the component's slot, you should use with_{slot_name}
:
PageComponent.new.call do |page|
page.with_title do
h1 { "Hello World!" }
end
end
Returning:
<header>
<h1>Hello World!</h1>
</header>
You can test if a slot has been passed to the component with {slot_name}_slot?
method. For example:
class PageComponent < Phlex::HTML
include Phlex::Slotable
slot :title
def template
if header_slot?
header { render title_slot }
else
plain "No title"
end
end
end
Slot collection
A slot collection denotes a slot capable of being rendered multiple times within a component. It has some minor differences compared to a single slot seen previously. First, you should pass collection: true
when defining the slot:
class ListComponent < Phlex::HTML
include Phlex::Slotable
slot :item, collection: true
end
To render a collection of slots, iterate over the {slot_name}_slots
collection and render each slot individually:
class ListComponent < Phlex::HTML
include Phlex::Slotable
slot :item, collection: true
def template
if item_slots?
ul do
item_slots.each do |item_slot|
li { render item_slot }
end
end
end
span { "Total: #{item_slots.size}" }
end
end
To set slot content, use the with_{slot_name}
method when rendering the component. Unlike the single slot, with_{slot_name}
can be called multiple times:
ListComponent.new.call do |list|
list.with_item { "Item A" }
list.with_item { "Item B" }
list.with_item { "Item C" }
end
Returning:
<ul>
<li>Item A</li>
<li>Item B</li>
<li>Item C</li>
</ul>
<span>Total: 3</span>
Component slot
Slots have the capability to render other components. When defining a slot, provide the name of a component class as the second argument to define a component slot
class ListHeaderComponent < Phlex::HTML
# omitted code
end
class ListItemComponent < Phlex::HTML
# omitted code
end
class ListComponent < Phlex::HTML
include Phlex::Slotable
slot :header, ListHeaderComponent
slot :item, ListItemComponent, collection: true
def template
div id: "header" do
render header_slot if header_slot?
end
ul do
item_slots.each { |slot| render slot }
end
end
end
ListComponent.new.call do |list|
list.with_header(size: "lg") { "Hello World!" }
list.with_item(active: true) { "Item A" }
list.with_item { "Item B" }
list.with_item { "Item C" }
end
Returning:
<div id="header">
<h1 class="text-lg">Hello World!</h1>
<ul>
<li class="active">Item A</li>
<li>Item B</li>
<li>Item C</li>
</ul>
</div>
Tip
You can also pass the component class as a string if your component class hasn't been defined yet. For example:
slot :header, "HeaderComponent"
slot :item, "ItemComponent", collection: true
Lambda slot
Lambda slots are valuable when you prefer not to create another component for straightforward structures or when you need to render another component with specific parameters.
class ListComponent < Phlex::HTML
include Phlex::Slotable
slot :header, ->(size:, &content) do
render HeaderComponent.new(size: size, color: "primary")
end
slot :item, ->(href:, &content) { li { a(href: href, &content) } }, collection: true
def template
div id: "header" do
render header_slot if header_slot?
end
ul do
item_slots.each { |slot| render slot }
end
end
end
ListComponent.new.call do |list|
list.with_header(size: "lg") { "Hello World!" }
list.with_item(href: "/a") { "Item A" }
list.with_item(href: "/b") { "Item B" }
list.with_item(href: "/c") { "Item C" }
end
Returning:
<div id="header">
<h1 class="text-lg text-primary">Hello World!</h1>
<ul>
<li><a href="/a">Item A</a></li>
<li><a href="/b">Item B</a></li>
<li><a href="/c">Item C</a></li>
</ul>
</div>
Tip
You can access the internal component state within lambda slots. For example
slot :header, ->(&content) { render HeaderComponent.new(featured: @featured), &content }
def initialize(featured:)
@featured = feature
end
Polymorphic slot
Polymorphic slots can render one of several possible slots, allowing for flexibility in component content. This feature is particularly useful when you require a fixed structure but need to accommodate different types of content. To implement this, simply pass a types hash containing the types along with corresponding slot definitions.
class IconComponent < Phlex::HTML
# omitted code
end
class ImageComponent < Phlex::HTML
# omitted code
end
class CardComponent < Phlex::HTML
include Phlex::Slotable
slot :avatar, types: { icon: IconComponent, image: ImageComponent }
def template
if avatar_slot?
div id: "avatar" do
render avatar_slot
end
end
end
end
User = Data.define(:image_url)
user = User.new(image_url: "user.png")
CardComponent.new.call do |card|
if user.image_url
card.with_image_avatar(src: user.image_url)
else
card.with_icon_avatar(name: :user)
end
end
Returning:
<div id="avatar">
<img src="user.png"/>
</div>
Note that you need to use with_{type}_{slot_name}
to set slot content. In the example above, it was used with_image_avatar
and with_icon_avatar
.
Tip
You can take advantage of all the previously introduced features, such as lambda slot and slot collection:
slot :avatar, collection: true, types: {
icon: IconComponent,
image: "ImageComponent",
text: ->(&content) { span(class: "avatar", &content) }
}
Performance
Using Phlex::Slotable you don't suffer a performance penalty compared to using Phlex::DeferredRender, sometimes it can even be a little faster.
Generated using `ruby benchmark/main.rb`
Phlex 1.11.0
Phlex::Slotable 0.5.0
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin23]
Warming up --------------------------------------
Deferred 22.176k i/100ms
Slotable 23.516k i/100ms
Calculating -------------------------------------
Deferred 222.727k (± 0.8%) i/s (4.49 μs/i) - 1.131M in 5.078157s
Slotable 237.405k (± 0.6%) i/s (4.21 μs/i) - 1.199M in 5.051936s
Comparison:
Slotable: 237405.0 i/s
Deferred: 222726.8 i/s - 1.07x slower
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/stephannv/phlex-slotable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Phlex::Slot project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.