Last active
December 9, 2024 03:55
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
--> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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