Skip to content

Instantly share code, notes, and snippets.

@iserdmi
Last active January 30, 2023 11:00
Show Gist options
  • Save iserdmi/bc0e35051d0a8bc1ebf20476df466abd to your computer and use it in GitHub Desktop.
Save iserdmi/bc0e35051d0a8bc1ebf20476df466abd to your computer and use it in GitHub Desktop.
ColumnFiller
// Live: https://startupgrad.io/#services
<ColumnFiller
items={services
.filter((service) => !selectedTag || service.tags.includes(selectedTag))
.map((service) => ({ ...service, key: service.textsKey }))}
columnsCount={windowWidth > 900 ? 3 : windowWidth > 600 ? 2 : 1}
columnRender={({ children, index, width }) => (
<div key={index} style={{ width }} className={css.column}>
{children}
</div>
)}
itemRender={({ item: service, setItemRef }) => (
<Service key={service.textsKey} setItemRef={setItemRef} service={service} />
)}
/>
import _ from 'lodash'
import React, { useEffect, useRef, useState } from 'react'
import { useWindowSize } from '../../lib/useWindowSize'
type ItemType<TItem extends { key: string } = { key: string }> = TItem
type ItemRender<TItem extends ItemType = ItemType> = (props: {
item: TItem
index: number
setItemRef: React.RefCallback<any>
}) => JSX.Element
const Item: React.FC<{
index: number
item: ItemType<any>
itemRender: ItemRender<any>
itemHeights: Record<string, number>
setItemHeights: React.Dispatch<React.SetStateAction<Record<string, number>>>
}> = ({ index, itemRender, item, setItemHeights, itemHeights }) => {
const { width: windowWidth } = useWindowSize()
const itemRef = useRef<HTMLElement | null>()
useEffect(() => {
const height = itemRef.current?.offsetHeight || 0
if (itemHeights[item.key] !== height) {
setItemHeights({
...itemHeights,
[item.key]: height,
})
}
}, [itemRef, setItemHeights, itemHeights, item.key, windowWidth])
return itemRender({
item,
index,
setItemRef: (ref) => (itemRef.current = ref),
})
}
export const ColumnFiller = <TItem extends ItemType = ItemType>({
items,
columnRender,
columnsCount,
itemRender,
}: {
items: TItem[]
columnRender: (props: { index: number; children: React.ReactNode; width: string }) => JSX.Element
columnsCount: number
itemRender: ItemRender<TItem>
}) => {
const [itemsWithColumnIndex, setItemsWithColumnIndex] = useState<Array<{ item: TItem; columnIndex: number }>>(
items.map((item) => ({ item, columnIndex: 0 }))
)
const columnWidth = `${(100 / columnsCount).toFixed(3)}%`
const [itemHeights, setItemHeights] = useState<Record<string, number>>({})
useEffect(() => {
const normalizeItemsWithColumnIndex = () => {
const newItemsWithColumnIndex: Array<{ item: TItem; columnIndex: number }> = []
const columnsHeights = _.times(columnsCount, () => 0)
for (const item of items) {
const itemHeight = itemHeights[item.key] || 0
const lowestColumnIndex = _.minBy(_.range(columnsCount), (columnIndex) => columnsHeights[columnIndex]) || 0
columnsHeights[lowestColumnIndex] += itemHeight
newItemsWithColumnIndex.push({ item, columnIndex: lowestColumnIndex })
}
setItemsWithColumnIndex(newItemsWithColumnIndex)
}
normalizeItemsWithColumnIndex()
}, [items, itemHeights, columnsCount])
return (
<>
{_.times(columnsCount, (columnIndex) => {
return columnRender({
index: columnIndex,
width: columnWidth,
children: (
<>
{itemsWithColumnIndex.map(
(itemWithColumnIndex, itemIndex) =>
columnIndex === itemWithColumnIndex.columnIndex && (
<Item
key={itemWithColumnIndex.item.key}
item={itemWithColumnIndex.item}
index={itemIndex}
itemRender={itemRender}
itemHeights={itemHeights}
setItemHeights={setItemHeights}
/>
)
)}
</>
),
})
})}
</>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment