DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Build Chakra UI Grid 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 Grid & GridItem components.

  • I would like you to first check the chakra docs for 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.

Prerequisite

Please check the previous post where we have completed the Wrap Component. Also please check the Chakra Grid Component code here. 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. 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 -
git checkout -b atom-layout-grid
Enter fullscreen mode Exit fullscreen mode
  • 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 -
export type Dict<T = any> = Record<string, T>;

export type FilterFn<T> = (value: any, key: any, object: T) => boolean;
Enter fullscreen mode Exit fullscreen mode
  • Now under utils/objects.ts we will create 2 functions objectFilter, filterUndefined. Let me first explain the javascript code for these -
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);
}
Enter fullscreen mode Exit fullscreen mode
  • 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 -

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)[];
Enter fullscreen mode Exit fullscreen mode
  • 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 -
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)
  );
};
Enter fullscreen mode Exit fullscreen mode
  • 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 -
<Box bg={["blue500", "red400"]}></Box>
<Box bg={{ sm: 'blue500', md: 'orange300' }}></Box>
Enter fullscreen mode Exit fullscreen mode
  • Let me first paste the code for mapResponsive function -
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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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 -

export * from "./assertions";
export * from "./dom";
export * from "./objects";
export * from "./responsive";
Enter fullscreen mode Exit fullscreen mode

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 -

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

GridItem Component

  • Let me first paste the code for this component -
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>
    );
  }
);
Enter fullscreen mode Exit fullscreen mode
  • 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.
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>
  ),
};
Enter fullscreen mode Exit fullscreen mode
  • Now run npm run storybook check the stories.

Build the Library

  • Under the /layout/grid/index.ts paste the following -
export * from "./grid";
Enter fullscreen mode Exit fullscreen mode
  • 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";
export * from "./grid";
Enter fullscreen mode Exit fullscreen mode
  • 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.

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

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. 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.

Discussion (0)