Help me choose
Internship/2021 year-end summer internship

[WK2-W] MDX to generate documentation/stories in Storybook

by hajinny 2021. 12. 1.

0 Waking up

I slept really late today, and I don't want to say why because it's embarrassing. But I probably had like 3 hours of sleep. Thankfully I didn't fall asleep during work, like how? I'm not that tired at the moment and it's already 4:35pm

1 Write markdown documentation & generate stories in Storybook

Using markdown brings a complete shift to how you write stories. The best way to
understand markdown is by converting existing stories to stories/docs generated
via markdown. Many features of MDX with respect to Storybook are quite scattered
around Storybook website, so I've handpicked the features that might be of our
interest. After reading this, it will be pretty easy to add more features that
were not written on this guide.

We will take Carousel (Atom component) as the component of our interest.

carousel.stories.js

import React from 'react';
import { Paper } from '@material-ui/core';
// Until this supports IE11, we are going to
// import it directly so that its not bundled up by default
import { Carousel, LayoutContainer } from '@dfd/ui-components';

export default {
  title: 'Atoms/Carousel',
};

const colors = ['#CFEDED', '#F1E3F2', '#FEDFF0'];

const children = colors.map((color, i) => (
  <Paper
    key={i}
    elevation={0}
    style={{ backgroundColor: color, height: '300px', width: '100%' }}
  />
));

export const CarouselWithNavButton = () => (
  <LayoutContainer>
    <Carousel>{children}</Carousel>
  </LayoutContainer>
);

export const CarouselWitouthNavButton = () => (
  <LayoutContainer>
    <Carousel hideNavButtons>{children}</Carousel>
  </LayoutContainer>
);

export const CarouselAutoPlay = () => (
  <LayoutContainer>
    <Carousel autoPlay>{children}</Carousel>
  </LayoutContainer>
);

export const CarouselWithoutAutoPlay = () => (
  <LayoutContainer>
    <Carousel>{children}</Carousel>
  </LayoutContainer>
);

export const CarouselWithIndicatorsOnContents = () => (
  <LayoutContainer>
    <Carousel overlapIndicators>{children}</Carousel>
  </LayoutContainer>
);

Table of Contents

Step 1: Dynamically bind props arguments

The point of this is to make sure that in argsTable, props are properly
displayed. Also, by doing this, you get an added benefit of being able to tweak
the prop values passed to the components.

Furthermore, it reduces duplication by a lot as you will see very soon.

Dynamic binding of arguments is a standard feature in VanilaJS. It works by
creating a templating function and assigning arguments to it.

import React from 'react';
import { Paper } from '@material-ui/core';
// Until this supports IE11, we are going to
// import it directly so that its not bundled up by default
import { Carousel, LayoutContainer } from '@dfd/ui-components';

export default {
  title: 'Atoms/Carousel',
};

const colors = ['#CFEDED', '#F1E3F2', '#FEDFF0'];

const children = colors.map((color, i) => (
  <Paper
    key={i}
    elevation={0}
    style={{ backgroundColor: color, height: '300px', width: '100%' }}
  />
));

const CarouselTemplate = (args) => (
  <LayoutContainer>
    <Carousel {...args}>{children}</Carousel>
  </LayoutContainer>
);

export const CarouselSkeleton = CarouselTemplate.bind({});
CarouselSkeleton.args = {
  hideNavButtons: false,
  autoPlay: false,
  overlapIndicators: false,
};

 

 

Now, we only need one story to represent various states of the carousel.

We can honestly stay here if we can't be bothered writing up more
documentations, but we want to also document things better than just displaying
the UI components. That's where mdx comes in, and Step 1 defintely guides us
to that.

Step 2: Create a markdown

There are things you need to be aware of when you start using mdx, so I'll
briefly touch on that.

  1. \*.stories.js no longer serves to export any displaying documents You might
    wonder what that means, but it will be clear if you try the following.
  • Create carousel.stories.mdx (doesn't matter where, but I created it in
    src/stories/carousel)
  • Copy and paste the following to carousel.stories.mdx:
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Atoms/Carousel" /> // this tells storybook that mdx will generate a document for Atoms/Carousel page

# Hello world!
  • Comment out metadata in carousel.stories.js
// export default {
//   title: 'Atoms/Carousel',
// };

Then you will see this:

  1. Meta tag serves as a component level export default. As you will see soon,
    Meta tag substitutes for any meta information for a component like:
export default {
  title: 'Atoms/Buttons',
  parameters: {
    // Description for Button component
    componentSubtitle: 'General purpose buttons for DFD platforms',
    status: {
      // Possible statuses:  'beta' | 'stable' | 'deprecated' | 'releaseCandidate'
      // To add more statuses: go to preview.js
      type: ['beta'],
      url: 'http://www.url.com/status',
      statuses: 'released',
    },
  },
  // Describes each prop argument
  argTypes: {
    label: {
      description: 'Main text displayed for this button',
    },
    onClick: {
      description:
        'When you use this component, simply assign an appropriate onClick handler to this button.',
    },
  },
};

Step 3: Generate Stories through Markdown

Important:

The existence of carousel.stories.mdx means that Storybook will not look at
carousel.stories.js to generate stories. Instead, it will look at mdx file for
Story definitions, and will present them.

Here's how we define stories within mdx:


import { Meta, Canvas, Story } from '@storybook/addon-docs/blocks';
import { CarouselSkeleton } from './carousel.stories.js';

<Meta title="Atoms/Carousel" />

# Hello world!

## So you want to use markdown?

That's cool!

<Canvas>
  <Story name="CarouselInstance" args={{}}>
    {CarouselSkeleton.bind({})}
  </Story>
</Canvas>

You can embed story anywhere like this.

Doing so results in something like this:

You can see that a story named CarouselInstance has been created as per the
use of Story tag with such name, with no arguments bound to it (args={{}}).
Also, as you would have noticed from the preceding images, any use of <Story>
will result in creation of a story as well as display of that story within the
Docs tab for that component.

However, we know that we had bunch of stories in the initial
carousel.stories.js file. Let's go ahead and immitate that:

import { Meta, Canvas, Story } from '@storybook/addon-docs/blocks';
import { CarouselSkeleton } from './carousel.stories.js';

<Meta title="Atoms/Carousel" />

# Carousel

## Overview

Carousel serves to display multiple graphical items of the same dimensions.

## Default Carousel

Default settings are such that

<Canvas>
  <Story
    name="Default Carousel"
    args={{
      hideNavButtons: false,
      autoPlay: false,
      overlapIndicators: false,
    }}
  >
    {CarouselSkeleton.bind({})}
  </Story>
</Canvas>

## When to use each different configuration of Carousel

There are 3 customisable props for Carousel: `hideNavButtons`, `autoPlay`,
`overlapIndicators`. The following sections explain where it is appropriate to
enable such props.

## Using `hideNavButtons=true`

Nav Button is a button users can press to switch to next image in the carousel.
It's intended to be used whenever both conditions are met:

- UX-3847: It is appropriate for users to actively navigate through the carousel
  (eg promotions)
- UX-3721: The carousel is in the landing page

<Canvas>
  <Story name="CarouselWithNavButton" args={{}}>
    {CarouselSkeleton.bind({})}
  </Story>
</Canvas>

## Using `autoPlay=true`

Autoplay automatically scrolls the carousel, **one by one to the right every 5
seconds**. Currently, autoplay is fixed to such configuration and cannot be
modified. It's intended to be used whenever both conditions are met:

- UX-3211: When customer UX experience can improve with automatic scrolling of
  carousel
- UX-3422: When it is not expected that the customer scrolls by themselves

<Canvas>
  <Story
    name="CarouselWithAutoplay"
    args={{
      autoPlay: true,
    }}
  >
    {CarouselSkeleton}
  </Story>
</Canvas>

## Using `overlapIndicators=true`

I'm run out of things to say, but you get the idea.

- UX-3212: Yes, I definitely ran out of ideas
- UX-1928: Use it on your discretion

<Canvas>
  <Story
    name="CarouselWithOverlapIndicators"
    args={{
      overlapIndicators: true,
    }}
  >
    {CarouselSkeleton}
  </Story>
</Canvas>

This nicely generates the following markdown for Docs section of the Carousel
component:

Also, it generates all the stories defined with <Story> within
carousel.stories.mdx.

The coolest feature of mdx documetation is that you only worry about the
content of documentation
and generation of stories is simply byproduct of
this. Unnecessary stories are pruned early in time, allowing developers to focus
on what needs to be communicated rather than simply churning out various states.
It encourages documentation of each story, such as:

  • When it is appropriate to use the UI component with certain set of props
  • What features are non-configurable (eg autoPlay speed)
  • What UX/UI principles have been taken into account, and necessary improvements
    in the future.

Step 4: Further decoration

You might be interested in some further customisations of this markdown, so
let's cover some few that might be useful.

Args table

Args table allows user to play around with props of a story. To show ArgsTable
for a story named Default Carousel, simply insert
<ArgsTable story="Default Carousel" />. Now, you get a table where you can
manipulate values for each props. This is useful, you can provide it at the
start of the documentation with a default UI component, and allow users to
simply toggle values to see how UI component looks with different set of props.

Status

Previously, we used 'export default' in order to set meta-properties of a
component. Now, you instead set properties of Meta tag in order to achieve the
same effect.

This is an example of what you would have had before - this is taken from
button.stories.js.

export default {
  title: 'Atoms/Buttons',
  parameters: {
    // Description for Button component
    componentSubtitle: 'General purpose buttons for DFD platforms',
    status: {
      // Possible statuses:  'beta' | 'stable' | 'deprecated' | 'releaseCandidate'
      // To add more statuses: go to preview.js
      type: ['beta'],
      url: 'http://www.url.com/status',
      statuses: 'released',
    },
  },
  // Describes each prop argument
  argTypes: {
    label: {
      description: 'Main text displayed for this button',
    },
    onClick: {
      description:
        'When you use this component, simply assign an appropriate onClick handler to this button.',
    },
  },
};

However, now we can do the same thing with this: (carousel.stories.mdx)

import { Meta, Canvas, Story, ArgsTable } from '@storybook/addon-docs/blocks';
import { CarouselSkeleton } from './carousel.stories.js';
const argDescription = {
  hideNavButtons: {
    description: 'This hides navigation button',
  },
  autoPlay: {
    description: 'Scrolls carousel automatically',
  },
  overlapIndicators: {
    description: 'No idea what this is',
  },
};
const parameters = {
  status: {
    type: ['beta'],
  },
};

<Meta
  title="Atoms/Carousel"
  argTypes={argDescription}
  parameters={parameters}
/>;

So there's one-to-one mapping of those property names from export default to
Meta tag properties.

html

You can choose to embed any standard html/css into mdx. In fact, you can even
use @emotional/Styled.

Embed other stories

You can even take existing story and render it at any point in the document by
<Story id="atoms-buttons--primary" />

 

2 What did I do?

So today, I finally managed to write up a documentation about how to use mdx in Storybook. It was a rather long one, and it did take me a bit, but it was really interesting to write up about (probably because mdx is markdown, and markdown is cool)

 

Also, I was assigned a first ticket! Woohoo 🥳 *though I'm not so sure if that's something to celerate, whatever. The idea is that we try to keep scope it down to a concrete deliverables, and I think I know what I'm doing for now so that's good.

So I had the sprint meeting as usual, and it seems that we will be going on a picnic soon. That's because we had a successful deployment to one of our partners. We are likely to hold a picnic at Grey Lynn park or Cornwall park in the coming weeks. Let's see how awkward I'd be as a piece of intern in the group of upper level engineers lol.

 

Then, I worked on writing up the report for the ticket deliverable. I'm currently focusing on just writing down our current approaches and why we might need to integrate certain features of Storybook with respect to documentation, testing and deployment. I'll post it up when it's written though, so probably won't be out any time soon.

 

Another thing that I'll start doing from tomorrow is to just take a look at various testing mechanisms. That's one of the Storybook ecosystem, so it is worth putting time and effort to investigate into it.

 

That's it! See you next time.