Superpowered slots for ActionView partials
Slotify adds an unobtrusive (but powerful!) content slot API to ActionView partials.
Slots are a convenient way to pass blocks of content in to a partial without having to resort to ugly <% capture do ... end %> workarounds or unscoped (global) <% content_for :foo %> declarations.
Slotified partials are a great way to build components in a Rails app without the additional overhead and learning curve of libraries like ViewComponent or Phlex.
[!CAUTION] Slotify is still in a early stage of development. The documentation is still quite sparse and the API could change at any point prior to a
v1.0release.
Slotify basics
Slotify slots are defined using a strict locals-style magic comment at the top of partial templates (more details here).
<%# slots: (title:, body: nil, theme: "default") -%>
Slot content is accessed via standard local variables within the partial. So a simple, slot-enabled article partial template might look something like this:
<!-- _article.html.erb -->
<%# slots: (title: "Default title", body: nil) -%>
<article>
<h1><%= title %></h1>
<% if body.present? %>
<div>
<%= body %>
</div>
<% end %>
</article>
[!NOTE] The above should feel familiar to anyone who has partials (and strict locals) in the past. This is just regular partial syntax but with
slotsdefined instead oflocals(don't worry - you can still define locals too!).
When the partial is rendered, a special partial object is yielded as an argument to the block. Slot content is set by calling the appropriate #with_<slot_name> methods on this partial object.
For example, here our article partial is being rendered with content for the title and body slots that were defined above:
<%= render "article" do |partial| %>
<% partial.with_title "This is a title" %>
<% partial.with_body do %>
<p>You can use <%= tag.strong "markup" %> within slot content blocks without
having to worry about marking the output as <code>html_safe</code> later.</p>
<% end %>
<% end %>
[!NOTE] If you've ever used ViewComponent then the above code should also feel quite familiar to you - it's pretty much the same syntax used to provide content to component slots.
But this example just scratches the surface of what Slotify slots can do. Have a look at the more full-featured example below or jump to the usage information.
More full-featured example
```erb
<%# locals: (id:) -%>
<%# slots: (title: "Example title", lists: nil, quotes: nil, website_link:) -%>
<%= tag.section id: do %>
<%= title %>
Example link: <%= link_to website_link, data: "external-link" %>
<%= render lists, title: "Default title" %> <% if quotes.any? %>Quotes
<% quotes.each do |quote| %>> <%= quote %> <%== "— #tagtag.cite(quotetag.cite(quote.optionstag.cite(quote.options.citation)" if quote.options.citation.present? %><% end %> <% end %> <% end %> ``` ```erb <%# locals: (title:) -%> <%# slots: (items: nil) -%>
<%= title %>
<% if items.any? %> <%= tag.ul class: "list" do %> <%= content_tag :li, items, class: "list-item" %> <% end %> <% end %> ``` ```erb <%= render "example", id: "slotify-example" do |partial| %> <% partial.with_subtitle do %> This is the <%= tag.em "subtitle" %> <% end %> <% partial.with_website_link "example.com", "https://example.com", target: "_blank", data: "preview-link" %> <% partial.with_list do |list| %> <% list.with_item "first thing" %> <% list.with_item "second thing", class: "text-green-700" %> <% list.with_item "third thing" %> <% end %> <% partial.with_quote citation: "A. Person", class: "text-lg" do %>Lorem ipsum dolor sit amet consectetur adipisicing elit.
<% end %> <% partial.with_quote do %>Non quos explicabo eius hic quaerat laboriosam incidunt numquam.
<% end %> <% end %> ```Usage
Defining slots
Slots are defined using a strict locals-style magic comment at the top of the partial template. The slots: signature uses the same syntax as standard Ruby method signatures:
<%# slots: (title:, body: "No content available", author: nil) -%>
Required slots
Required slots are defined without a default value.
If no content is provided for a required slot then a StrictSlotsError exception will be raised.
<!-- _required.html.erb -->
<%# slots: (title:) -%>
<h1><%= title %></h1>
<%= render "required" do |partial| %>
<!-- ❌ raises an error, no content set for the `title` slot -->
<% end %>
Optional slots
If a default value is set then the slot becomes optional. If no content is provided when rendering the partial then the default value will be used instead.
<%# slots: (title: "Default title", author: nil) -%>
Using alongside strict locals
Strict locals can be defined in 'slotified' partial templates in the same way as usual,
either above or below the slots definition.
<!-- _article.html.erb -->
<%# locals: (title:) -%>
<%# slots: (body: "No content available") -%>
<article>
<h1><%= title %></h1>
<div><%= body %></div>
</article>
Locals are provided when rendering the partial in the usual way.
<%= render "article", title: "Article title here" do |partial| %>
<% partial.with_body do %>
<p>Body content here...</p>
<% end %>
<% end %>
Setting slot values
Content is passed into slots using dynamically generated partial#with_<slot_name> writer methods.
Content can be provided as either the first argument or as a block when calling these methods at render time. The following two examples are equivalent:
<%= render "example" do |partial| %>
<% partial.with_title "Title passed as argument" %>
<% end %>
<%= render "example" do |partial| %>
<% partial.with_title do %>
Title passed as block content
<% end %>
<% end %>
[!TIP] Block content is generally better suited for longer-form content containing HTML tags because it will not need to be marked as
html_safewhen used in the partial template.
The content will be available as a local variable in the partial template whichever way it is provided.
<%# slots: (title:) -%>
<h1><%= title %></h1>
Slot options
The slot value writer methods also accept optional arbitrary keyword arguments.
These can then be accessed in the partial template via the .options method on the slot variable.
<%= render "example" do |partial| %>
<% partial.with_title "The title", class: "color-hotpink", data: {controller: "fancy-title"} %>
<% end %>
<%# slots: (title:) -%>
<%= title.options.keys %> <!-- [:class, :data] -->
<%= title %> <!-- The title -->
Slot options can be useful for providing tag attributes when rendering slot content or rendering variants of a slot based on an option value.
When rendered as a string the options are passed through the Rails tag.attributes helper to generate an HTML tag attributes string:
<h1 <%= title.options %>><%= title %></h1>
<!-- <h1 class="color-hotpink" data-controller="fancy-title">The title</h1> -->
Slot types
There are two types of slots.
- Single-value slots can only be called once and return a single value.
- Multi-value slots can be called many times and return an array of values.
Single-value slots
Single-value slots are defined using a singular slot name:
<%# slots: (item: nil) -%>
Single-value slots can be called once (at most) and their corresponding template variable represents a single value:
<%= render "example" do |partial| %>
<% partial.with_item "Item one" %>
<% end %>
<%# slots: (item: nil) -%>
<div>
<%= item %> <!-- "Item one" -->
</div>
[!WARNING] Calling a single-value slot more than once when rendering a partial will raise an error:
<%= render "example" do |partial| %> <% partial.with_item "Item one" %> <% partial.with_item "Item two" %> # ❌ raises an error! <% end %>
Multi-value slots
Multi-value slots are defined using a plural slot name:
<%# slots: (items: nil) -%>
Multi-value slots can be called as many times as needed and their corresponding template variable represents an array of values.
The slot writer methods for multi-value slots use the singluar form of the slot name (e.g. #with_item for the items slot).
<%= render "example" do |partial| %>
<% partial.with_item "Item one" %>
<% partial.with_item "Item two" %>
<% partial.with_item "Item three" %>
<% end %>
<%# slots: (items: nil) -%>
<%= items %> <!-- ["Item one", "Item two", "Item three"] -->
<ul>
<% items.each do |item| %>
<li>
<% item %>
</li>
<% end %>
</ul>
Using slots with helpers
Docs coming soon...
<% partial.with_title "The title", class: "color-hotpink" %>
<% partial.with_website_link "Example website", "https://example.com", data: {controller: "external-link"} %>
<% partial.with_item "Item one" %>
<% partial.with_item "Item two", class: "highlight" %>
<%= content_tag :h1, title %> <!-- <h1 class="color-hotpink">The title</h1> -->
<%= content_tag :h1, title, class: "example-title" %> <!-- <h1 class="example-title color-hotpink">The title</h1> -->
<%= link_to website_link %> <!-- <a href="https://example.com" data-controller="external-link">Example website</a> -->
<%= content_tag :li, items %> <!-- <li>Item one</li><li class="highlight">Item two</li> -->
<%= content_tag :li, items, class: "item" %> <!-- <li class="item">Item one</li><li class="item highlight">Item two</li> -->
Rendering slots
Docs coming soon...
Slot values API
Singlular slot value variables in partial templates are actually instances of Slotity::Value.
These value objects are automatically stringified so in most cases you will not even be aware of this and they can just be treated as regular string variables.
<%= render "example" do |partial| %>
<% partial.with_title class: "color-hotpink" do %>
The title
<% end %>
<% end %>
<% title.is_a?(Slotify::Value) %> <!-- true -->
<% items.is_a?(Slotify::ValueCollection) %> <!-- true -->
<%= title %> <!-- "The title" -->
<% title.content %> <!-- "The title" -->
<% title.options %> <!-- { class: "color-hotpink" } (hash of any options provided when calling the `.with_title` slot value writer method) -->
<%= title.options %> <!-- "class='color-hotpink'" (string generated by passing the options hash through the Rails `tag.attributes` helper) -->
Plural slot value variables in partial templates are instances of the enumerable Slotify::ValueCollection class, with all items instances of Slotity::Value.
<%= render "example" do |partial| %>
<% partial.with_item "Item one" %>
<% partial.with_item "Item two", class: "current" %>
<% end %>
<% items.is_a?(Slotify::ValueCollection) %> <!-- true -->
<% items.each do |item| %>
<li <%= item.options %>><%= item %></li>
<% end %>
<!-- <li>Item one</li> <li class="current">Item two</li> -->
<%= items %> <!-- "Item one Item two" -->
Slotity::Value
The following methods are available on Slotity::Value instances:
.content
Returns the slot content string that was provided as the first argument or as the block when calling the slot writer method.
.options
Returns a Slotify::ValueOptions instance that can be treated like a Hash. Calling .slice or .except on this will return another Slotify::ValueOptions instance.
When converted to a string either explicitly (via .to_s) or implicitly (by outputting the value template using ERB <%= %> expression tags) the stringified value is generated by passing the options hash through the Rails tag.attributes helper.
.with_default_options(default_options)
Merges the options set when calling the slot value writer method with the default_options hash provided and returns a new Slotity::Value instance with the merged options set.
<% title_with_default_opts = title.with_default_options(class: "size-lg", aria: {level: 1}) %> <!-- apply default options -->
<% title_with_default_opts.options %> <!-- { class: "size-lg color-hotpink", aria: {level: 1} } -->
<%= title_with_default_opts.options %> <!-- "class='size-lg color-hotpink' aria-level='1'" -->
Slotify vs alternatives
nice_partials
Slotify was very much inspired by the Nice Partials gem and both provide similar functionality. However there are a number of key differences:
- Slotify requires the explicit definition of slots using 'strict locals'-style comments; Nice partials slots are implicitly defined when rendering the partial.
- Slotify slot values are available as local variables;
with Nice partials slot values are accessed via methods on the
partialvariable. - Slotify has the concept (and enforces the use) of single- vs. multi-value slots.
- Slotify slot content and options are transparently expanded and merged into defaults when using with helpers like
content_tagandlink_to. - Slotify slot values are
renderableobjects
You might choose slotify if you prefer a stricter, 'Rails-native'-feeling slots implementation, and Nice Partials if you want more render-time flexibility and a clearer separation of 'nice partial' functionality from ActionView-provided locals etc.
view_component
Both ViewComponent and Slotify provide a 'slots' API for content blocks.
Slotify's slot writer syntax (i.e. .with_<slot_name> methods) and the concept of single-value (renders_one) vs multi-value (renders_many) slots
are both modelled on ViewComponent's slots implementation.
However apart from that they are quite different. Slotify adds functionality to regular ActionView partials whereas ViewComponent provides a complete standalone component system.
Each ViewComponent has an associated class which can be used to extract and encapsulate view logic. Slotify doesn't have an analagous concept, any view-specific logic will by default live in the partial template (as per standard partial rendering patterns).
You might choose Slotify if you want a more 'component-y' API but you don't want the overhead or learning curve associated with a tool that sits somewhat adjacent to the standard Rails way of doing things. But if you have components with a lot of view logic or want a more formalised component format then ViewComponent is likely a better fit for your project.
Installation
Add the following to your Rails app Gemfile:
gem "slotify"
And then run bundle install. You are good to go!
Requirements
Rails 7.1+Ruby 3.1+
Testing
Slotify uses MiniTest for its test suite.
Appraisal is used in CI to test against a matrix of Ruby/Rails versions.
Run tests
bin/test
Benchmarks
Rendering performance benchmark tests for Slotify and a few alternatives (action_view, view_component & nice_partials) can be found in the /performance directory.
These benchmarks are a little crude right now!
Run benchmarks:
bin/benchmarks # run all benchmarks
bin/benchmarks slotify # run Slotify benchmarks only
Recent benchmark results
``` 🏁🏁 ACTION_VIEW 🏁🏁 ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23] Warming up -------------------------------------- no slots 13.120k i/100ms slots 11.038k i/100ms Calculating ------------------------------------- no slots 127.047k (± 5.2%) i/s (7.87 μs/i) - 1.273M in 10.051203s slots 106.400k (± 5.0%) i/s (9.40 μs/i) - 1.071M in 10.095061s Comparison: no slots: 127047.3 i/s slots: 106400.3 i/s - 1.19x slower 🏁🏁 NICE_PARTIALS 🏁🏁 ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23] Warming up -------------------------------------- no slots 11.451k i/100ms slots 3.889k i/100ms Calculating ------------------------------------- no slots 117.258k (± 3.3%) i/s (8.53 μs/i) - 1.179M in 10.070693s slots 40.737k (± 4.5%) i/s (24.55 μs/i) - 408.345k in 10.051584s Comparison: no slots: 117257.7 i/s slots: 40736.8 i/s - 2.88x slower 🏁🏁 VIEW_COMPONENT 🏁🏁 ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23] Warming up -------------------------------------- no slots 20.270k i/100ms slots 7.445k i/100ms Calculating ------------------------------------- no slots 211.571k (± 2.6%) i/s (4.73 μs/i) - 2.128M in 10.067334s slots 72.508k (± 5.2%) i/s (13.79 μs/i) - 729.610k in 10.096809s Comparison: no slots: 211570.7 i/s slots: 72508.0 i/s - 2.92x slower 🏁🏁 SLOTIFY 🏁🏁 ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23] Warming up -------------------------------------- no slots 12.051k i/100ms slots 2.710k i/100ms Calculating ------------------------------------- no slots 116.156k (± 5.4%) i/s (8.61 μs/i) - 1.169M in 10.102387s slots 26.454k (± 5.4%) i/s (37.80 μs/i) - 265.580k in 10.077285s Comparison: no slots: 116155.7 i/s slots: 26454.0 i/s - 4.39x slower ```Credits
Slotify was inspired by the excellent nice_partials gem as well as ViewComponent's slots implementation.
nice_partials provides very similar functionality to Slotify but takes a slightly different approach/style. So if you are not convinced by Slotify then definitely check it out!