Build Chakra UI Wrap component using react, typescript, styled-components and styled-system
Tags: react, typescript
Published: 2021-09-02 07:57:15

### Introduction

Let us continue building our chakra components using `styled-components` & `styled-system`. In this tutorial we will be cloning the Chakra UI `Wrap` & `WrapItem` components.

- I would like you to first check the [chakra docs] for [wrap](https://chakra-ui.com/docs/layout/wrap).

- We will **compose** (extend) our `Flex` component to create the `Wrap` & `WrapItem` components.

- All the code for this tutorial can be found [here](https://github.com/yaldram/chakra-ui-clone) under the **atom-layout-wrap** branch.

### Prerequisite

Please check the previous post where we have completed the Container Components. Also please check the Chakra Wrap Component code [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/layout/src/wrap.tsx).

In this tutorial we will -

- Create a Wrap component.
- Create a WrapItem component.
- Create story for the components.
- As opposed to the Chakra Wrap component which renders a `ul` tag and WrapItem which renders an `li` tag, we will render `div` tags instead. Though we can pass the polymorphic `as` prop but our props aren't applying the styles, one of the many drawbacks / caveats of using `styled-components` & `styled-systems` which we will cover later. You can check the issue [here](https://github.com/styled-system/styled-system/issues/1253#issuecomment-622375248).

### Setup

- First let us create a branch, from the main branch run -

```bash
git checkout -b atom-layout-wrap
```

- Under the `components/atoms/layout` folder create a new folder called wrap. Under wrap folder create 2 files `index.tsx` and `wrap.stories.tsx`.

- So our folder structure stands like - **src/components/atoms/layout/wrap**.

### Wrap Component

- After reading the docs you got an idea of how the wrap component works, it basically has a margin around its children. The code is below -

```tsx
import * as React from "react";
import styled from "styled-components";

import { Flex, FlexProps } from "../flex";

type WrapOptions = {
  spacing: string;
};

type BaseWrapProps = FlexProps & WrapOptions;

const BaseWrap = styled(Flex)<BaseWrapProps>`
  ${({ spacing, theme }) => {
    const margin = theme.space[spacing] ?? spacing;

    return {
      margin: `calc(${margin} / 2 * -1)`,
      "& > *:not(style)": {
        margin,
      },
    };
  }}
`;

export interface WrapProps extends FlexProps, Partial<WrapOptions> {
  shouldWrapChildren?: boolean;
}

export const Wrap = React.forwardRef<HTMLDivElement, WrapProps>(
  (props, ref) => {
    const {
      spacing = "md",
      shouldWrapChildren = false,
      children,
      ...delegated
    } = props;

    const childrenToRender = shouldWrapChildren
      ? React.Children.map(children, (child, index) => (
          // eslint-disable-next-line react/no-array-index-key
          <WrapItem key={index}>{child}</WrapItem>
        ))
      : children;

    return (
      <BaseWrap ref={ref} spacing={spacing} wrap="wrap" {...delegated}>
        {childrenToRender}
      </BaseWrap>
    );
  }
);
```

- First we start with our imports. Given the fact that we are extending the `Flex` Component and creating the `Wrap` Component, we extend `FlexProps`. We have 2 additional props `spacing` for actual margin and `shouldWrapChildren` if passed as true we will wrap the children with `WrapItem`.

- Also take a note on the BaseWrap styled component, here we can pass both design tokens like "sm, md" from our theme or raw values like "1rem, 20px, etc". Therefore we first check our theme.space else just pass the incoming spacing value.

- As mentioned earlier we are adding a defined space / margin around our children. Also note that we passed wrap = "wrap" for the BaseWrap, which means our children will wrap automatically if there isn't enough space to fit any more in the same row. And each our children gets a margin thanks to this selector - `& > *:not(style)`.

- It is very important to note that because each children inside the `Wrap` component gets a margin, we kind a give some negative margin to the Wrap container (half the value of the spacing to be precise). Check this code for the `BaseWrap` - **margin: `calc(${margin} / 2 * -1)`**.

### WrapItem Component

- It is very simple, paste the code below -

```tsx
export interface WrapItemProps extends FlexProps {}

export const WrapItem = React.forwardRef<HTMLDivElement, WrapItemProps>(
  (props, ref) => {
    return <Flex ref={ref} align="flex-start" {...props} />;
  }
);
```

### Story

- With the above our `Wrap` & `WrapItem` components are completed, let us create a story.
- Under the `src/components/atoms/layout/wrap/wrap.stories.tsx` file we add the below story code.
- We will create 2 stories - Playground and Default.

```tsx
import * as React from "react";

import { spacingOptions } from "../../../../theme/spacing";
import { Center } from "../containers";
import { Wrap, WrapItem, WrapProps } from ".";

export default {
  title: "Atoms/Layout/Wrap",
};

export const Playground = {
  argTypes: {
    spacing: {
      name: "spacing",
      type: { name: "string", required: false },
      defaultValue: "md",
      description: `Margin for the child elements`,
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "md" },
      },
      control: {
        type: "select",
        ...spacingOptions(),
      },
    },
    justify: {
      name: "justify",
      type: { name: "string", required: false },
      defaultValue: "flex-start",
      description: "Shorthand for justifyContent style prop",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "flex-start" },
      },
      control: {
        type: "select",
        options: [
          "justify-content",
          "flex-start",
          "flex-end",
          "center",
          "space-between",
          "space-around",
          "space-evenly",
          "initial",
          "inherit",
        ],
      },
    },
    align: {
      name: "align",
      type: { name: "string", required: false },
      defaultValue: "stretch",
      description: "Shorthand for alignItems style prop",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "stretch" },
      },
      control: {
        type: "select",
        options: [
          "stretch",
          "center",
          "flex-start",
          "flex-end",
          "baseline",
          "initial",
          "inherit",
        ],
      },
    },
  },
  render: (args: WrapProps) => (
    <Wrap {...args}>
      <WrapItem>
        <Center w="180px" h="80px" bg="red200">
          Box 1
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="180px" h="40px" bg="green200">
          Box 2
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="120px" h="80px" bg="tomato">
          Box 3
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="180px" h="120px" bg="blue200">
          Box 4
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="180px" h="80px" bg="blackAlpha500">
          Box 5
        </Center>
      </WrapItem>
    </Wrap>
  ),
};

export const Default = {
  render: () => (
    <Wrap spacing="30px">
      <WrapItem>
        <Center w="180px" h="80px" bg="red200">
          Box 1
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="180px" h="80px" bg="green200">
          Box 2
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="120px" h="80px" bg="tomato">
          Box 3
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="180px" h="80px" bg="blue200">
          Box 4
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="180px" h="80px" bg="blackAlpha500">
          Box 5
        </Center>
      </WrapItem>
    </Wrap>
  ),
};
```

- Now run `npm run storybook` check the stories. Under the Playground stories check the controls section play with the props, add more controls if you like.

### Build the Library

- Under the `/layout/index.ts` file and paste the following -

```tsx
export * from "./box";
export * from "./flex";
export * from "./stack";
export * from "./containers";
export * from "./wrap";
```

- Now `npm run build`.

- Under the folder `example/src/App.tsx` we can test our `Wrap` component. Copy paste the following code and run `npm run start` from the `example` directory.

```tsx
import * as React from "react";
import { Wrap, WrapItem, Center } from "chakra-ui-clone";

export function App() {
  return (
    <Wrap spacing="30px">
      <WrapItem>
        <Center w="180px" h="80px" bg="red.200">
          Box 1
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="180px" h="80px" bg="green.200">
          Box 2
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="180px" h="80px" bg="tomato">
          Box 3
        </Center>
      </WrapItem>
      <WrapItem>
        <Center w="180px" h="80px" bg="blue.200">
          Box 4
        </Center>
      </WrapItem>
    </Wrap>
  );
}
```

### Summary

There you go guys in this tutorial we created `Wrap` and `WrapItem` components just like chakra ui and stories for them. You can find the code for this tutorial under the **atom-layout-wrap** branch [here](https://github.com/yaldram/chakra-ui-clone). In the next tutorial we will create `Grid` component. Until next time PEACE.