DEV Community

Erik Kroes
Erik Kroes

Posted on • Originally published at Medium

Extending a component library and it’s documentation for 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.

Styling is extremely flexibleStyling 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. ExampleButton and example-button.

mkdir example-components
Enter fullscreen mode Exit fullscreen mode

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

npm init @open-wc
Enter fullscreen mode Exit fullscreen mode

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 fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Within the folder src, open the following file:

example-button/src/ExampleButton.js
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

npm i @open-wc/demoing-storybook --save-dev
Enter fullscreen mode Exit fullscreen mode

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}'],
Enter fullscreen mode Exit fullscreen mode

to add the Lion readme

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

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 from LionButton 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-button and install this tool by adding it to our package.json:

npm i providence-analytics --save-dev
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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 LionButton documentation to ExampleButton documentation. 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
Enter fullscreen mode Exit fullscreen mode

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]],
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

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

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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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 Lion specific, 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;
  },
  [...],
};
Enter fullscreen mode Exit fullscreen mode

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')
```
Enter fullscreen mode Exit fullscreen mode
```js script
import '../example-button.js';
```
Enter fullscreen mode Exit fullscreen mode

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;
};
```
Enter fullscreen mode Exit fullscreen mode

We can remove content by targeting a specific heading:

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

Or we can add an extra paragraph below the content:

```
::addMdAfter(':scope:last-child')
```
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)