How to Create a unit converter application using Storybook component stories

 I will use the convert-units library to implement the unit conversion app. Open a second terminal in your project folder and run the command below.

npm install -E convert-units@2.3.4

Now, in your IDE, create a new file, src/stories/Converter.jsx, and fill it with the contents below.

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import * as convert from 'convert-units';
import { Input, Select } from './Components';

export const Converter = ({measure}) => {
  const possibilities = convert().possibilities(measure).map((unit) => {
      const descr = convert().describe(unit);
      return {
          value: descr.abbr,
          description: `${descr.singular} (${descr.abbr})`
      };
  });

  const [fromUnit, setFromUnit] = useState(possibilities[0].value);
  const [toUnit, setToUnit] = useState(possibilities[0].value);
  const [fromValue, setFromValue] = useState(1);
  const [toValue, setToValue] = useState(convert(1).from(fromUnit).to(toUnit));

  const updateFromUnit = (event) => {
    setFromUnit(() => event.target.value);
    setToValue(() => convert(fromValue).from(event.target.value).to(toUnit));
  };

  const updateToUnit = (event) => {
    setToUnit(() => event.target.value);
    setToValue(() => convert(fromValue).from(fromUnit).to(event.target.value));
  };

  const updateValue = (event) => {
    setFromValue(() => event.target.value);
    setToValue(() => convert(event.target.value).from(fromUnit).to(toUnit));
  };
  
  return <div className="converter">
      <Select label="From:" options={possibilities} onChange={updateFromUnit}></Select>
      <Select label="To:" options={possibilities} onChange={updateToUnit}></Select>
      <Input label="Value:" type="floating-point" onChange={updateValue}></Input>
      <p>{fromValue} {fromUnit} = {toValue} {toUnit}</p>
  </div>
};

Converter.propTypes = {
  measure: PropTypes.string.isRequired
};

Input.defaultProps = {
  measure: 'length'
};

The component takes a single property called measure, which specifies the type of units to be converted and can be something like mass or length. The code for this component then consists of four parts. The first action is to query the convert-units library for all the possible unit conversion options. Units are mapped into an array of objects, ready to use with the Select component. In the next part, you’ll define four state properties, followed by three event handlers. These will react to a change in the user input and update the state accordingly. These event handlers contain the actual calls to the convert-units library where the unit conversion happens. Finally, the component is put together from all the parts and returned. You can also create a story for this more complex component with the individual components. Create a file src/stories/Converter.stories.jsx and paste in the following contents.

import React from 'react';
import { Converter } from './Converter';

export default {
  title: 'Components/Converter',
  component: Converter,
};

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

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

Default.args = {
  measure: 'length'
};

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

Mass.args = {
  measure: 'mass'
};

When you installed Storybook with the npx sb command, the initialization script added a few components as examples to demonstrate Storybook’s capabilities. You will be reusing two of these components for the unit-conversion app. Open src/stories/Header.jsx and replace its contents with the following code.

import React from 'react';
import PropTypes from 'prop-types';
import { Button } from './Button';
import './header.css';

export const Header = ({ user, onLogin, onLogout }) => (
  <header>
    <div className="wrapper">
      <div>
        <h1>Unit Converter</h1>
      </div>
      {user ? <div> Hello {user.given_name} </div> : ""}
      <div>
        {user ? (
          <Button size="small" onClick={onLogout} label="Log out" />
        ) : (
          <>
            <Button size="small" onClick={onLogin} label="Log in" />
          </>
        )}
      </div>
    </div>
  </header>
);

Header.propTypes = {
  user: PropTypes.shape({}),
  onLogin: PropTypes.func.isRequired,
  onLogout: PropTypes.func.isRequired,
  onCreateAccount: PropTypes.func.isRequired,
};

Header.defaultProps = {
  user: null,
};

I have modified the header component to show the correct application name and allow some structured user data to be passed in. In the story for the header, in the file src/stories/Header.stories.jsx, modify the arguments passed to the LoggedIn story to reflect this change.

LoggedIn.args = {
  user: {
    given_name: "Username"
  },
};

Now, open src/stories/Page.jsx and modify its contents to match the code below.

import React from 'react';
import PropTypes from 'prop-types';
import { Header } from './Header';
import './page.css';
import { Tabs } from './Components';
import { Converter } from './Converter';

export const Page = ({useAuth}) => {
  const [user, login, logout] = useAuth();
  return <article>
    <Header user={user} onLogin={login} onLogout={logout} />
    <section>
      <Tabs>
        <Converter measure="length" label="Length" key="length"></Converter>
        <Converter measure="mass" label="Mass" key="mass"></Converter>
        <Converter measure="volume" label="Volume" key="volume"></Converter>
      </Tabs>
    </section>
  </article>;
}

Page.propTypes = {
  useAuth: PropTypes.func.isRequired
};

Page.defaultProps = {
};

This component displays the application page, including the header and a tabbed container that allows switching between Converter components configured to convert different measures. The page needs a useAuth hook passed in that returns the user information and callbacks to log the user in or out. In the stories for the page, in, you need to create a mock function that supplies fake user data. Edit the contents of this file to look like the following code.

import React from 'react';
import { Page } from './Page';

export default {
  title: 'Pages/Page',
  component: Page,
};

const mockUseAuth = (loggedIn) => () => [
  loggedIn ? {given_name: "Username"} : undefined, 
  () => {}, 
  () => {}
];

const Template = (args) => <Page useAuth={mockUseAuth(true)} {...args}/>;

export const LoggedIn = Template.bind({});
LoggedIn.args = {
  useAuth: mockUseAuth(true),
};

LoggedIn.parameters = {
  controls: { hideNoControlsWarning: true },
};

export const LoggedOut = Template.bind({});
LoggedOut.args = {
  useAuth: mockUseAuth(false),
};

LoggedOut.parameters = {
  controls: { hideNoControlsWarning: true },
};

Note how mockUseAuth uses currying to return a function that can be used as the useAuth hook in the Page component. You can now use Storybook again to test the Converter component and the full application page. If it’s not still running, run npm run storybook again. You can navigate to Pages -> Page in the left sidebar, and you should see something like the image below.

Testing the application page with Storybook


Comments

Popular posts from this blog

[SVN] Simple way to do code review

How to Choose a Technology Stack for Web Application Development

Setting ESLint on a React Typescript project