Zane Bliss
Delegate functionality with Object-backed View Components
Jun 18, 2023Rendering server-side UI components in Ruby on Rails can be made easier with an open-source gem called View Component. Here is the description of View Component, from the documentation.
A framework for creating reusable, testable & encapsulated view components, built to integrate seamlessly with Ruby on Rails.
A View Component is composed of a Ruby class, a template (in .html.erb
), and a call to render the component in a Rails view. Below is a sample implementation from View Componentās documentation.
Component class
# app/components/message_component.rb
class MessageComponent < ViewComponent::Base
def initialize(name:)
@name = name
end
end
Component markup
<%# app/components/message_component.html.erb %>
<h1>Hello, <%= @name %>!</h1>
Instantiated and passed to render
<%# app/views/demo/index.html.erb %>
<%= render(MessageComponent.new(name: "World")) %>
Produces the following markup
<h1>Hello, World!</h1>
Conventional usage of View Components
In the Ruby on Rails codebases that Iāve worked on, Iāve had the opportunity View Components extensively. There are typically two use cases of View Components Iāve observed. First is a more generic component which is primarily defined by generic attributes passed into it that are not related to some unique business object. Examples include generic buttons, pagination components, or links.
The second use case of View Components is defined primarily by an object that has a relationship to the domain and is more complex and is conceptually coupled. This could be something like an event or an āAbout meā card. These typically have several related attributes and may already be represented by a domain object.
Although View Components have many benefits, there are sometimes cases where following the conventional approach in View Componentās documentation becomes unwieldy. Especially in cases like the second, where there is a component that is representing an object with several related attributes. Here is a contrived example of an event component.
Component class
# app/components/event_component.rb
class EventComponent < ViewComponent::Base
def initialize(name:, date:, address:, spots_left:, ticket_cost:, organizer_email:)
@name = name
@date = date
@address = address
@spots_left = spots_left
@ticket_cost = ticket_cost
@organizer_email = organizer_email
end
end
Component markup
<%# app/components/event_component.html.erb %>
<div>
<h1><%= @name %></h1>
<p><%= @date %></p>
<p><%= @address %></p>
<p><%= @spots_left %></p>
<p><%= @ticket_cost %></p>
<p><%= @organizer_email %></p>
</div>
Instantiated and passed to render
<%# app/views/demo/index.html.erb %>
<%=
render(
EventComponent.new(
name: "Sydney.rb",
date: 2.weeks.from_now,
address: "P. Sherman 42 Wallaby Way",
spots_left: 12,
ticket_cost: "$42",
organizer_email: "pixar@nemo.com"
)
)
%>
Even though this is a contrived example, in reality, Iāve seen cases where the attributes or methods of an object were being accessed and called to populate the attributes of a related View Component.
<%# app/views/demo/index.html.erb %>
<%=
render(
EventComponent.new(
name: event.name,
date: event.date,
address: event.address,
spots_left: event.spots_left,
ticket_cost: event.ticket_cost,
organizer_email: event.organizer_email
)
)
%>
Unfortunately, this can beg the question: āWhere does the presentational code liveā? In the object? In the View Component? In the markdown? In a module? Inevitably some of the data in the object needs to be capitalized or turned into currency, or pretty printed, etc. This can lead to code in View Components that is more related to the object being presented then the View Component itself.
At face value, this doesnāt seem like much of an issue, but it can lead to awkward separation of concerns, strange method naming, and annoying testing, among other things. The View Component should be concerned about rendering a component, not about the presentational concerns of the object it is representing.
As an alternative to this frustration, consider using Object-backed View Components!
The pattern
When you have a UI component that is conceptually related to a Ruby object, instead of passing in several attributes to the View Component constructor, consider passing in the entire object, and delegating method calls using ActiveSupport delegate
(source).
Following the example from before, suppose you have an event you need to render. Instead of putting the presentational logic in the View Component, instead keep it in the object itself (as an alternative, you could also decorate the object using the decorator pattern).
# app/models/event.rb
class Event
attr_reader :name, :date, :address, :organizer_email
def initialize(name:, date:, address:, organizer_email:)
@name = name
@date = date
@address = address
@organizer_email = organizer_email
end
def human_readable_date
# presentational logic
end
def spots_left
# some api call
end
def ticket_cost
# some api call
end
# ... some presentational methods
end
Instead of passing in each of the event attributes, instead, pass in the event instance.
<%# app/views/demo/index.html.erb %>
<%= render(EventComponent.new(event: event)) %>
Then in the View Component class delegate method calls to the event
object.
# app/components/event_component.rb
class EventComponent < ViewComponent::Base
delegate :name,
:date,
:address,
:organizer_email,
:human_readable_date,
:spots_left,
:ticket_cost
to: :@event
def initialize(event:)
@event = event
end
end
Then in the view code, call methods like so.
<%# app/components/event_component.html.erb %>
<div>
<h1><%= name %></h1>
<p><%= date %></p>
<p><%= address %></p>
<p><%= spots_left %></p>
<p><%= ticket_cost %></p>
<p><%= organizer_email %></p>
</div>
Now you have a clearer separation of concerns, unit testing the component will be much easier (since now you can use a dummy object in your component test), and it is easier to organize your presentational code that relates to the object youāre rendering.
Conclusion
Carefully consider when is appropriate to use this pattern. It kind of goes against some of the standard approaches demonstrated in the View Component documentation. Do you need a custom component that is conceptually coupled to a domain object? Consider using it. Do you need a generic component with a small number of attributes? This might be overkill.
Thanks for reading. Please reach out to me at zanebliss@icloud.com if you have questions or comments! Iād love to chat.