Categories
Storybook for React

Storybook for React — Writing and Browsing Stories

Storybook lets us prototype components easily with various parameters.

In this article, we’ll look at how to write and browse stories with Storybook.

Browse Stories

We can browse stories by running:

npm run storybook

Then we’ll see the storybook at the URL that’s displayed.

The sidebar has the components.

And we have the preset props that are set with args on the left side.

We can zoom in visually and change the props with that.

The background can be changed so that we can see what the component looks like with different backgrounds.

We can also change the viewport side so we can see how it renders with them.

The docs tab shows the documentation about the components.

They are inferred from the source code.

Toolbars are customizable. We can use globals to toggle themes and languages.

Also, we can add addons to extend Storybook’s functionality.

They are located at the bottom of the preview pane.

The Controls addon lets us interact with the component.

Actions let us set the callbacks and simulate the result that we get with them.

Write Stories

We can write stories by adding some code with Storybook.

We put our story code files in the src/stories folder.

The story files end with the stories.js or stories.ts file.

We can set the story name with the storyName property.

For example, we can write:

import React from 'react';

import { Button } from './Button';

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};
Primary.storyName='primary button';

to create a basic story.

We set the story name by assigning a string to the storyName property.

Also, instead of using args, we can set our own props with a story.

For example, we can write:

export const Primary = () => <Button primary label="Button" />;
Primary.storyName='primary button';

to add the Primary story.

Also, we can set various parameters that we can test our component with the parameters property.

For example, we can write:

import React from 'react';

import { Button } from './Button';

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
  parameters: {
    backgrounds: {
      values: [
        { name: 'red', value: '#f00', },
        { name: 'green', value: '#0f0', },
        { name: 'blue', value: '#00f', },
      ]
    }
  }
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  label: 'Button',
};

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

We added the backgrounds property which should be displayed at the top menu.

This way, we can set different background colors and see what our component looks like with it.

Conclusion

We can add components with their stories and test them with Storybook.

Categories
Storybook for React

Storybook for React — Decorators and Multiple Components

Storybook lets us prototype components easily with various parameters.

In this article, we’ll look at how to write and browse stories with Storybook.

Decorators

We can add decorators with the decorators property.

It’s used to wrap our component with our own markup.

For example, we can write:

src/stories/Button.js

import React from 'react';
import PropTypes from 'prop-types';
import './button.css';

export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
  const mode = primary ? 'button-primary' : 'button-secondary';
  return (
    <button
      type="button"
      className={['button', `button-${size}`, mode].join(' ')}
      style={backgroundColor && { backgroundColor }}
      {...props}
    >
      {label}
    </button>
  );
};

Button.propTypes = {
  primary: PropTypes.bool,
  backgroundColor: PropTypes.string,
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func,
};

Button.defaultProps = {
  backgroundColor: null,
  primary: false,
  size: 'medium',
  onClick: undefined,
};

src/stories/Button.stories.js

import React from 'react';

import { Button } from './Button';

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
  decorators: [(Story) => <div style={{ margin: '20px' }}><Story /></div>]

};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  label: 'Button',
};

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

to add decorators with the decorators property.

It’s an array with functions we render the component with.

Story is the component that we’re wrapping the component with.

It should be the button since we’re testing the button.

Stories for two or more components

If we have 2 or more components, then we put them under the same folder.

For example, we can write:

src/stories/ListItem.js

import React from 'react';

export const ListItem = ({ text }) => {
  return (
    <li>
      {text}
    </li>
  );
};

src/stories/List.js

import React from 'react';
import PropTypes from 'prop-types';

export const List = ({ children, backgroundColor }) => {
  return (
    <ul style={{ backgroundColor }}>
      {children}
    </ul>
  );
};

List.propTypes = {
  backgroundColor: PropTypes.string
}

src/stories/List.stories.js

import React from 'react';
import { List } from './List';
import { ListItem } from './ListItem';

export default {
  component: List,
  title: 'List',
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};

export const Empty = (args) => <List {...args} />;

export const OneItem = (args) => (
  <List {...args}>
    <ListItem text='foo' />
  </List>
);

export const ManyItems = (args) => (
  <List {...args}>
    <ListItem text='foo' />
    <ListItem text='bar' />
    <ListItem text='baz' />
  </List>
);

We created the ListItem and List components that we used together in our story.

The List component accepts the backgroundColor prop and we set the argTypes property to set the control for it to a color picker.

This way, we can set the color for it.

Conclusion

We can compose different components and test them together within one story with Storybook.

Categories
Storybook for React

Storybook for React — Args

Storybook lets us prototype components easily with various parameters.

In this article, we’ll look at how to work with args with Storybook.

Args Object

The args object lets us pass in arguments that we can pass into our components to change it.

It can be passed at the story and component level.

Args is an object with string keys.

Story Args

We can add args to a story by writing:

src/stories/button.css

.button {
  font-weight: 700;
  border: 0;
  border-radius: 3em;
  cursor: pointer;
  display: inline-block;
  line-height: 1;
}
.button-primary {
  color: white;
  background-color: #1ea7fd;
}
.button-secondary {
  color: #333;
  background-color: transparent;
}
.button-small {
  font-size: 12px;
  padding: 10px;
}
.button-medium {
  font-size: 14px;
  padding: 11px;
}
.button-large {
  font-size: 16px;
  padding: 12px;
}

src/stories/Button.js

import React from 'react';
import PropTypes from 'prop-types';
import './button.css';

export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
  const mode = primary ? 'button-primary' : 'button-secondary';
  return (
    <button
      type="button"
      className={['button', `button-${size}`, mode].join(' ')}
      style={backgroundColor && { backgroundColor }}
      {...props}
    >
      {label}
    </button>
  );
};

Button.propTypes = {
  primary: PropTypes.bool,
  backgroundColor: PropTypes.string,
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func,
};

Button.defaultProps = {
  backgroundColor: null,
  primary: false,
  size: 'medium',
  onClick: undefined,
};

src/stories/Button.stories.js

import React from 'react';

import { Button } from './Button';

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  label: 'Button',
};

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

We create the Template function so that we render the Button with the props we want.

Then we call Template.bind so that we can set the args property on the returned object.

This way, we can set the default value of the props.

We can merge different args object together.

For example, we can write:

src/stories/Button.stories.js

import React from 'react';

import { Button } from './Button';

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

export const PrimaryLongName = Template.bind({});

PrimaryLongName.args = {
  ...Primary.args,
  label: 'Primary button long name',
}

We merged the args from the Primary object into the PrimaryLongName object so we can reuse it.

Component Args

We can define args on a component.

For example, we can write:

src/stories/Button.stories.js

import React from 'react';

import { Button } from './Button';

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
  args: {
    primary: true,
  },
};

We set the primary prop to true on all the buttons in the story.

Conclusion

We can set arguments in our stories to pass in different props to our React components for testing.

Categories
Storybook for React

Getting Started with Storybook for React

Storybook lets us prototype components easily with various parameters.

In this article, we’ll look at how to get started with Storybook.

Getting Started

We can get started by first creating a React project with Create React App.

To create it, we run:

npx create-react-app storybook-project

Then we can add Storybook to it by running:

npx sb init

Then we can run it by running:

npm run storybook

to run Storybook.

Now we can create a story.

A story has the component which may take some arguments to let us adjust it.

We can provide default values for these arguments.

First, we create a component that takes some props.

In the src/stories folder, we create a Button.js to create a button that takes some props:

import React from 'react';
import PropTypes from 'prop-types';
import './button.css';

export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
  const mode = primary ? 'button-primary' : 'button-secondary';
  return (
    <button
      type="button"
      className={['button', `button-${size}`, mode].join(' ')}
      style={backgroundColor && { backgroundColor }}
      {...props}
    >
      {label}
    </button>
  );
};

Button.propTypes = {
  primary: PropTypes.bool,
  backgroundColor: PropTypes.string,
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func,
};

Button.defaultProps = {
  backgroundColor: null,
  primary: false,
  size: 'medium',
  onClick: undefined,
};

Then we add a button.css to style it:

.button {
  font-weight: 700;
  border: 0;
  border-radius: 3em;
  cursor: pointer;
  display: inline-block;
  line-height: 1;
}
.button-primary {
  color: white;
  background-color: #1ea7fd;
}
.button-secondary {
  color: #333;
  background-color: transparent;
}
.button-small {
  font-size: 12px;
  padding: 10px;
}
.button-medium {
  font-size: 14px;
  padding: 11px;
}
.button-large {
  font-size: 16px;
  padding: 12px;
}

Our button takes a bunch of props to let us modify the button with different styles.

primary is a boolean prop.

backgroundColor is a string prop.

size lets us change the size with an enum type.

label lets us show a label.

onClick is a function to let us handle clicks.

To make it display in Storybook, we add the Button.stories.js file and add:

import React from 'react';

import { Button } from './Button';

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  label: 'Button',
};

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

We export an object with the things we want to display.

title has the title we want to display in Storybook.

component is the component we want to preview.

argTypes lets us set the control types for various props so we can change and preview them in Storybook.

Below that is the args. The args lets us pass in props to our component so that we can change it.

We provide some default values with the Template.bind method and setting the args property to the values we want.

Conclusion

We can create components and preview them with Storybook.

They can be changed if they accept props and bind to them with args.

Categories
JavaScript

Formatting Relative Time with JavaScript’s RelativeTimeFormat Constructor

With the Intl.RelativeTimeFormat constructor, we can format relative time in a locale sensitive manner with ease. We can style in different ways and format it the manner we want. This lets us format relative time strings without the hassle that comes from manipulating strings. The constructor takes 2 arguments. The first argument is a locale string or an array of such strings. The second argument is an object which takes a variety of arguments for adjusting the relative time string to the way we want. The instance of this constructor has a few methods to return the formatted string, the formatted string as an array of substrings, and a method to return the options that we set for formatting the string.

The first argument for the Intl.RelativeTimeFormat constructor is the locale which should be a BCP 47 language tag or an array of such locale strings. This is an optional argument.

The second argument accepts an object with a few properties — localeMatcher, numeric, and style .

The localeMatcher option specifies the locale matching algorithm to use. The possible values are lookup and best fit. The lookup algorithm searches for the locale until it finds the one that fits the character set of the strings that are being compared. best fit finds the locale that is at least but possibly more suited than the lookup algorithm.

The numeric option lets us set the option for how the formatted string’s message is outputted. The possible values are always which is like ‘2 days ago’, or auto , for example, like ‘yesterday’. The auto allows us to not always use numeric values for output. The style option lets us change the length of the internationalized message. The possible values are long, short, or narrow. long would output something like ‘in 2 months’, short would be something like ‘in 2 mo.’, and narrow would be something like in ‘in 2 mo.’ It could be similar to the short style in some locales.

Instances of the Intl.RelativeTimeFormat constructor have a few methods. It has the format method to get the formatted relative time string with the value and the unit according to the locale and the formatting option that’s given in the constructor. The formatToParts method is similar to the format method except that the formatted string is returned as an array instead of a string. The resolvedOptions method returns an object with the options that we set in the constructor for formatting the string and the locale that were set.

The format method takes 2 arguments. The first is the value for the quantity of the relative date and the second is the time unit in string form. For example, we can format relative dates with the format method like in the following code:

const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit",
  numeric: "always",
  style: "long",
});

console.log(rtf.format(-1, "day"));

The code above would log ‘1 day ago’ since we specified the value of the relative date to be -1, which means 1 day before today, and the time unit is day . We can also put in other units. For example, if we want minutes, then we get:

const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit",
  numeric: "always",
  style: "long",
});

console.log(rtf.format(-10, "minute"));

Then we get ‘10 minutes ago’ from the console.log statement. We can also change the style and the length. For example, we can write:

const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit",
  numeric: "auto",
  style: "short",
});
console.log(rtf.format(10, "minute"));

Then we get ‘in 10 min.’ from the console.log statement since we have positive 10 instead of negative 10 which is 10 minutes from the current time. Also, we had the short style which abbreviates the unit.

We can also change the locale for different locales. For example, we can write:

const rtf = new Intl.RelativeTimeFormat("zh-hant", {
  localeMatcher: "best fit",
  numeric: "auto",
  style: "long",
});
console.log(rtf.format(1, "minute"));

This gets the relative date-time string in Chinese Traditional characters instead of English. If we run the console.log statement in the code above, we get ‘1 分鐘後’, which means 1 minute later.

We can get the formatted string in an array of string parts with the formatToParts() method. It returns an array of substrings of the formatted strings. For example, we can call it like in the following code:

const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit",
  numeric: "always",
  style: "long",
});

const parts = rtf.formatToParts(-1, "day");
console.log(parts);

The code above would get us:

[
  {
    "type": "integer",
    "value": "1",
    "unit": "day"
  },
  {
    "type": "literal",
    "value": " day ago"
  }
]

with the console.log statement in the code above.

The method works equally well with non-English locales. For example, we can write:

const rtf = new Intl.RelativeTimeFormat("zh-hant", {
  localeMatcher: "best fit",
  numeric: "auto",
  style: "long",
});

const parts = rtf.formatToParts(1, "minute")
console.log(parts);

Then we get:

[
  {
    "type": "integer",
    "value": "1",
    "unit": "minute"
  },
  {
    "type": "literal",
    "value": " 分鐘後"
  }
]

with the console.log statement in the code above.

The resolvedOptions() method gets us an object with the options that we set in the constructor for formatting the string and the locale that was set. We can use it as in the following code:

const rtf = new Intl.RelativeTimeFormat("zh-hant", {
  localeMatcher: "best fit",
  numeric: "auto",
  style: "long",
});
console.log(rtf.`resolvedOptions`());

With the code above, we get the following from the console.log statement:

{
  "locale": "zh-Hant",
  "style": "long",
  "numeric": "auto",
  "numberingSystem": "latn"
}

The Intl.RelativeDateFormat constructor also has a supportedLocalesOf method to get us the supported locales for formatting date and time. It takes an array of BCP 47 locale strings as an argument. Unicode extension keys will be returned along with the locale code even though it has no relevance for date formatting if provided. It takes the localeMatcher option to specify the locale matching algorithm to use. The possible values are lookup and best fit . The lookup algorithm search for the locale until it finds the one that fits the character set of the strings that are being compared. best fit finds the locale that is at least but possibly more suited than the lookup algorithm.

For example, we can use it as in the following code:

const locales = ['en-ca', 'id-u-co-pinyin', 'ban'];
const options = {
  localeMatcher: 'lookup'
};

console.log(Intl.RelativeTimeFormat.supportedLocalesOf(locales, options));

Then we get [“en-CA”, “id-u-co-pinyin”] . This is because Balinese is similar enough to Indonesian to be considered the same for the lookup algorithm. Note that the Unicode extensions that are in the input array are returned along with the output even though it has no relevance in this context.

The JavaScript Intl.RelativeTimeFormat constructor let us format relative time in a locale sensitive manner with ease. We can style in different ways and format it the way we want. This lets us format relative time strings without much hassle that comes from manipulating strings. The constructor takes 2 arguments. The first argument is for a locale string or an array of such strings. The second is an object which takes variety of arguments for adjusting the relative time string to the way we want. The instance of this constructor has a few methods to return the formatted string, the formatted string as an array of substrings, and a method to return the options that we set for formatting the string. We can also check the locales that supported with the static supportedLocalesOf method.