DEV Community

Roland Studer
Roland Studer

Posted on

More expressive APIs for View Components

View components offer two primary ways to interact with the component: passing arguments to the initializer and using slots:

render SomeComponent.new(some_params) do |component|
  component.with_some_slot(some_other_params) { "My slot content" }
end
Enter fullscreen mode Exit fullscreen mode

Having worked with Phlex (the better way to build views in Ruby), I came to appreciate calling methods directly on the component. While the slot API offers a lot and is versatile, it can sometimes feel like you have to work around it rather than with it.

Let's consider a BreadCrumbsComponent as an example. Traditionally, one might pass in all the data using an array of hashes, like this:

BreadCrumbsComponent.new([
  {label: "Home", url: root_path},
  {label: "Settings", url: settings_path},
  {label: "Notifications", url: settings_notifications_path}
])
Enter fullscreen mode Exit fullscreen mode

However, I prefer a more expressive API like this:

BreadCrumbsComponent.new do |bread|
  bread.crumb "Home", root_path
  bread.crumb "Settings", settings_path
  bread.crumb "Notifications", settings_notifications_path
end
Enter fullscreen mode Exit fullscreen mode

Solution with lambda slots

To achieve the desired API using the ViewComponent gem, we can utilize lambda slots:

class BreadCrumbsComponent < ViewComponent::Base
  renders_many :crumbs, -> (label, url) { @paths << {label: label, url: url} }
end
Enter fullscreen mode Exit fullscreen mode

With a lambda slot, you can create a similar API, though it forces you to use specific method naming, as you will need to call c.with_crumb("Home", root_path) in the template.

BreadCrumbsComponent.new do |c| 
  c.with_crumb "Home", root_path
end
Enter fullscreen mode Exit fullscreen mode

However, there is an issue here. When iterating through the @paths supposedly set by the slot calls, you will notice that it doesn't work as expected. The with_crumb method did not have any effect.
Why? ViewComponent only calls the block if you make a call to the accessor method for those slots, in this case crumbs, which is what you do when in your template you are calling crumbs.each …. This means that even if you don't render the slots, you still need to call the accessor method for it, a hint that we are somewhat abusing slots in this scenario.

Better solution

My preferred approach would be to directly call an instance method on the component, like this:

class BreadCrumbsComponent < ViewComponent::Base
  def crumb(label, url)
    @paths << {label: label, url: url}
  end
end
Enter fullscreen mode Exit fullscreen mode

Now this should work seamlessly:

BreadCrumbsComponent.new do |bread|
  bread.crumb "Home", root_path
end
Enter fullscreen mode Exit fullscreen mode

However the same caveat applied. The block is not called.

To address this issue, I explored how I could call the block myself.
The block is stored
in @__vc_render_in_block, a very private looking instance variable.

Then I realized that a call to content would invoke the block.
The solution is to add a before_render lifecycle method.
This calls the block and like this the crumb method calls on the component will be performed as expected. So our component ruby file can look like this:

class BreadcrumbsComponent < ViewComponent::Base
  attr_reader :paths

  def before_render
    content # ensures that block is called
  end

  def initialize(&block)
    @paths = []
  end

  def crumb(label, url)
    @paths << {label: label, url: url}
  end
end
Enter fullscreen mode Exit fullscreen mode

For further use, it would be a good idea to extract this into a module like ViewComponent::CallBlock (or a better named module), and to then simply include it.

While some may argue that passing this data directly might be a cleaner approach since it is essentially data, I wanted to explore if I could achieve this particular API with a view component.

In conclusion: With a small tweak you can, directly calling instance methods on the component to shape your component. This way you can offer a very expressive and straightforward API.

Originally posted here

Top comments (1)

Collapse
 
megatux profile image
Cristian Molina

Thanks! I like Phlex & want to start testing it so I found your blog.
I think there is an issue with the phlex homepage link in the article.
Regards!