Some years ago I wrote page_for
a UI generation library that lets you do stuff like
= table_for @products do |t|
- t.col :name
- t.col :price
- t.col :popularity do |product|
= product.likes.count
While I've loved this library for years, it's a bit outdated
Uses bootstrap instead of tailwind
Uses jquery instead of Turbo 8
I'm going to use this blog post to collect my research on how I could approach this problem again, but possibly using components instead of helpers and partials.
Research
https://stanko.io/vanilla-rails-view-components-with-partials-41zctBGbN9ka
ApplicationComponent shipped with Jump Start Pro has some good ideas about no dependency components.
<%= render TabsComponent.new do |component| %>
<% component.capture_for :tabs, "Tab 1" %>
<% component.capture_for :tabs, "Tab 2" %>
<% component.capture_for :tabs, "Tab 3" %>
<% component.capture_for :panels do %>
Tab panel 1
<% end %>
<% component.capture_for :panels do %>
Tab panel 2
<% end %>
<% component.capture_for :panels do %>
Tab panel 3
<% end %>
<% end %>
<% end %>
Results
I ended up going with no dependencies like ViewComponents. Instead, I took inspiration from JumpStart Pro's ideas.
Here is a tab component used in a slim view:
= render TabsBuilder.new do |t|
- t.tab "Tab 1" do
| Hello
- t.tab "Tab 2" do
| World
Here is the meat of the TabsBuilder Component. I call them Builders instead of Components.
class TabsBuilder < ApplicationBuilder
attr_accessor :tabs
before_filter :set_selected
def initialize
@tabs = []
end
def tab(title, &block)
tab_key = key_for(title)
@tabs << OpenStruct.new(
title: title,
content: lambda { capture(&block) },
tab_key: tab_key,
selected: false,
query_params: query_params(tab_key)
)
end
def set_selected
@tabs.each do |tab|
tab.selected = tab.tab_key == selected_key
end
end
# ...
Here is the full ApplicationBuilder base class.
We get the precious
view_context
when in the view above we callrender TabsBuilder.new
. Behind the scenes, rails callsrender_in
on that new object and passes theview_context
.Some conveniences are added so we can find the partial by
convention
I redacted the before_filter functionality for brevity.
class ApplicationBuilder
attr_accessor :view_context
# Triggered by Rails' render call
def render_in(view_context, &block)
@@before_filters.each { |method| send(method) }
@view_context = view_context
block.call(self)
render
end
def capture(&block)
@view_context.capture(&block)
end
def render
@view_context.render partial_path, builder: self
end
def partial_path
"builders/#{builder_path}"
end
def builder_path
self.class.name.delete_suffix("Builder").underscore
end
end
And finally here is the partial in /view/builders/_tabs.html.slim
I
- Styling removed for brevity.
- builder.tabs.each do |tab|
=link_to tab.title, tab.query_params
= builder.selected_tab.content.call
I like how I was able to defer rendering the content of a tab until it's known that it was selected. Lazy.
Top comments (0)