Feb 1, 2023

Using view_component for Design System Implementation in Rails Projects

Using view_component for Design System Implementation in Rails Projects

A design system is a tool to simplify design creation as well as software development. Its creation for the Showmax platform was a true challenge and you can read more about it in Showmax Design System: The Precious. Our Precious introduces the roots of all the designs, including logos, icons, or foundations like colours, spacing, borders, and more. The design system also states which elements arise from the roots. And for each of the Showmax platforms we have a specific library including elements like buttons, containers, status messages, input fields, and others. Anybody creating a new design or developing it in the code can easily find the right element in the library and focus on the problem instead of redesigning the basics from scratch. To put the Design System into practice, we had to find a way to transfer the libraries from Figma to coders' screens.

Implementation in the Backend-Alpha team 

In my team (that is focused on user acquisition) we maintain and build upon those crucial first steps of a Showmax user's journey. Our team's jurisdiction is over what we call "the Landing Pages" and "the Secure Pages". The Landing Pages are a huge array of static HTML pages. They all link the user to our authorization service and eventually the authorization service links them to the payments journey. We refer to the auth and payments journey as "the Secure Pages".

In the marketing pages, the tech stack consists of the NextJS framework (a React based framework that enables server-side rendering and the generation of static websites). The secure pages are several standalone Ruby on Rails applications.

We knew from the very beginning that no matter what the design system's code would be written in, both the Marketing pages and the Secure pages would be using identical CSS. We needed to find a clever way to share this style code. The obstacle was the significant difference in environments between a modern next-js app written in typescript, bundled with Webpack and a few several-years-old Rails apps running Sprockets to bundle their assets.

About UI components

Before I get into the code sharing, let's quickly talk about UI components and what form we think they are most effective in.

I would define UI components as simple functions or classes that spit out styled html. Nothing more. They should follow the single responsibility principle and be void of any business logic. Essentially they should be "dumb" and they shouldn't do much with their input other than displaying it however you tell them to display it i.e with various CSS class modifiers or simple arguments like “dark” or “light” to indicate what style the component should be.

If you're familiar with some of the React/Redux design patterns there is a concept of "container components" and "presentation components". Container components are all rendered higher in the state tree and are therefore closer to the data source. They make decisions about what will be rendered (some text string for example) while the presentation components will take that text string no matter what the content is and style it in the way that they're told to. If they take any arguments on initialization the arguments should only be about style, nothing else.

We were really only concerned with presentation components in this project and a vanilla Rails project leaves a lot to be desired in this department. If you want to stick with the default libraries you can split your views up into partials for instance, but partials don’t have a method signature so we can’t just glance at the file and know exactly what data is required to render a partial correctly. Furthermore, partials and views know too much about the controllers that render them. Because they have access to the controllers’ instance variables it's tempting to access this data in the partial. If you are accessing an instance variable in a partial, you tend to lose the reusability. The partial is now coupled to this controller action. If you want to reuse it, you need to make sure that same instance variable exists wherever else you need the partial.

In my opinion, the best way to easily add UI components to a Rails project without worrying about javascript is ViewComponent. It's fairly simple and easy to wrap your head around. You don’t need to think about “global state” or “redux” or  “hooks”. In the end you get a ViewComponent::Base class that simply allows you to write a ruby class that renders html.

Container components and higher-level business logic would be implemented as the UI Components would start to become used in production, with more context.

So we left them as open and flexible as possible.

For example, here is our Ruby CheckBox component:

# ruby class description
module BeDesignSystem
  class CheckboxComponent < ViewComponent::Base

    def initialize(label_text: nil, id: nil, name: nil, value: nil, checked: false, html_options: {})
      @id = id
      @name = name
      @value = value
      @label_text = label_text
      @checked = checked
      @html_options = html_options.merge({ id: @id, class: 'smFormCheck_input' })

It's accompanying template:

<% # the erb template for BeDesignSystem::CheckboxComponent %>
<label class="smFormCheck">
  <%= check_box_tag @name, @value, @checked, @html_options %>
  <span class="smFormCheck_label"><%= @label_text %></span>

And the related React component for the landing pages:

type CheckboxProps = {
  text: string;
  id?: string;

const Checkbox = ({ text, id }: CheckboxProps): JSX.Element => (
  <label htmlFor={id} className={styles.smFormCheck}>
    <input className={styles.smFormCheck_input} id={id} type="checkbox" />
    <span className={styles.smFormCheck_label}>{text}</span>

export default Checkbox;

The important thing is that you can call these with whatever arguments you want but in the end the html structure is the same in both environments:

<label for="check-id" class="smFormCheck">
    <input type="checkbox" name="name" id="check-id" value="value" class="smFormCheck_input" />
    <span class="smFormCheck_label">First checkbox</span>

So as a team of two, one working in React, the other in view_component, we would be able to share styles as long as we kept the html structured the same and used classes only as CSS selectors.

Which was easy to do for us as we already use the BEM model for our CSS. https://getbem.com/introduction/

The challenging part – Rails apps with Sprockets

If you're familiar with Rails, you might wonder why sharing CSS between Rails and any other web based framework would be worth writing a blog about. They both speak the universal language of Javascript and you can easily bundle CSS using a bundler like webpack! But this wasn't so easy for us because our Rails apps we were working with Sprockets. Sprockets is old fashioned and handles everything much differently than webpacker. Without webpacker we had no package.json or any way to consume npm modules in the Rails apps. 

So we couldn't create an npm module that would just allow the marketing pages and the secure pages to

import styles from "be_design_system/form_check.module.scss";

We needed a way to do that and feed the styles to Sprockets.

2 for the price of 1

In one repo we wrote an npm package and a Ruby gem. We separated the ruby and javascript code at the top level into separate directories.

- be_design_system
- src
- app

be_design_system is where we had our SCSS files

app is where our gem code is. It's a Rails engine, which helped us tremendously as Rails engines can contain a miniature sub Rails app in their test directory. This gives us the ability to run another engine called Lookbook (more on this later) where we can prototype, document, and show off our new UI components in real time as we develop them.

src is the Javascript.

In the Ruby engine I was able to import the SCSS and process the files by sprockets:

config.assets.paths   << File.expand_path('../../be_design_system', __dir__)

Sprockets can then just be used like this to include our assets:

*= require be_design_system/form_check

Using git LFS in our gemspec

spec.files         = `git ls-files -- '*.rb' '*.scss' '*.erb'`.split("\n")

We can exclude any non-npm packages from our final gem build. It was an unconventional way to cut out code duplication. While it did cause a mild headache in the beginning due to sprockets not being out of the box configured to consume SCSS module files i.e. “button.module.scss” The end result is quite nice. We have all our design system code relating to our team in one place and when we need to jump into the different worlds (e.g. TypeScript vs Ruby), we have this one place that helps make the context switching a little easier.


Now that we have made these UI components available to any of our Rails apps, they are still useful to anyone other than me and my teammate who built this with me. I know what they all look like but how does another team member really know if "Primary Button" will work for them in their project?


Lookbook started out as a pet project of Mark Perkins. If you are familiar with a component gallery called Story Book, this is very similar but much simpler.

When you create a new ViewComponent class you have the option to generate a "preview" file. These are just erb|slim|haml templates where we can render our components with test data. You can even set up a controller class to serve up these files with some data fetched over the http, for instance, or you can just use the controller to parse some fixture files to hydrate your preview classes.

What's so cool about lookbook is how little effort is required to get it working. Even for someone working without webpack. It doesn't need any fancy javascript (it works with or without js precompilers).

To get it up and running you only need to install the gem and navigate to localhost:3000/lookbook. If you already have some preview files, they will be there in all their glory:

Button previews

You might have noticed that there is a source code button. There you can see the ruby / template that is rendering this preview. When you make changes to this ruby file, lookbook has a websockets connection setup that refetches this page (lookbook renders your component in an Iframe and that Iframe is dynamically refreshed).

What I find even more helpful is the html view. It's extremely handy to have a quick way of flipping back and forth between html and rendered html without all the clutter of chrome dev tools. Many of our components render various data-something attributes for data transfer between the server and the client. This allows you to quickly test that your changes are working properly and debug any weirdness.

Post image

So we have components, we have a way to share them and so far we've done all of this with sprockets as our js transpiler. What more could we want?



As you can see above we have blocks of code, a summary, and a little window where the button is rendered so you can see everything you need to render this component. Lookbook renders these by parsing button.md.erb so it's an erb-ified markdown doc. Enhancing this beyond just a basic markdown document actually gives you a ton of power. For instance, each one of these files (called a page) has a page, next_page, previous_page, and pages variable. This allows you to intricately reference and interlink them. You can use ruby to dynamically write docs.

<%= 10.times do %>
  something repetitive
<% end =%>

So I hope you got something out of this article. If nothing else, just the curiosity to think about adding a design system to your project. Be sure to checkout Lookbook and ViewComponent

Share article via: