dramatic sunset photo of Portland's famed Fremont Bridge

Bridgetown

Cards

Render Resource Components Conditionally

Tagged: resources ERB Serbea

Added by: @jaredcwhite @jaredcwhite

Tested on: Bridgetown v1.0

Note: this technique requires that you use ERB or Serbea as your template language. Here’s information on how to use Ruby components in Brigdgetown.

Let’s first look at the scenario where you’d like to display a resource differently depending on the category it’s in. The naïve way to do it would be to add a conditional to your template directly and render different components accordingly:

<!-- ERB -->
<% if resource.data.category == "category1" %>
  <%= render Category1Resource.new(resource: resource) %>
<% elsif resource.data.category == "category2" %>
  <%= render Category2Resource.new(resource: resource) %>
<% end %>
<!-- Serbea -->
{% if resource.data.category == "category1" %}
  {%@ Category1Resource resource: resource %}
{% elsif resource.data.category == "category2" %}
  {%@ Category2Resource resource: resource %}
{% end %}

Obviously that gets pretty messy pretty fast, and while you could wrap it up in its own partial to avoid duplication, it’s not “elegant”. There’s also the conundrum where each component might want to make use of reusable logic, and if they’re all one-off components, that’s not easily feasible. Let’s instead look at a more maintainable solution.

Creating Your Base Component

What we need is a base class which is responsible for providing the relevant subclass for a particular type of resource. Any shared logic between resource types can be placed here in the base class. It might look something like this:

# src/_components/resources/base_component.rb

module Resources
  class BaseComponent < Bridgetown::Component
    attr_reader :resource, :h_level, :show_thumbnail

    class << self
      def component_for_resource(resource)
        case resource.data.category
        when "articles"
          ArticleComponent
        when "pictures"
          PictureComponent
        when "links"
          LinkComponent
        when "thoughts"
          ThoughtComponent
        when "videos"
          VideoComponent
        end
      end
    end

    def initialize(resource:, h_level: :h2, show_thumbnail: false)
      @resource, @h_level, @show_thumbnail = resource, h_level, show_thumbnail
    end
  end
end

This is copied verbatim from my own site codebase. What’s happening here is we define a class method called component_for_resource. It examines the resource’s category and returns the relevant component class. If you wanted to look at collections, instead of categories, you could write case resource.collection.label.

This class also defines an initialize method that’s common across all subclasses. Since the code in a template would instantiate any of the subclasses in the same manner, there’s no opportunity for a subclass to define its own unique initializer.

Now let’s look at one of the subclasses:

# src/_components/resources/article_component.rb

module Resources
  class ArticleComponent < BaseComponent
  end
end

Whoa, that’s it?! Yes, for the most part all your subclasses won’t do anything special on the Ruby side. On the component template side, of course they’ll all be different. (You’d have article_component.(erb|serb), link_component.(erb|serb), etc.) However, in cases where you want commonality between the various component templates, they in turn can render a shared component! For example, I also have a PublishedDateComponent which renders out the date when the resource was published, and that component is rendered out by the article template, the picture template, the thought template, etc.

Rendering the Right Component via the Base Class

Now that you have your base class and subclasses set up, here’s how you render the relevant component for a resource:

<!-- ERB -->
<%= render Resources::BaseComponent.component_for_resource(resource).new(resource: resource) %>

<!-- or in another setting: -->
<%= render Resources::BaseComponent.component_for_resource(resource).new(resource: resource, h_level: :h3, show_thumbnail: true) %>
<!-- Serbea -->
{%@ Resources::BaseComponent.component_for_resource(resource) resource: resource %}

<!-- or in another setting: -->
{%@ Resources::BaseComponent.component_for_resource(resource) resource: resource, h_level: :h3, show_thumbnail: true %}

The first part of the method call is Resources::BaseComponent.component_for_resource(resource). This passes the resource to the component_for_resource method which you saw defined above, and the return value is a specific component class. That component class in turn is instantiated with the resource, and possibly other optional keyword arguments.

One potential gotcha to be aware of if there’s no matching category or collection, the above code as written would raise an error since the new method would be called on nil. You could fix this by adding a UnknownComponent subclass and returning that in an else clause within the case statement. Then you’d just need to figure out what UnknownComponent renders in its template. An exercise left for the reader!

Return to Cards