Extending a component library and its documentation for a speedy design system

Lion is a set of white label cross-platform Web Components with accessibility and performance built-in. You can extend them with your own styling to create a complete Design System with little effort.

Erik Kroes
ING Blog
6 min readDec 1, 2020

--

Screenshots of different dropdowns or comboboxes. They’re styled like Google, GitHub and WhatsApp.
Styling is extremely flexible

This blog will demonstrate that extending Lion is not limited to components. Documentation and demos can be reused as well. This removes duplicate work such as writing and maintaining documentation.

A naming convention that is similar to Lion* for class names and lion-* for tag names is required for this to work. For this demo, we use the the names ExampleButton and example-button.

Table of Contents

  1. Setting up, and Extending Lion
  2. Select documentation to re-use
  3. Change input paths
  4. Remove, add and replace sections
  5. Conclusion

Setting up, and extending Lion

This article assumes some basic terminal knowledge, and a working installation of npm. Yarn can work as well.

Create a new folder for our components using the terminal.

mkdir example-components

Enter the folder example-components and run the following command to scaffold a new project using open-wc.

npm init @open-wc

When presented with a menu, pick (at least) the following options.

What would you like to do today? › Scaffold a new project
✔ What would you like to scaffold? › Web Component
✔ What would you like to add? › Demoing (storybook)
✔ Would you like to use typescript? › No
✔ Would you like to scaffold examples files for? › Demoing (storybook)
✔ What is the tag name of your application/web component? example-button

Enter the folder example-compponents and run the following command to make lion-button a dependency. It is the component we will be extending.

npm i @lion/button --save

Within the folder src, open the following file:

example-button/src/ExampleButton.js

Replace the content with the following:

import { css } from 'lit-element';
import { LionButton } from '@lion/button';
export class ExampleButton extends LionButton {
static get styles() {
return [
super.styles,
css`
/* our styles can go here */
`
];
}
connectedCallback() {
super.connectedCallback();
this._setupFeature();
}
_setupFeature() {
// our code can go here
}
}

You have now extended <lion-button> and created <example-button> from it. The component can be experienced in the browser by running npm run storyboook inside the example-button-folder.
Feel free to add styles and make it your own. This can be the start of a whole set of Web Components for your Design System.

For this article we assume you set up the project like mentioned before, using Prebuilt Storybook with MDJS. If you already have a repository, you can also add Storybook using open-wc. Enter the following:

npm init @open-wc

And pick ‘upgrade an existing project’. Or install it manually by entering the following:

npm i @open-wc/demoing-storybook --save-dev

Select documentation to re-use

We need to specify which stories to load in .storybook/main.js .

Change the following line:

stories: ['../stories/**/*.stories.{js,md,mdx}'],

to add the Lion readme

stories: ['../stories/**/*.stories.{js,md,mdx}', '../node_modules/@lion/button/README.md'],

This is where we extend the documentation of LionButton, for our own ExampleButton. This step, by itself, gives us the LionButton docs inside our own Storybook.

Change input paths

We can change the import paths fromLionButton to the new paths of ExampleButton. We use Providence for this. This tool has a command that creates a full map of all the import paths of a reference project (Lion) and can replace them with the correct paths of a target project (Example).

Navigate the terminal to example-buttonand install this tool by adding it to our package.json:

npm i providence-analytics --save-dev

We can use it by adding a script to our package.json:

"scripts": {
"providence:extend": "providence extend-docs -r 'node_modules/@lion/*' --prefix-from lion --prefix-to example"
}

The --prefix-from is the prefix of the project you extend from (in this case lion). --prefix-to is the prefix of our own project (in this case example).
It will look for the classnames Lion* and Example*, and for the tagnames lion-* and example-*.

As we only use a single component from Lion, we can reduce the time the tool needs for analysis. Specify the single package by replacing -r 'node_modules/@lion/* with -r 'node_modules/@lion/button'.

We can review all from/to information in providence-extend-docs-data.json. Providence creates this critical file.

Replacing paths and names

With the information in the JSON-file, we can start transforming the LionButtondocumentation to ExampleButtondocumentation. We created a babel-plugin called babel-plugin-extend-docs for this.

This plugin will analyse the content of the markdown files, and transform it on the fly in es-dev-server and when building with Rollup for production.

To install this plugin, we navigate the terminal back to example-button and install this plugin by adding it to our package.json:

npm i babel-plugin-extend-docs --save-dev

A babel.config.js in the root of our project is also needed. It should contain:

const path = require('path');
const providenceExtendConfig = require('./providence-extend-docs-data.json');
const extendDocsConfig = {
rootPath: path.resolve('.'),
changes: providenceExtendConfig,
};
module.exports = {
overrides: [
{
test: ['./node_modules/@lion/*/README.md', './node_modules/@lion/*/docs/*.md'],
plugins: [['babel-plugin-extend-docs', extendDocsConfig]],
},
],
};

We import the providence output file (providence-extend-docs-data.json) and pass it to the plugin options as the changesproperty.

The babel plugin runs for the files that we specify in the testproperty, replaces the imports, and replaces the tag names inside JavaScript code snippets!

It will only transform JavaScript snippets that use MDJS syntax such as ```js script, ```js story and ```js preview-story

We also have to add Babel to our es-dev-server-configuration to make it work with Storybook.

Create a .storybook/main.js with the following content:

module.exports = {
stories: ['../node_modules/@lion/button/README.md', '../packages/**/!(*.override)*.md'],
esDevServer: {
nodeResolve: true,
watch: true,
open: true,
babel: true,
},
};

We should now see the LionButton instances transformed into our own ExampleButton!

Remove, add and replace sections

We might not want to show all examples of how to use a component. Sometimes information is Lionspecific, or perhaps in your Design System people are not allowed to use a certain feature that we documented in Lion.

In our example, we will remove the Rationale-section that we would normally inherit from the Lion-documentation.

We assume a folder structure here /packages/<package>/... here. When updating an existing repository, it might be different.

For this step we make use of a remark plugin for the MD content, similar to how we use a babel plugin for JS content. It is called Remark extend. It lets us add, remove or replace sections or specific words.

Remark extend needs the following content added to .storybook/main.js:

const fs = require('fs');
const { remarkExtend } = require('remark-extend');
function isLion(filePath) {
return filePath.indexOf('@lion/') !== -1;
}
function getLocalOverridePath(filePath, root = process.cwd()) {
const rel = filePath.substring(filePath.indexOf('/@lion/') + 7, filePath.length - 3);
return `${root}/packages/${rel}.override.md`;
}
module.exports = {
[...],
setupMdjsPlugins: (plugins, filePath) => {
if (!isLion(filePath)) {
return plugins;
}
const newPlugins = [...plugins];
const markdownIndex = newPlugins.findIndex(plugin => plugin.name === 'markdown');
const overridePaths = [`${process.cwd()}/.storybook/all.override.md`];
overridePaths.push(getLocalOverridePath(filePath));
let i = 0;
for (const overridePath of overridePaths.reverse()) {
if (fs.existsSync(overridePath)) {
const extendMd = fs.readFileSync(overridePath, 'utf8');
newPlugins.splice(markdownIndex, 0, {
name: `remarkExtend${i}`,
plugin: remarkExtend.bind({}),
options: { extendMd, filePath, overrideFilePath: overridePath },
});
}
i += 1;
}
return newPlugins;
},
[...],
};

In the code example mentioned, we have two places in where we can do overrides: ./.storybook/all.override.md for generic overrides and via getLocalOverridePath for each component. When needed, the rel needs to be the same in lion and our own project to be able to override the right file.

In each file we need to specify which section we want to override. We want to load example-button in the project:

```
::addMdAfter(':root')
```
```js script
import '../example-button.js';
```

And then replace each button with it.

```js ::replaceFrom(':root')
module.exports.replaceSection = node => {
if (node.type === 'code' && node.value) {
let newCode = node.value;
newCode = newCode.replace(/<lion-button/g, '<example-button');
node.value = newCode;
}
return node;
};
```

We can remove content by targeting a specific heading:

```
::removeFrom('heading:has([value=Usage with native form])')
```

Or we can add an extra paragraph below the content:

```
::addMdAfter(':scope:last-child')
```

The documentation of Remark extend has many more options and insights

Conclusion

Writing good extensive documentation can be hard and time consuming. Being able to extend both code (components) and documentation will increase your work speed.

We set up and adjusted the documentation to fit our extended component. please contact us if this article doesn’t answer your questions.

--

--

Erik Kroes
ING Blog
Writer for

I like to sink my teeth into a challenge, get the most out of people and situations, and create structure where there is none.