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