Builder Pattern
Published: 2023-10-15 16:38:21

Some years ago I wrote `page_for` a UI generation library that lets you do stuff like

```slim
= 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://rstuder.ch/2023-supercharged-table-component/

* https://stanko.io/vanilla-rails-view-components-with-partials-41zctBGbN9ka

* ApplicationComponent shipped with Jump Start Pro has some good ideas about no dependency components.

```erb
<%= 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:

```slim
= 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.

```ruby
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 call `render TabsBuilder.new`.  Behind the scenes, rails calls `render_in` on that new object and passes the `view_context`.

* Some conveniences are added so we can find the partial by `convention`

* I redacted the before_filter functionality for brevity.

```ruby
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.

```slim
- 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.