Building view components in Middleman


This site is styled using the wonderful Terminal CSS which has multiple components included. One feature of Javascript frameworks that I like is this concept of components.

The HTML, JavaScript and CSS live together rather than being spread about in different folders in the code base. The ViewComponent framework has more recently been introduced to Rails which achieves this and, along with things like Hotwire, are improving the front end experience for developers.

View components are not something that exist for the Middleman static site generator but it's something I fancied trying to implement on my site which is built with Middleman. I wanted something that could be configured once and continue to work when adding new Ruby code, JavaScript and CSS.

In terms of creating the HTML, there were a few approaches that could be taken. Partials for example, are a well used pattern for reusing similar code and locals can be passed to tailor their content and appearance. Partials however would really need to live in their own folder in order to work with Middleman so not really suitable for the goal of grouping the code with JavaScript and CSS. Partials are also less performant so I would rather avoid their use for components which will be called extensively on every page.

Helpers in Middleman are another option but again there are expectations in Middleman about where the code should sit and it is generally within a helpers folder at the root of the project. However, helpers can also sit within Middleman extensions where you can add a helpers block. The extensions are good because they can sit anywhere within the code base and simply have to be required and activated in Middleman so this looked like a good area to experiment on.

So, first things first, load and activate. I didn't want to manually require a new extension every time I wanted a new component so a neater solution was to keep them under a folder (a folder called components in this case) and load all extensions using a glob pattern from config.rb:

Dir["./components/**/*.rb"].each { |file| load file }

and then activate all components by following a naming convention:

Pathname.new("./components").children.each do |entry|
  return unless entry.directory?
  activate "#{entry.basename.to_s}_component".to_sym
end

where each folder for a component's code would have the same name as the extension followed by _component.

My site has a timeline component for example which sits at components/timeline/timeline_component.rb and this is a Middleman extension.

This is a simple example of a component which looks as follows:

module Components
  module Timeline
    class TimelineComponent < Middleman::Extension
      helpers do
        def timeline(&block)
          content_tag(:div, nil, class: "terminal-timeline", &block)
        end
      end
    end
  end
end

::Middleman::Extensions.register(:timeline_component, Components::Timeline::TimelineComponent)

The HTML is created using Padrino tag helpers which are a standard part of Middleman. You can see more of my components in my repo. These components are easy to call from erb view files like so:

<% timeline do %>
    ...
<% end %>

and generates the following HTML:

<div class="terminal-timeline">
    ...
</div>

This example is very simple and doesn't remove too much code from the erb view file but many of the components are more complicated and have many levels of nested HTML. Addtional helpers could also be added if there are variations in the types of timelines and any additional code logic for timelines can be grouped together in this extension. The components simplify things greatly and changes can easily be made from the one file to automatically update all the occurrences of that component across the site.

As mentioned earlier, I also want the JS and CSS to live in the same component folder which can easily be achieved when using Webpack as my module bundler. For my entries I can once again use the glob pattern:

const glob = require("glob");
const path = require("path");

...

entry: {
    ...
    components: glob.sync(path.resolve(__dirname, "./components/**/*.js")),
    components_css: glob.sync(path.resolve(__dirname, "./components/**/*.scss")),
    ...
},

which bundles files of components.js and components_css.css from all of the files ending in .js and .scss respectively in the components folder.

This is a simple solution for my codebase at the moment. If the number of components grew to the point where the JS and CSS bundles became very large then I would need to look at ways of splitting them up but this isn't an issue at the moment.

Benchmark

I mentioned previously about partials being less performant and I thought it would be interesting to perform some benchmark testing against a simple button component I have which renders HTML something like

<button class="btn btn-default">Default</button>

Below are benchmark tests agsinst rendering this HTML via a partial in comparison to my component.

user system total real
100 times
Partial 0.086290 0.000340 0.086630 (0.086687)
Component 0.001114 0.000043 0.001157 (0.001114)
500 times
Partial 0.443698 0.004322 0.448020 (0.449804)
Component 0.005451 0.000066 0.005517 (0.005542)
1000 times
Partial 0.878199 0.005667 0.883866 (0.886939)
Component 0.011255 0.000210 0.011465 (0.011431)
5000 times
Partial 4.211261 0.015211 4.226472 (4.229765)
Component 0.053359 0.000282 0.053641 (0.053803)
10000 times
Partial 8.217367 0.022930 8.240297 (8.238036)
Component 0.103162 0.000494 0.103656 (0.103661)
25000 times
Partial 20.676865 0.061173 20.738038 (20.732092)
Component 0.255417 0.000819 0.256236 (0.256320)
50000 times
Partial 41.146729 0.126237 41.272966 (41.263908)
Component 0.493842 0.001792 0.495634 (0.495767)

As you can see, the performance starts to become more noticeable on a larger site. Take the last example of 50000 calls. Say you have 10 components per page and therefore 5000 pages which is a reasonably large static site. The component approach using the above benchmarks will save in the region of 41 seconds in the build time which is a nice saving.

This test was on my laptop which has 4 cores and 16GB of memory. Consider a Netlify build on their standard machine which utilises up to 2 cores and 6GB of memory and you'll see even greater time savings than demonstrated above.

I'm very happy with how this is working and hopefully this provides inspiration to others. Please feel free to look at my repo to dig into the code.