Build Chakra UI Grid component using react, typescript, styled-components and styled-system
Tags: react, typescript
Published: 2021-09-03 11:53:50

### Introduction

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

- I would like you to first check the chakra docs for [grid](https://chakra-ui.com/docs/layout/grid).
- We will **compose** (extend) our `Box` component to create the `Grid` & `GridItem` components.
- All the code for this tutorial can be found under the **atom-layout-grid** branch [here](https://github.com/yaldram/chakra-ui-clone).

### Prerequisite

Please check the previous post where we have completed the Wrap Component. Also please check the Chakra Grid Component code [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/layout/src/grid.tsx). After checking the chakra source code you might have noticed that they are using some util functions we too will use these of-course you can find that code [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/utils/src/object.ts). In this tutorial we will -

- Create a Grid component.
- Create a GridItem component.
- Create story for the Grid component.

### Setup

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

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

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

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

- Also under the utils folder create 4 files `utils/types.ts`, `utils/objects.ts`, `utils/assertions.ts` & finally `utils/responsive.ts`.

### Util Functions

- First under `utils/types.ts` paste the following code -

```ts
export type Dict<T = any> = Record<string, T>;

export type FilterFn<T> = (value: any, key: any, object: T) => boolean;
```

- Now under `utils/objects.ts` we will create 2 functions `objectFilter, filterUndefined`. Let me first explain the javascript code for these -

```js
function objectFilter(obj, fun) {
  const result = {};

  Object.keys(obj).forEach((key) => {
    const value = obj[key];
    const shouldPass = fun(value, key, obj);
    if (shouldPass) {
      result[key] = value;
    }
  });

  return result;
}

function filterUndefined(obj) {
  return objectFilter(obj, (value) => value !== null && value !== undefined);
}
```

- `objectFilter` takes in an object and a function, under this function we map over the Object keys pick its value and for each object key we run the passed function. If that function returns true we add that key and value to the result variable. How are we suppose to use it ?

- Check the `filterUndefined` function below this will remove all the keys in the object whose value is undefined.

- To be really honest, if I were to code this requirement of having a `filterUndefined` I would directly write the code for this use case, but this code from `chakra/utils` is such great, it passes the Single Responsibility Principle and also allows for composing functions. This is why I advise you to read open source packages / code you learn a lot from these. With that let us add typings to our `utils/objects.ts`, final code -

```ts
import { Dict, FilterFn } from "./types";

export function objectFilter<T extends Dict>(object: T, fn: FilterFn<T>) {
  const result: Dict = {};

  Object.keys(object).forEach((key) => {
    const value = object[key];
    const shouldPass = fn(value, key, object);
    if (shouldPass) {
      result[key] = value;
    }
  });

  return result;
}

export const filterUndefined = (object: Dict) =>
  objectFilter(object, (value) => value !== null && value !== undefined);

export const objectKeys = <T extends Dict>(obj: T) =>
  Object.keys(obj) as (keyof T)[];
```

- Now Under `utils/assertion.ts` we have some basic assertions to check if the value is an object, or a function, or an array, etc. Copy the following -

```ts
import { Dict } from "./types";
export const isNull = (value: any): value is null => value === null;

export function isNumber(value: any): value is number {
  return typeof value === "number";
}

export function isArray<T>(value: any): value is Array<T> {
  return Array.isArray(value);
}

export function isFunction(value: any): value is Function {
  return typeof value === "function";
}

export const isObject = (value: any): value is Dict => {
  const type = typeof value;
  return (
    value != null &&
    (type === "object" || type === "function") &&
    !isArray(value)
  );
};
```

- Under `utils/responsive.ts` we have a function called `mapResponsive`. Keep in mind that with styled-system we can pass responsive values to our props like -

```tsx
<Box bg={["blue500", "red400"]}></Box>
<Box bg={{ sm: 'blue500', md: 'orange300' }}></Box>
```

- Let me first paste the code for `mapResponsive` function -

```ts
import { isArray, isObject } from "./assertions";
import { objectKeys } from "./objects";
import { Dict } from "./types";

export function mapResponsive(prop: any, mapper: (val: any) => any) {
  if (isArray(prop)) {
    return prop.map((item) => {
      if (item === null) {
        return null;
      }

      return mapper(item);
    });
  }

  if (isObject(prop)) {
    return objectKeys(prop).reduce((result: Dict, key) => {
      return { ...result, [key]: mapper(prop[key]) };
    }, {});
  }

  if (prop !== null) {
    return mapper(prop);
  }

  return null;
}
```

- The above code is simple to understand, the function receives the responsive prop and a mapper function. It checks if the prop is an array, object and for each value runs the mapper function. You will see later how we are going to use this.

- Now under `utils/index.ts` paste the following -

```ts
export * from "./assertions";
export * from "./dom";
export * from "./objects";
export * from "./responsive";
```

### Grid Component

- Similar to our `Flex` component instead of using the styled-system utility function `flexbox` we will create our own utility props using the system function, so that we can use shorthand names for the props.

- Following is the code -

```tsx
import * as React from "react";
import styled from "styled-components";
import {
  system,
  GridProps as StyledGridProps,
  ResponsiveValue,
  AlignItemsProps,
  JustifyItemsProps,
  AlignSelfProps,
  JustifySelfProps,
  JustifyContentProps,
  AlignContentProps,
} from "styled-system";

import { filterUndefined, mapResponsive } from "../../../../utils";
import { Box, BoxProps } from "../box";

type GridOmitted = "display";

type GridOptions = {
  templateColumns?: StyledGridProps["gridTemplateColumns"];
  rowGap?: StyledGridProps["gridRowGap"];
  columnGap?: StyledGridProps["gridColumnGap"];
  gap?: StyledGridProps["gridGap"];
  autoFlow?: StyledGridProps["gridAutoFlow"];
  autoRows?: StyledGridProps["gridAutoRows"];
  autoColumns?: StyledGridProps["gridAutoColumns"];
  templateRows?: StyledGridProps["gridTemplateRows"];
  templateAreas?: StyledGridProps["gridTemplateAreas"];
  area?: StyledGridProps["gridArea"];
  column?: StyledGridProps["gridColumn"];
  row?: StyledGridProps["gridRow"];
  align?: AlignItemsProps["alignItems"];
  justify?: JustifyItemsProps["justifyItems"];
};

type BaseGridProps = GridOptions &
  BoxProps &
  AlignContentProps &
  JustifyContentProps;

const BaseGrid = styled(Box)<BaseGridProps>`
  display: grid;
  ${system({
    templateAreas: {
      property: "gridTemplateAreas",
    },
    templateColumns: {
      property: "gridTemplateColumns",
    },
    templateRows: {
      property: "gridTemplateRows",
    },
    rowGap: {
      property: "gridRowGap",
      scale: "space",
    },
    columnGap: {
      property: "gridColumnGap",
      scale: "space",
    },
    gap: {
      property: "gridGap",
      scale: "space",
    },
    autoFlow: {
      property: "gridAutoFlow",
    },
    autoRows: {
      property: "gridAutoRows",
    },
    autoColumns: {
      property: "gridAutoColumns",
    },
    area: {
      property: "gridArea",
    },
    column: {
      property: "gridColumn",
    },
    row: {
      property: "gridRow",
    },
    align: {
      property: "alignItems",
    },
    justify: {
      property: "justifyItems",
    },
    alignContent: true,
    justifyContent: true,
  })}
`;

export interface GridProps extends Omit<BaseGridProps, GridOmitted> {}

export const Grid = React.forwardRef<HTMLDivElement, GridProps>(
  (props, ref) => {
    const { children, ...delegated } = props;

    return (
      <BaseGrid ref={ref} {...delegated}>
        {children}
      </BaseGrid>
    );
  }
);
```

### GridItem Component

- Let me first paste the code for this component -

```tsx
type GridItemSpanValue = ResponsiveValue<number | "auto">;

function spanFun(span?: GridItemSpanValue) {
  return mapResponsive(span, (value) => {
    if (!value) return null;

    return value === "auto" ? "auto" : `span ${value}/span ${value}`;
  });
}

type GridItemOptions = {
  colSpan?: GridItemSpanValue;
  colStart?: GridItemSpanValue;
  colEnd?: GridItemSpanValue;
  rowStart?: GridItemSpanValue;
  rowEnd?: GridItemSpanValue;
  rowSpan?: GridItemSpanValue;
};

type BaseGridItemProps = GridItemOptions &
  BoxProps &
  AlignSelfProps &
  JustifySelfProps;

const BaseGridItem = styled(Box)<BaseGridItemProps>`
  ${system({
    colSpan: {
      property: "gridColumn",
    },
    rowSpan: {
      property: "gridRow",
    },
    colStart: {
      property: "gridColumnStart",
    },
    colEnd: {
      property: "gridColumnEnd",
    },
    rowStart: {
      property: "gridRowStart",
    },
    rowEnd: {
      property: "gridRowEnd",
    },
    alignSelf: true,
    justifySelf: true,
  })}
`;

export interface GridItemProps extends BaseGridItemProps {}

export const GridItem = React.forwardRef<HTMLDivElement, GridItemProps>(
  (props, ref) => {
    const {
      colSpan,
      rowSpan,
      colStart,
      colEnd,
      rowStart,
      rowEnd,
      children,
      ...delegated
    } = props;

    const gridItemProps = filterUndefined({
      colSpan: spanFun(colSpan),
      rowSpan: spanFun(rowSpan),
      colStart,
      colEnd,
      rowStart,
      rowEnd,
    });

    return (
      <BaseGridItem ref={ref} {...gridItemProps} {...delegated}>
        {children}
      </BaseGridItem>
    );
  }
);
```

- Here we used our utils functions for `mapResponsive` & `filterUndefined`. You can see the `filterUndefined` function here we filter the undefined or null props.

- The best way to understand this code, is to work with these components at Chakra Docs and read our code. The `spanFun` is a great example if we pass colSpan={2} it will replace this value with the valid css value `span 2 / span 2`. This my friends, this api is so cool, if I had done this I would be okay with the user passing colSpan="span 2 / span 2", but look how chakra simplifies this. That is why try these component libraries and also read their code and implement our own components with it.

### Story

- With the above our `Grid` and `GridItem` components are completed, let us create a story.
- Under the `src/components/atoms/layout/grid/grid.stories.tsx` file we add the below story code.
- We will create 1 single story called Default.

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

import { Grid, GridItem, GridProps } from "./grid";

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

export const Default = {
  render: (args: GridProps) => (
    <Grid
      h="200px"
      templateRows="repeat(2, 1fr)"
      templateColumns="repeat(5, 1fr)"
      gap="md"
      {...args}
    >
      <GridItem rowSpan={2} colSpan={1} bg="tomato" />
      <GridItem colSpan={2} bg="papayawhip" />
      <GridItem colSpan={2} bg="papayawhip" />
      <GridItem colSpan={4} bg="tomato" />
    </Grid>
  ),
};
```

- Now run `npm run storybook` check the stories.

### Build the Library

- Under the `/layout/grid/index.ts` paste the following -

```ts
export * from "./grid";
```

- 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";
export * from "./grid";
```

- Now `npm run build`.

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

```tsx
import * as React from "react";
import { Grid, GridItem } from "chakra-ui-clone";

export function App() {
  return (
    <Grid
      h="200px"
      templateRows="repeat(2, 1fr)"
      templateColumns="repeat(5, 1fr)"
      gap="md"
    >
      <GridItem rowSpan={2} colSpan={1} bg="tomato" />
      <GridItem colSpan={2} bg="papayawhip" />
      <GridItem colSpan={2} bg="papayawhip" />
      <GridItem colSpan={4} bg="tomato" />
    </Grid>
  );
}
```

### Summary

There you go guys in this tutorial we created `Grid` and `GridItem` components just like chakra ui and stories for them. You can find the code for this tutorial under the **atom-layout-grid** branch [here](https://github.com/yaldram/chakra-ui-clone). Use them pass different props, use console.logs throughout the code to understand its working. In the next tutorial we will create a SimpleGrid component. Until next time PEACE.