Skip to content

Instantly share code, notes, and snippets.

@trinhvanminh
Last active December 9, 2024 03:55
Show Gist options
  • Save trinhvanminh/9954f56808a176975a88709e2a7bc022 to your computer and use it in GitHub Desktop.
Save trinhvanminh/9954f56808a176975a88709e2a7bc022 to your computer and use it in GitHub Desktop.
Auto Grid with props xs, ... as a List. GridLayout, GridContainer, GridItem, Container (based on Mui Grid v2, Mui Container)
// Define MUI breakpoints
$breakpoints: (
xs: 0,
sm: 600px,
md: 900px,
lg: 1200px,
xl: 1536px,
) !default;
// Define the grid system variables
$grid-columns: 12 !default;
$grid-gutter-width-xs: 1rem !default;
$grid-gutter-width-sm: 1.5rem !default;
// Container padding
$container-padding-x: $grid-gutter-width-xs !default;
$container-padding-x-sm: $grid-gutter-width-sm !default;
// Import the variables file to access the breakpoints and other variables
@import './variables';
/* Base container styles */
.root {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: $container-padding-x;
padding-right: $container-padding-x;
box-sizing: border-box;
@media (min-width: map-get($breakpoints, sm)) {
padding-left: $container-padding-x-sm;
padding-right: $container-padding-x-sm;
}
// Disable gutters: no padding
&.disableGutters {
padding-left: 0;
padding-right: 0;
}
// Fixed width container
&.fixed {
@each $size, $value in $breakpoints {
@if ($value != 0) {
@media (min-width: $value) {
max-width: $value;
}
}
}
}
// Loop through the breakpoints to apply max-width for each size
@each $size, $value in $breakpoints {
&.maxWidth-#{$size} {
max-width: $value;
}
}
}
@import './variables';
.container {
display: flex;
flex-wrap: wrap;
min-width: 0;
box-sizing: border-box;
--Grid-columns: #{$grid-columns};
gap: var(--Grid-rowSpacing, 0px) var(--Grid-columnSpacing, 0px);
> * {
--Grid-parent-rowSpacing: var(--Grid-rowSpacing, 0px);
--Grid-parent-columnSpacing: var(--Grid-columnSpacing, 0px);
--Grid-parent-columns: var(--Grid-columns, 12);
}
}
// Mixin for grid size definitions
@mixin generate-grid($prefix, $columns, $breakpoint) {
@if $breakpoint != null {
@media (min-width: map-get($breakpoints, $breakpoint)) {
@for $i from 0 through $columns {
.#{$prefix}-#{$i} {
--size: #{$i};
}
}
}
} @else {
@for $i from 0 through $columns {
.#{$prefix}-#{$i} {
--size: #{$i};
}
}
}
}
// Generate classes for each breakpoint
// XS (Extra Small) - <600px
@include generate-grid('grid-xs', $grid-columns, null);
// SM (Small) - 600px and up
@include generate-grid('grid-sm', $grid-columns, sm);
// MD (Medium) - 900px and up
@include generate-grid('grid-md', $grid-columns, md);
// LG (Large) - 1200px and up
@include generate-grid('grid-lg', $grid-columns, lg);
// XL (Extra Large) - 1536px and up
@include generate-grid('grid-xl', $grid-columns, xl);
// Grid item style
.item {
flex-grow: 0;
flex-basis: auto;
width: calc(
100% * var(--size, var(--Grid-parent-columns)) / var(--Grid-parent-columns) -
(var(--Grid-parent-columns) - var(--size, var(--Grid-parent-columns))) *
(var(--Grid-parent-columnSpacing) / var(--Grid-parent-columns))
);
min-width: 0;
box-sizing: border-box;
}
import { Box, Grid } from '@mui/material';
import { SystemProps } from '@mui/system';
import React from 'react';
// MUI GRID
const GridLayout = (props: GridLayoutProps & SystemProps) => {
const { children, xs, sm, md, lg, xl, spacing, alignItems, ...otherProp } = props;
return (
<Box width={1} {...otherProp}>
<Grid container alignItems={alignItems} spacing={spacing}>
{React.Children.map(children, (node: any, index: number) => {
return (
node && (
<Grid
key={index}
xs={xs.at(index % xs.length)}
sm={sm?.at(index % sm?.length)}
md={md?.at(index % md?.length)}
lg={lg?.at(index % lg?.length)}
xl={xl?.at(index % xl?.length)}
item
>
{node}
</Grid>
)
);
})}
</Grid>
</Box>
);
};
export interface GridLayoutProps {
sx?: any;
xs: number[];
md?: number[];
sm?: number[];
lg?: number[];
xl?: number[];
spacing?: number;
alignItems?: any;
children?: any;
}
export default GridLayout;
<!--
<GridLayout xs={[3, 9]} spacing={2} alignItems="center">
<Label text="Email" required /> <--- Grid 3
<TextField <--- Grid 9
field="email"
model={user}
validation={validation}
onChange={setUser}
disabled={loading || !isCreate}
/>
<Label text="Email" required /> <--- Grid 3
<TextField <--- Grid 9
field="email"
model={user}
validation={validation}
onChange={setUser}
disabled={loading || !isCreate}
/>
</GridLayout>
-->
import { FC, PropsWithChildren } from 'react';
import styles from './Container.module.scss';
const Container2: FC<PropsWithChildren<Container2Props>> = ({
children,
disableGutters = false,
fixed = false,
maxWidth = 'lg',
className,
style,
}) => {
const classNames = [styles.root];
if (className) {
classNames.push(className);
}
if (disableGutters) {
classNames.push(styles.disableGutters);
}
if (fixed) {
classNames.push(styles.fixed);
} else if (maxWidth) {
classNames.push(styles[`maxWidth-${maxWidth}`]);
}
return (
<div className={classNames.join(' ')} style={style}>
{children}
</div>
);
};
export default Container2;
export type Container2Props = {
/**
* If `true`, the left and right padding is removed.
* @default false
*/
disableGutters?: boolean;
/**
* Set the max-width to match the min-width of the current breakpoint.
* This is useful if you'd prefer to design for a fixed set of sizes
* instead of trying to accommodate a fully fluid viewport.
* It's fluid by default.
* @default false
*/
fixed?: boolean;
/**
* Determine the max-width of the container.
* The container width grows with the size of the screen.
* Set to `false` to disable `maxWidth`.
* @default 'lg'
*/
maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | false;
className?: string;
style?: React.CSSProperties;
};
import styled from '@emotion/styled';
import { FC, PropsWithChildren } from 'react';
import styles from './Grid.module.scss';
import { breakpoints, type ResponsiveStyleValue } from './GridLayout2';
const GRID_CONTAINER_CLASS_NAME = 'container';
const Container = styled.div<ContainerProps>`
${(props) => {
const { $rowSpacing, $columnSpacing } = props;
let styles = '';
// Iterate through the rowSpacing object first and generate media queries for each breakpoint
if (typeof $rowSpacing === 'object') {
Object.keys($rowSpacing).forEach((key) => {
const minWidth = breakpoints[key as keyof typeof breakpoints];
styles += `
@media (min-width: ${minWidth}) {
--Grid-rowSpacing: ${($rowSpacing[key as keyof typeof $rowSpacing] || 0) * 8}px;
}
`;
});
} else {
styles += `--Grid-rowSpacing: ${(($rowSpacing as number) || 0) * 8}px;`;
}
// Handle Column Spacing After Row Spacing
if (typeof $columnSpacing === 'object') {
Object.keys($columnSpacing).forEach((key) => {
const minWidth = breakpoints[key as keyof typeof breakpoints];
styles += `
@media (min-width: ${minWidth}) {
--Grid-columnSpacing: ${($columnSpacing[key as keyof typeof $columnSpacing] || 0) * 8}px;
}
`;
});
} else {
styles += `--Grid-columnSpacing: ${(($columnSpacing as number) || 0) * 8}px;`;
}
return styles;
}}
`;
const GridContainer: FC<PropsWithChildren<GridContainerProps>> = ({ children, spacing, rowSpacing, columnSpacing }) => {
return (
<Container
className={styles[GRID_CONTAINER_CLASS_NAME]}
$rowSpacing={rowSpacing || spacing}
$columnSpacing={columnSpacing || spacing}
>
{children}
</Container>
);
};
export default GridContainer;
type ContainerProps = {
$rowSpacing?: GridContainerProps['rowSpacing'];
$columnSpacing?: GridContainerProps['columnSpacing'];
};
export type GridContainerProps = {
spacing?: ResponsiveStyleValue;
rowSpacing?: ResponsiveStyleValue;
columnSpacing?: ResponsiveStyleValue;
};
import { FC, PropsWithChildren } from 'react';
import styles from './Grid.module.scss';
import { ResponsiveStyleValue } from './GridLayout2';
const GRID_ITEM_CLASS_NAME = 'item';
const GRID_RESPONSIVE_PREFIX_CLASS_NAME = 'grid';
const GridItem: FC<PropsWithChildren<GridItemProps>> = ({ size = 0, children }) => {
if (!children) return null;
let className = '';
if (typeof size === 'number') {
className = `${GRID_RESPONSIVE_PREFIX_CLASS_NAME}-xs-${size}`;
return <div className={`${styles[GRID_ITEM_CLASS_NAME]} ${className}`}>{children}</div>;
}
if (typeof size === 'object') {
const responsiveClassNames = Object.keys(size).reduce<string[]>((acc, key) => {
const value = size[key as keyof typeof size];
if (value !== undefined && value !== null) {
acc.push(`${GRID_RESPONSIVE_PREFIX_CLASS_NAME}-${key}-${value}`);
}
return acc;
}, []);
const styleClassNames = responsiveClassNames.map((c) => styles[c]);
className = styleClassNames.join(' ');
return <div className={`${styles[GRID_ITEM_CLASS_NAME]} ${className}`}>{children}</div>;
}
return null;
};
export default GridItem;
type GridItemProps = {
size?: ResponsiveStyleValue;
};
import React from 'react';
import GridContainer, { GridContainerProps } from './GridContainer';
import GridItem from './GridItem';
export const breakpoints = {
xs: 0,
sm: '600px',
md: '900px',
lg: '1200px',
xl: '1536px',
} as const;
export type ResponsiveStyleValue = number | Record<keyof typeof breakpoints, number>;
// VANILLA GRID
const GridLayout2 = (props: GridLayout2Props) => {
const { children, xs, sm, md, lg, xl, spacing, rowSpacing, columnSpacing, style } = props;
return (
<div style={{ width: '100%', ...style }}>
<GridContainer spacing={spacing} rowSpacing={rowSpacing} columnSpacing={columnSpacing}>
{React.Children.map(children, (node, index) => {
const size = {
xs: xs?.at(index % xs.length),
sm: sm?.at(index % sm?.length),
md: md?.at(index % md?.length),
lg: lg?.at(index % lg?.length),
xl: xl?.at(index % xl?.length),
};
return (
<GridItem key={index} size={size as ResponsiveStyleValue}>
{node}
</GridItem>
);
})}
</GridContainer>
</div>
);
};
export interface GridLayout2Props extends GridContainerProps {
xs: number[];
md?: number[];
sm?: number[];
lg?: number[];
xl?: number[];
style?: React.CSSProperties;
children?: React.ReactElement[];
}
export default GridLayout2;
// utils
/**
* Separates items into multiple arrays based on the number of columns.
* Each sub-array represents a "column" and contains a portion of the items.
* The items are **distributed cyclically** across the columns.
*
* @param {T[]} items - The array of items to be separated.
* @param {number} [columns=2] - The number of columns to distribute the items into. Defaults to 2.
* @returns {Array<T[]>} - A 2D array where each sub-array represents a column.
*
* @example
* const items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
* const columns = 3;
* console.log(groupItemsByColumns(items, columns));
* // Output: [['a', 'd', 'g'], ['b', 'e', 'h'], ['c', 'f']]
*
* @example
* const items = ['apple', 'banana', 'cherry', 'date', 'elderberry'];
* console.log(groupItemsByColumns(items, 2));
* // Output: [['apple', 'date'], ['banana', 'cherry', 'elderberry']]
*/
export function groupItemsByColumns<T>(items: T[], columns: number = 2): Array<T[]> {
const result: Array<T[]> = Array.from({ length: columns }, () => []);
items.forEach((item, index) => {
result[index % columns].push(item);
});
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment