DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on • Updated on

Build Chakra UI Wrap component using react, typescript, styled-components and styled-system

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.

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

  • All the code for this tutorial can be found here 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.

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.

Setup

  • First let us create a branch, from the main branch run -
git checkout -b atom-layout-wrap
Enter fullscreen mode Exit fullscreen mode
  • 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 -
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>
    );
  }
);
Enter fullscreen mode Exit fullscreen mode
  • 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 -
export interface WrapItemProps extends FlexProps {}

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

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.
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>
  ),
};
Enter fullscreen mode Exit fullscreen mode
  • 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 -
export * from "./box";
export * from "./flex";
export * from "./stack";
export * from "./containers";
export * from "./wrap";
Enter fullscreen mode Exit fullscreen mode
  • 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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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. In the next tutorial we will create Grid component. Until next time PEACE.

Discussion (0)