DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 967,611 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for How we automated storybook documentation for our web components
Bryan Ollendyke
Bryan Ollendyke

Posted on

How we automated storybook documentation for our web components

We recently updated our storybook in a few dramatic ways that I wanted to share the process we used to improve them.

  1. I audited what we were communicating and removed dead leaves
  2. I added things we were missing
  3. I revamped our docs to include a simple way to supply developer shortcuts to get started with our elements
  4. I added in custom-elements.json details for advanced devs

Here's what it looks like if you didn't navigate to the vercel driven storybook.

shows documentation page for our rpg character web component

How we handle stories

Most of how we've automated storybook stories can be attributed to:

  • My coworker Nikki is an Evil Genius and syphons off the class properties to generate knobs automatically!
  • custom-elements.json is auto built by our tooling using the web-component-analyzer package with it's wca command
  • Our elements almost all have static get tag() for the name of the element and export their class to make it easier to build out
  • We wire many elements to our HAXSchema that supplies custom demo content

Here's an example of how our RPG character page works as it's a typical implementation.

import { withKnobs } from "@open-wc/demoing-storybook";
import { StorybookUtilities } from "@lrnwebcomponents/storybook-utilities/storybook-utilities.js";
import { RpgCharacter } from "./rpg-character.js";

export default {
  title: "System|RPG Character",
  component: "rpg-character",
  decorators: [withKnobs],
  parameters: {
    options: { selectedPanel: "storybookjs/knobs/panel" },
  },
};
const utils = new StorybookUtilities();
export const RpgCharacterStory = () =>
  utils.makeUsageDocs(
    RpgCharacter,
    import.meta.url,
    utils.makeElementFromHaxDemo(RpgCharacter)
  );

Enter fullscreen mode Exit fullscreen mode

storybook-utilities

  • npm install @lrnwebcomponents/storybook-utilities
  • NPM page

The makeElementFromHaxDemo can also be swapped out for makeElementFromClass to be used more broadly in any web component storybook!

utils.makeElementFromClass(GitCorner,
      {
        source: 'https://github.com/elmsln/lrnwebcomponents',
        alt: "Our monorepo of all the things you see here",
        corner: true,
        size: "large"
      })
Enter fullscreen mode Exit fullscreen mode

Our pattern follows that we pass in the class for the element (it should work with any library as our Vanilla and LitElement bases both work well)

makeUsageDocs

This is where the quality stepped up a notch. By passing the template for our demo through this function, as well as import.meta.url we've got everything we need in order to build our standard doc page that includes:

  • how to install via npm, yarn or pnpm
  • how to use in your project
  • deep API details from custom-elements.json
  • links to github / npm

How we get custom-elements.json

So this probably has better ways of handling it but I went with a simple fetch relative to import.meta.url and then stored it in a localStorage variable. Why? Because Storybook didn't enjoy rendering out async. As a result, there are 2 features that sorta "show up" as you use the stories more (we get our haxProperties in a similar way if its in a remote file).

Here's what the code looks like for that specific custom-elements.json section:

    const url = new URL(path);
    let entryFile = el.tag;
    let importPath = url.pathname.replace('/elements/','@lrnwebcomponents/').replace('.stories.js','.js');
    packageName = packageName || `${importPath.split('/')[0]}/${importPath.split('/')[1]}`;
    var description = window.localStorage.getItem(`${entryFile}-description`);
    setTimeout( async () => {
      // pull from the custom-elements json file since our tooling rebuilds this
      const d = await fetch(`${url.pathname}/../custom-elements.json`.replace('/lib/','/')).then((e) => e.json()).then(async (d) => {
        let allD = '';
        if (d.tags) {
          await d.tags.forEach((item) => {
            // ignore source versions
            if (!item.path || !item.path.startsWith('./src/')) {
              allD += item.name + "\n" + (item.path ? item.path + "\n" : '') + (item.description ? item.description + "\n" : '') + "\n";
            }
          })
        }
        return allD;
      })
      window.localStorage.setItem(`${entryFile}-description`, d);
    }, 0);
Enter fullscreen mode Exit fullscreen mode

Here we can see that we use import.meta.url from the rpg-character.stories.js file requesting. This gives us the path to the file that loaded the story so that we can then base the path to custom-elements.json off of that. As our tooling runs in that same directory we just take the pathname from a new URL(), go back a directory (/../) which is a hack that can be used to move out of file names (could have replaced it but meh) and target custom-elements.json from there.

After getting that file we walk through the json and use pre to print the contents that we care about. wca did all the hard work of documenting tags, descriptions, and properties; now we're just printing it out in-case people want to see that at a glance without digging into the source.

I hope this helps give you ideas on how to automate your web component monorepos!

Top comments (0)

DEV has this feature:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. πŸ›