Stencil, Storybook, Typescript

Embarking on my first major UI/UX project in a few years, I decided to take a few spikes into our first few sprints to get reacquainted with the Frontend Development Ecosystem. Frontend development, amongst all of the disciplines, has a reputation for moving fast, and I myself have been burned by the links of Angular 2 through 6 upgrades. In the years since I was focusing on developing user experiences, a lot has happened, new tools have overtaken old ones, and new patterns have emerged.

During my research spikes, meant to inform the technology decisions that would need to be made before breaking ground, I found 2 technologies that were net new to me and that rather impressed me: Stencil and Storybook.

Stencil, according to their website, is a toolchain for building reusable, scalable Design Systems. It is a compiler for Web Components, which I have used in the past, but have always relied on heavier libraries like Polymer to do the heavy lifting. Since my time with Polymer, the standards around web components have changed and native browser support has become much more commonplace. The result is the ability to create web components, one of the downsides is the API to do it natively is sometimes complex and not the cleanest. Stencil fixes this gap, by giving you a way of declaring your component API in typescript, and it compiles into the different ways of distributing web components.

The truly impressive thing is the framework integrations. Building as set of native web-components is nice, but being able to reuse them across frameworks like Angular and React is impressive. Back at Comcast, working in Polymer on some projects and AngularJS on others, we never dreamed of interop (partially due to browser support). But the new native web components are “just html”, which means anything that can control the DOM can interop with Stencil Components.

With Elm being all but decided on for the new project, the ability to develop styled web components that I can just use in an elm application without having to worry about styling came as a major benefit. I love Elm. It is amazing at many things. But I recently tried out Elm-CSS and was happy with how it worked on the project I used it on, but seriously doubted the scalability of it to the side of an entire application. In the past I have used SASS to build component systems and elm to laydown the styles, but there is type checking or ability to make sure the right class is applied. I have tried Elm-UI, and I think it is an amazing project, but I dont think it is production ready yet. The ability to encapsulate all of the styles and behaviors of a reusable component and just use them at the elm layer is incredibly exciting to me.

So once you have a nice library of reusable, styled, self-contained components, how do you make sure you aren’t recreating the wheel, and how do you share them with the design team and other developers to make sure everyone is speaking the same language; after all one of the purposes of building good design systems is to create a common lexicon or vocabulary between developers, designers, and users.

Enter Storybook. Storybook is a tool for developing UI Components in isolation and including documentation and examples to allow for developers to clearly communicate with each other and designers. Currently it has strong integrations with all of the major Javascript Frameworks like React, Angular, and Vue; it even has some of the more obscure ones like Svelte and Mithiril. It really provides an awesome and intuitive interface for exploring a teams Visual Vocabulary.

The drawback with Storybook is also the impetus for this post, it doesnt have a first class integration with Stencil; yet. I have seen a few guides online for combining the to using the @storybook/html preset; which makes sense, since Stencil just produces self contained HTML web components. However a lot of the guides made very little use of the storybook features and I felt there was something missing, so I started digging into their codebases to see how I could integrate them a little better. One of my goals was to completely the use of Typescript during development. Having done a number of Typescript projects and obviously being obsessed with Elm, I find it hard not to develop with at least type hints, and developing without any type safety in mind makes me feel dirty. So here is my findings on integrating Storybook with Stencil to maximize the use of Typescript.

Integrating Storybook with Stencil using Typescript,

We are going to start with a sample project to build a component library with stencil and storybook.

1
$ npm init stencil

This command uses the create-* trick in NPM which I will be covering in a different post. Essentially, it is building a structured folder for stencil based off of a template and some questions you have to answer on the command line. For this example we are going with the component project type and naming the project sample-project.

This creates all of the structure we need and even installs all of the necessary pieces. Lets enter the repo.

1
$ cd sample-project

This is the initial stencil setup for component libraries. It created a starter component for us at src/componennts/my-component.

Now to add storybook.

1
$ npx -p @storybook/cli sb init --type html

This uses the npx command which allows you to run node code without installing it first. Here we are using the storybook cli to initialize a storybook with the html preset. This command will create a stories folder and a.storybook folder that houses the default story and the storybook config respectively. It also install the necessary packages in your package.json.

While we are installing things, we know we want to write storybook using typescript, but since we are already using babel for both stencil and storybook, we should use the babel typescript compiler instead of the native one. We can do this by adding the @babel/preset-typescript package:

1
$ npm install @babel/preset-typescript --save-dev

The next part is to update the stories to be colocated with the components, so instead of in /stories they will be included with the component. It is always a good idea for documentation to live close to code; it prevents docs from getting out of date or worse, lying to new developers.

First we can remove the /stories directory since we wont be using it.

1
$ rm -rf stories

Now we update the .storybook/main.js to look in the next directory and to look for ts or tsx files and compile them:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
stories: ['../src/**/*.stories.tsx'],
webpackFinal: async config => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve('babel-loader'),
});
config.resolve.extensions.push('.ts', '.tsx');
return config;
},
};

At this point we have storybook looking for tsx files in the same directories we develop them in, but we still need to load our component library into the browser when storybook starts. We can do this by hooking into a few of the lifecycle parts of storybook like .storybook/preview-head.html. This file gets loaded inside of storybook, which allows us to use our stencil components inline.

1
2
<script type="module" src="./sample-project/sample-project.esm.js"></script>
<script nomodule src="./sample-project/sample-project.js"></script>

This uses the javascript module system with a default fallback. This allows the browser to load what it needs when it needs it if it can, or download the whole thing if it can’t.

Now we need to give the storybook access to these files (which are in /dist after building stencil). The storybook commands take a -s flag to add static files to the storybook build. Here we will update our package.json to add these flags to the run commands.

1
2
3
4
5
"scripts": {
...
"storybook": "start-storybook -p 6006 -s ./dist",
"build-storybook": "build-storybook -s ./dist"
}

At this point we are ready to build our first story. Using the provided component, we can create src/components/my-component/my-component.stories.tsx with the following contents.

1
2
3
4
5
export default {
title: 'MyComponent'
}

export const Basic = () => `<my-component first="stencil" middle="storybook" last="typescript"></my-component>`;

The default export sets the group for my component, and the rest of the exports are created as stories in that group. Here we can put different configuration options of our component on display.

Now running npm run storybook, we should be able to see our example component.

storybook-final.png

That should be enough to get up in running. I will be putting another guide up shortly to integrate stencil’s docs with the @storybook/webcomponent preset.