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
Boxcomponent to create theGrid&GridItemcomponents. - 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
Under the
components/atoms/layoutfolder create a new folder calledgrid. Under grid folder create 3 filesgrid.tsxandgrid.stories.tsxandindex.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& finallyutils/responsive.ts.
Util Functions
- First under
utils/types.tspaste the following code -
export type Dict<T = any> = Record<string, T>;
export type FilterFn<T> = (value: any, key: any, object: T) => boolean;
- Now under
utils/objects.tswe will create 2 functionsobjectFilter, 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);
}
objectFiltertakes 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
filterUndefinedfunction 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
filterUndefinedI would directly write the code for this use case, but this code fromchakra/utilsis 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 ourutils/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)[];
- Now Under
utils/assertion.tswe 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)
);
};
- Under
utils/responsive.tswe have a function calledmapResponsive. 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>
- Let me first paste the code for
mapResponsivefunction -
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.tspaste the following -
export * from "./assertions";
export * from "./dom";
export * from "./objects";
export * from "./responsive";
Grid Component
Similar to our
Flexcomponent instead of using the styled-system utility functionflexboxwe 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>
);
}
);
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>
);
}
);
Here we used our utils functions for
mapResponsive&filterUndefined. You can see thefilterUndefinedfunction 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
spanFunis a great example if we pass colSpan={2} it will replace this value with the valid css valuespan 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
GridandGridItemcomponents are completed, let us create a story. - Under the
src/components/atoms/layout/grid/grid.stories.tsxfile 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>
),
};
- Now run
npm run storybookcheck the stories.
Build the Library
- Under the
/layout/grid/index.tspaste the following -
export * from "./grid";
- Under the
/layout/index.tsfile and paste the following -
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.tsxwe can test ourGridcomponent. Copy paste the following code and runnpm run startfrom theexampledirectory.
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. 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.
Top comments (0)