Gem version CI status

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.0 release.

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 slots defined instead of locals (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_safe when 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 partial variable.
  • 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_tag and link_to.
  • Slotify slot values are renderable objects

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!