Storybook & Atomic Design – 1.9. – Building Our Navigation

In this lesson, we'll be building our Navigation component.

The Navigation component falls under the concept of a molecule, as it will hold many children nodes of the classification atom (links, list items, buttons).

Initially, we will focus on passing one prop to our Navigation component. The prop will be -

  1. direction - string
  2. items - array

For each array item in our items props we can expect an object with the following keys -

  1. title - string
  2. url - string

Creating the Component Files

We will want to create the Navigation component in a new directory under components, molecules then navigation. This directory will house the component itself, the associated stories and styled-components logic.

- __Storybook__
   - __components__
     - __molecules__
       - __navigation__
         - [navigation.jsx](components/molecules/navigation/navigation.jsx)
         - [navigation.knobs.json](components/molecules/navigation/navigation.knobs.json)
         - [navigation.stories.js](components/molecules/navigation/navigation.stories.js)
         - [navigation.styles.jsx](components/molecules/navigation/navigation.styles.jsx)
import styled from "styled-components";

const StyledNavigation = styled.nav`
	display: flex;
	flex-direction: ${props =>
		props.direction !== "horizontal" ? `column` : undefined};
	padding: 16px;

	a + a {
		margin-left: ${props =>
			props.direction === "horizontal" ? `24px` : undefined};
		margin-top: ${props =>
			props.direction !== "horizontal" ? `24px` : undefined};
	}
`;

export default StyledNavigation;

Our navigation component accepts two props, one for the styled direction of the navigation items and one that contains an array of objects to return a group of anchor elements.

The first prop is named direction and will be a string, the second named items which is an array we can map over to return several anchor tag elements within our styled nav element.

First, we destructure the props passed and extract the named values of direction and items.

Our component will immediately return our Styled-Component StyledNavigation which we defined above.

Within our Styled-Component children, we loop through every node available within our array.

For every node we find, we are then immediately destructuring the keys of the node object and extracting named values for title and url.

We then use an arrow function to pass these values into a return statement which will return an anchor HTML element with the href property set to the value of url and finally using the title value as the inner text of the anchor tag.

import React from "react";

import StyledNavigation from "./navigation.styles";

const Navigation = ({ direction, items }) => (
	<StyledNavigation direction={direction}>
		{items.map(item => (
			<a href={item.url}>{item.title}</a>
		))}
	</StyledNavigation>
);

To keep consistency with our other components and to ensure the Navigation component accepts a valid array of values, we will define the propTypes our component should expect.

As we are defining an array of objects, we will use a new syntax to define a complex set of props using arrayOf and shape to tell our component to expect an array of objects with specific keys.

arrayOf tells the component to accept an array for items.

shape allows us to create a custom structure which in this case is an object of keys title and url both of the type string.

import React from "react";
import { arrayOf, shape, string } from "prop-types";

import StyledNavigation from "./navigation.styles";

const Navigation = ({ direction, items }) => (
	<StyledNavigation direction={direction}>
		{items.map(item => (
			<a href={item.url}>{item.title}</a>
		))}
	</StyledNavigation>
);

// Expected prop values
Navigation.propTypes = {
	direction: string.isRequired,
	items: arrayOf(
		shape({
			title: string.isRequired,
			url: string.isRequired
		})
	)
};

// Default prop values
Navigation.defaultProps = {
	direction: "horizontal",
	items: []
};

export default Navigation;

As we haven't used any of the reserved names of arrayOf, shape, or string for any other values in our component, we can destructure these named values from our prop-types package using the following.

import { arrayOf, shape, string } from "prop-types";

With the new values available to us, we can remove any references to PropTypes in our type definitions.

import React from "react";
import { arrayOf, shape, string } from "prop-types";

import StyledNavigation from "./navigation.styles";

const Navigation = ({ direction, items }) => (
	<StyledNavigation direction={direction}>
		{items.map(item => (
			<a href={item.url}>{item.title}</a>
		))}
	</StyledNavigation>
);

// Expected prop values
Navigation.propTypes = {
	direction: string.isRequired,
	items: arrayOf(
		shape({
			title: string.isRequired,
			url: string.isRequired
		})
	)
};

// Default prop values
Navigation.defaultProps = {
	direction: "horizontal",
	items: []
};

export default Navigation;

As we don't have any data-source for our component, we will use a JSON file to emulate the data we would expect from a JSON payload.

We can also make use of our Knobs add-on to create an array input field in our story, allowing us to update the data when previewing the component.

{
	"direction": {
		"label": "Direction",
		"options": {
			"horizontal": "horizontal",
			"vertical": "vertical"
		},
		"default": "horizontal",
		"group": "variation"
	},
	"items": {
		"label": "Items",
		"default": [
			{ "title": "Home", "url": "/" },
			{
				"title": "About us",
				"url": "/about"
			},
			{
				"title": "Contact",
				"url": "/contact"
			}
		],
		"group": "content"
	}
}
import React from "react";
import { withKnobs, array, select } from "@storybook/addon-knobs";

import Navigation from "./navigation";

import knobData from "./navigation.knobs.json";
const { direction, items } = knobData;

export const horizontalNavigation = () => (
	<Navigation
		direction={select(
			direction.label,
			direction.options,
			direction.default,
			direction.group
		)}
		items={array(items.label, items.default, items.group)}
	/>
);
export const verticalNavigation = () => (
	<Navigation
		direction={select(
			direction.label,
			direction.options,
			"vertical",
			direction.group
		)}
		items={array(items.label, items.default, items.group)}
	/>
);

export default {
	component: Navigation,
	decorators: [withKnobs],
	title: "Molecules|Navigation"
};

Now we have a type defined navigation component which will return a navigation list dynamic to the data it receives as props.

In a future lesson, we'll revisit this component to add responsive styles, but for now, we've got a working navigation molecule that we can use in our Footer and Header organisms.

Continue Reading 📚