Storybook & Atomic Design – 1.10. – Building Our Footer

In this lesson, we'll be building our Footer component. The Footer component falls under the concept of an organism, as it could contain molecules and atoms as children nodes.

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

  1. menus - array

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

  1. items - array
  2. title - string

For each array item in our items prop 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 Footer component in a new directory under components, organisms then footer. This directory will house the component itself, the associated stories, emulated JSON payload data, and styled-components logic.

- __Storybook__
   - __components__
     - __organisms__
       - __footer__
         - footer.jsx
         - footer.knobs.json
         - footer.stories.js
         - footer.styles.jsx

footer.styles.jsx

Before we get started on the markup required to generate our Footer component elements, let's first import some styles to make our front-end implementation align with the Figma designs I've provided at the start of the course.

import styled from "styled-components"

export const StyledFooter = styled.footer`
  margin-left: calc(-50vw + 50%);
  margin-right: calc(-50vw + 50%);

  background-color: ${props => props.theme.grey800};
  color: ${props => props.theme.white};

  a {
    color: ${props => props.theme.grey300};
    font-size: 18px;
    line-height: 140%;
    text-decoration: none;
    transition: 0.2s color ease;

    &:active,
    &:focus,
    &:hover {
      color: ${props => props.theme.white};

      &:after {
        display: none;
      }

      svg {
        fill: ${props => props.theme.grey200};
      }
    }
  }

  a[aria-current="page"] {
    color: ${props => props.theme.white};

    &:after {
      display: none;
    }
  }

  button {
    min-width: auto;
  }

  button[type="submit"] {
    margin-top: 0;
    padding-left: 8px;
    padding-right: 8px;

    color: ${props => props.theme.grey200};
    transition: 0.2s color ease;

    &:active,
    &:focus,
    &:hover {
      color: ${props => props.theme.white};
    }

    &:focus {
      outline: 1px dotted ${props => props.theme.blue};
    }
  }

  form {
    display: flex;
    margin-top: 16px;

    border-bottom: 2px solid ${props => props.theme.white};
    background-color: ${props => props.theme.grey800};
    color: ${props => props.theme.white};
    transition: 0.2s background-color ease, 0.2s color ease;

    input {
      color: inherit;
    }
  }

  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    margin: 0 0 8px;
  }

  input[type="email"] {
    padding-left: 0;
    padding-top: 16px;
    padding-bottom: 16px;
    width: 100%;

    background-color: transparent;
    border: none;
    color: ${props => props.theme.white};
    font-size: 18px;
    font-weight: 400;
    line-height: 140%;

    &::placeholder {
      color: ${props => props.theme.white};
      font-size: 18px;
      font-weight: 400;
      line-height: 140%;
    }
  }

  svg {
    max-width: 20px;

    fill: ${props => props.theme.white};
    transition: 0.2s fill ease;
  }

  .form--submitted {
    cursor: default;

    background-color: ${props => props.theme.white};
    color: ${props => props.theme.grey800};
    transition: 0.2s background-color ease, 0.2s color ease;

    input {
      color: inherit;
    }

    input[disabled] {
      cursor: default;
    }

    input[type="email"] {
      padding-left: 12px;
    }
  }

  .footer__contents {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    margin: 0 auto;
    max-width: ${props => props.theme.gridMax};
    padding: 48px 30px 30px;
  }

  .footer__copyright {
    align-items: center;
    display: flex;
    flex-direction: row;
	  margin: 48px auto 0;
    width: 100%;

    * {
      margin: 0;
    }

    a {
      margin-left: 12px;
      padding: 16px;
      position: relative;

      color: ${props => props.theme.white};

      &::before {
        border-radius: 50%;
        content: "";
        display: block;
        height: 8px;
        left: -2px;
        position: absolute;
        top: 50%;
        width: 8px;

        background-color: ${props => props.theme.grey500};
        transform: translateY(-50%);
      }
    }
  }

  .footer__navigation {
    a {
      font-weight: 400;
    }

    a + a {
      margin-left: 0;
      margin-top: 16px;
    }

    nav {
      align-items: flex-start;
      flex-direction: column;
      margin-top: 16px;
      padding: 0;
    }
  }

  .footer__navigation + .footer__navigation {
	  margin-left: 64px;
    margin-top: 0;
    padding-top: 0;

	  border-top: none;
  }

  .footer__newsletter {
	  margin-left: auto;
    margin-top: 0;
    max-width: 320px;
    padding-top: 0;

    p {
      color: ${props => props.theme.grey300};
      font-size: 18px;
      font-weight: 300;
      line-height: 140%;
    }
  }

  .footer__social {
    margin: 24px auto 16px;

    a + a {
      margin-left: 16px;
    }
  }

  .footer__wrapper {
    display: flex;
    flex-direction: row;
  }
`

export default StyledFooter

footer.jsx

Our footer component accepts one prop named menus which is an array of objects that we can map over to return the title of the navigation items, followed by an instance of our Navigation molecule component.

First, we start by destructuring the accepted props passed to our Footer component and extract the named value of menus.

We start our component by referencing our StyledFooter as a parent wrapper of the component HTML elements and React child components.

If menus is defined and iterable, the footer component will loop over each node it finds, in this case, we have objects with two keys items and title. We destructure these values immediately in our array map function and pass them into a return statement.

The return statement renders a <React.Fragment> React component for which we use the shorthand <> to reference. React fragments allow us to wrap children nodes in an empty component, this is useful as returning our elements directly without a parent wrapper will throw an error, but we don't want to add another HTML node to the DOM.

Read more about React.Fragment at - https://reactjs.org/docs/fragments.html

Next, we use an inline AND syntax for JavaScript. To anyone unfamiliar with React, this may look a little odd, but what we are doing is querying on the left whether or not title is defined. If title is defined then we will continue with our JavaScript code which will return the h4 HTML element. If title is not defined then the statement exits and nothing is returned.

We do the same for items and check that it is defined and is an array by checking the value has a length property defined, before returning our Navigation molecule with the items value passed as a prop.

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

import StyledFooter from "./footer.styles.jsx";

import Navigation from "../../molecules/navigation/navigation";

const Footer = ({ navigation }) => (
	<StyledFooter>
		{navigation &&
			navigation.length > 0 &&
			navigation.map(({ items, title }) => (
				<>
					{title && <h4 className="footer__heading">{title}</h4>}
					{items && items.length && <Navigation items={items} />}
				</>
			))}
	</StyledFooter>
);

export default Footer;

PropTypes

To keep consistency with our other components and to ensure the Footer 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 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 itemswhich is an array we defined in our previous lesson on navigation, and title which is a type string.

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

import StyledFooter from "./footer.styles.jsx";

import Navigation from "../../molecules/navigation/navigation";

const Footer = ({ navigation }) => (
	<StyledFooter>
		{menus &&
			menus.length > 0 &&
			menus.map(({ items, title }) => (
				<>
					{title && <h4 className="footer__heading">{title}</h4>}
					{items && items.length && <Navigation items={items} />}
				</>
			))}
	</StyledFooter>
);

// Expected prop values
Footer.propTypes = {
	menus: arrayOf(
		shape({
			items: arrayOf(
				shape({
					label: string,
					target: string,
					url: string
				})
			),
			title: string
		})
	)
};

export default Footer;

Adding HTML Elements

For the sakes of keeping this lesson shorter, I've gone ahead and structured the other HTML elements inline with the initial Figma mockups.

First, importing four social media logo icons, then structuring a footer newsletter container with a form that holds no functionality yet (we will revisit this later).

Finally, adding a copyright tagline at the bottom of the footer component with credit to myself.

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

import StyledFooter from "./footer.styles.jsx";

import IconFacebook from "../../../assets/images/icons/brands/facebook.svg";
import IconInstagram from "../../../assets/images/icons/brands/instagram.svg";
import IconLinkedIn from "../../../assets/images/icons/brands/linkedin.svg";
import IconTwitter from "../../../assets/images/icons/brands/twitter.svg";

import Navigation from "../../molecules/navigation/navigation";

const Footer = ({ menus }) => (
	<StyledFooter>
		{menus &&
			menus.length > 0 &&
			menus.map(({ items, title }) => (
				<>
					{title && <h4 className="footer__heading">{title}</h4>}
					{items && items.length && <Navigation items={items} />}
				</>
			))}

		<div className="footer__newsletter">
			<h4 className="footer__heading">Join our newsletter</h4>
			<p>We will send you updates on new products and discounts.</p>
			<form>
				<input type="email" placeholder="Email Address..." />
				<button type="submit">Send</button>
			</form>
			<nav className="footer__social">
				<a href="<https://google.com>">
					<IconFacebook />
				</a>
				<a href="<https://instagram.com>">
					<IconInstagram />
				</a>
				<a href="<https://linkedin.com>">
					<IconLinkedIn />
				</a>
				<a href="<https://twitter.com>">
					<IconTwitter />
				</a>
			</nav>
		</div>

		<nav className="footer__copyright">
			<p>Copyright &copy; Celtic Elements 2020</p>
			<a href="<https://whatjackhasmade.co.uk>">Website by Jack Pritchard</a>
		</nav>
	</StyledFooter>
);

// Expected prop values
Footer.propTypes = {
	menus: arrayOf(
		shape({
			items: arrayOf(
				shape({
					label: string,
					target: string,
					url: string
				})
			),
			title: string
		})
	)
};

export default Footer;

footer.knobs.json

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.

{
  "footerNav": {
    "default": [
      {
        "title": "Sitemap",
        "items": [
          {
            "label": "Home",
            "target": null,
            "url": "#"
          },
          {
            "label": "Shop",
            "target": null,
            "url": "#"
          },
          {
            "label": "Contact",
            "target": null,
            "url": "#"
          }
        ]
      },
      {
        "title": "Information",
        "items": [
          {
            "label": "About us",
            "target": null,
            "url": "#"
          },
          {
            "label": "FAQ",
            "target": null,
            "url": "#"
          },
          {
            "label": "Insights",
            "target": null,
            "url": "#"
          },
          {
            "label": "Privacy policy",
            "target": null,
            "url": "#"
          }
        ]
      },
      {
        "title": "Your account",
        "items": [
          {
            "label": "Register",
            "target": null,
            "url": "#"
          },
          {
            "label": "Login",
            "target": null,
            "url": "#"
          },
          {
            "label": "My orders",
            "target": null,
            "url": "#"
          },
          {
            "label": "Shipping details",
            "target": null,
            "url": "#"
          }
        ]
      }
    ],
    "group": "Content",
    "label": "Footer navigation"
  }
}

footer.stories.js

We can then import the JSON payload in our Footer Storybook story and configure the array knob to give viewers of the component an input field to manipulate the prop data fed to the Footer.

import React from "react";
import { array } from "@storybook/addon-knobs";
import Footer from "./footer";

import knobData from "./footer.knobs.json";
const { footerNav } = knobData;

export const standardFooter = () => (
	<Footer
		menus={array(footerNav.label, footerNav.default, footerNav.group)}
	/>
);

export default {
	component: Footer,
	title: "Footer"
};

Conclusion

Now we have a type defined Footer component which makes use of our smaller component concepts (Navigation molecule) and dynamically renders navigation lists based on the prop data available to the component.

In a future lesson, we'll revisit this component to add a functional newsletter sign up form, but in the next lesson, we'll be covering how we can build a more complex organism, the Header component.

Continue Reading 📚