Skip to content

Instantly share code, notes, and snippets.

@nandorojo
Last active Jul 31, 2022
Embed
What would you like to do?
React Native Grid with Dripsy

Grid

Please consider this code as-is. I made it a long time ago (but still use it) and it's likely imperfect. That said, I made it public because it was requested from this Tweet.

<Grid>
  <Grid.Item>
    <Item />
  </Grid.Item>
  
  <Grid.Item>
    <Item />
  </Grid.Item>
</Grid>

Add a custom gap

<Grid gap={8}>
  <Grid.Item>
    <Item />
  </Grid.Item>
  
  <Grid.Item>
    <Item />
  </Grid.Item>
</Grid>

You can use theme values too:

<Grid gap="$3">
  <Grid.Item>
    <Item />
  </Grid.Item>
  
  <Grid.Item>
    <Item />
  </Grid.Item>
</Grid>

Or make it responsive:

<Grid gap={[0, '$3']}>
  <Grid.Item>
    <Item />
  </Grid.Item>
  
  <Grid.Item>
    <Item />
  </Grid.Item>
</Grid>

Specify number of columns

<Grid columns={[1, 2, 3]}>
  <Grid.Item>
    <Item />
  </Grid.Item>
  
  <Grid.Item>
    <Item />
  </Grid.Item>
</Grid>
import React from 'react'
import { View, useDripsyTheme, useResponsiveValue, Theme as DripsyTheme } from 'dripsy'
import { pickChild } from './pick-child'
type Props = {
/**
* Number of columns that should appear. If you want different columns at different screen sizes, use an array of numbers.
*/
columns?: number | number[]
children: React.ReactNode
/**
* A number in pixels, or a theme value, such as `$3`. If you're using an old version of Dripsy that allowed numbers as theme keys (such as from an array), you can pass `"14px"` instead of `14`.
*/
gap?:
| (number | (string & {}) | keyof DripsyTheme['space'])
| Array<(number | (string & {})) | keyof DripsyTheme['space']>
sx?: React.ComponentProps<typeof View>['sx']
}
function GridItem({ children, ...sx }: { children: React.ReactNode }) {
return <View sx={sx as any}>{children}</View>
}
export default function Grid(props: Props) {
const { sx = {}, children, gap: _gap = 3, ...viewProps } = props
const gap = useResponsiveValue(Array.isArray(_gap) ? _gap : [_gap])
const { space } = useDripsyTheme().theme as any as DripsyTheme
const columnStyle = (numberOfColumns: number, index: number) => {
const getGap = (gap: string | number) => {
let rawPadding = 0
if (typeof gap === 'string' && gap.endsWith('px')) {
rawPadding = Number(gap.replace('px', '')) + 0.0000001
} else if (typeof gap === 'string') {
rawPadding = space[gap]
} else if (typeof gap === 'number') {
rawPadding = space[gap] ?? gap
}
// we need to turn this into a raw value to avoid conflicts with dripsy themes
// for instance, if rawPadding / 2 = 4, and we have theme.space[4] = 8
// it'll incorrectly use 8. By adding .0000001 etc, we hack our way to ensuring we use 4px
// TODO once Dripsy has "10px" feature added, use that instead
const horizontalRawPadding = rawPadding / 2 + 0.000000001
const topRawPadding = rawPadding + 0.000001
return {
horizontalRawPadding,
topRawPadding,
}
}
let horizontalRawPadding: string | number | string[] | number[] = 0
let topRawPadding: string | number | string[] | number[] = 0
;({ horizontalRawPadding, topRawPadding } = getGap(gap))
const isLastItemInRow = (index + 1) % numberOfColumns === 0
const isFirstItemInRow = index % numberOfColumns === 0
const isAfterFirstRow = index + 1 > numberOfColumns
return {
paddingLeft: isFirstItemInRow ? 0 : horizontalRawPadding,
paddingRight: isLastItemInRow ? 0 : horizontalRawPadding,
width: `${(1 / numberOfColumns) * 100}%`,
paddingTop: isAfterFirstRow ? topRawPadding : 0,
}
}
const [, gridItemChildren] = pickChild(children, GridItem)
const gridItemChildrenCount = React.Children.count(gridItemChildren)
const {
// default the number of columns to the number of children, with reasonable limits
columns = [
Math.min(gridItemChildrenCount, 2), // mobile defaults to max 2 items per row
Math.min(gridItemChildrenCount, 3), // tablet defaults to max 3 items per row
Math.min(gridItemChildrenCount, 3), // laptop defaults to max 3 items per row
Math.min(gridItemChildrenCount, 4), // desktop defaults to max 4 items per row
],
} = props
return (
<View {...viewProps} sx={{ flexDirection: 'row', flexWrap: 'wrap', ...sx }}>
{React.Children.map(children, (child: React.ReactElement, index) => {
let paddingLeft: number | number[] = 0
let paddingRight: number | number[] = 0
let paddingTop: number | number[] = 0
let width: string | string[] = '100%'
if (typeof columns === 'number') {
;({ paddingLeft, paddingRight, width, paddingTop } = columnStyle(
columns,
index
))
} else {
const mappedColumns = columns.map((columnCount) =>
columnStyle(columnCount, index)
)
const reducedColumns = mappedColumns.reduce(
(acc, { paddingRight, paddingLeft, width, paddingTop }) => {
return {
// set the responsive width for this column
width: [...acc.width, width],
// these values don't need to be accumulated, since they're the same for a given column
paddingLeft: [...acc.paddingLeft, paddingLeft],
paddingRight: [...acc.paddingRight, paddingRight],
paddingTop: [...acc.paddingTop, paddingTop],
}
},
{
paddingLeft: [],
paddingRight: [],
width: [],
paddingTop: [],
}
)
;({ paddingRight, paddingLeft, width, paddingTop } = reducedColumns)
}
if (!child) return null
return React.cloneElement(child, {
...child.props,
pt: paddingTop,
pr: paddingRight,
width,
pl: paddingLeft,
})
})}
</View>
)
}
Grid.Item = GridItem
/**
* Credit to geist-ui/react for this file, it's copied from there.
*/
import React, { ReactNode } from 'react'
export const getId = () => {
return Math.random().toString(32).slice(2, 10)
}
export const hasChild = (
children: ReactNode | undefined,
child: React.ElementType
): boolean => {
const types = React.Children.map(children, (item) => {
if (!React.isValidElement(item)) return null
return item.type
})
return (types || []).includes(child)
}
export const pickChild = (
children: ReactNode | undefined,
targetChild: React.ElementType
) => {
const target: ReactNode[] = []
const withoutTargetChildren = React.Children.map(children, (item) => {
if (!React.isValidElement(item)) return item
if (item.type === targetChild) {
target.push(item)
return null
}
return item
})
const targetChildren = target.length >= 0 ? target : undefined
return [withoutTargetChildren, targetChildren] as const
}
export const pickChildByProps = (
children: ReactNode | undefined,
key: string,
value: any
): [ReactNode | undefined, ReactNode | undefined] => {
const target: ReactNode[] = []
const isArray = Array.isArray(value)
const withoutPropChildren = React.Children.map(children, (item) => {
if (!React.isValidElement(item)) return null
if (!item.props) return item
if (isArray) {
if (value.includes(item.props[key])) {
target.push(item)
return null
}
return item
}
if (item.props[key] === value) {
target.push(item)
return null
}
return item
})
const targetChildren = target.length >= 0 ? target : undefined
return [withoutPropChildren, targetChildren]
}
export const pickChildrenFirst = (
children: ReactNode | undefined
): ReactNode | undefined => {
return React.Children.toArray(children)[0]
}
export const setChildrenProps = (
children: ReactNode | undefined,
props: object = {},
targetComponents: Array<React.ElementType> = []
): ReactNode | undefined => {
if (React.Children.count(children) === 0) return []
const allowAll = targetComponents.length === 0
const clone = (child: React.ReactElement, props = {}) =>
React.cloneElement(child, props)
return React.Children.map(children, (item) => {
if (!React.isValidElement(item)) return item
if (allowAll) return clone(item, props)
const isAllowed = targetComponents.find((child) => child === item.type)
if (isAllowed) return clone(item, props)
return item
})
}
export const setChildrenIndex = (
children: ReactNode | undefined,
targetComponents: Array<React.ElementType> = []
): ReactNode | undefined => {
if (React.Children.count(children) === 0) return []
const allowAll = targetComponents.length === 0
const clone = (child: React.ReactElement, props = {}) =>
React.cloneElement(child, props)
let index = 0
return React.Children.map(children, (item) => {
if (!React.isValidElement(item)) return item
index = index + 1
if (allowAll) return clone(item, { index })
const isAllowed = targetComponents.find((child) => child === item.type)
if (isAllowed) return clone(item, { index })
index = index - 1
return item
})
}
export const getReactNode = (
node?: React.ReactNode | (() => React.ReactNode)
): React.ReactNode => {
if (!node) return null
if (typeof node !== 'function') return node
return (node as () => React.ReactNode)()
}
export const isChildElement = (
parent: Element | null | undefined,
child: Element | null | undefined
): boolean => {
if (!parent || !child) return false
let node: (Node & ParentNode) | null = child
while (node) {
if (node === parent) return true
node = node.parentNode
}
return false
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment