Skip to content

Commit

Permalink
Merge a0a37d4 into 092d883
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismclarke authored Jun 1, 2020
2 parents 092d883 + a0a37d4 commit 8262953
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 75 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"@types/react-select": "^2.0.17",
"@types/react-slick": "0.23.2",
"@types/react-table": "^6.8.5",
"@types/react-virtualized": "^9.21.10",
"@types/rebass": "4",
"@types/storybook__addon-actions": "^3.4.2",
"@types/storybook__addon-info": "^4.1.0",
Expand Down
140 changes: 140 additions & 0 deletions src/components/VirtualizedFlex/VirtualizedFlex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as React from 'react'
import { Flex, Box } from 'rebass'
import themes from 'src/themes/styled.theme'
import {
List,
WindowScroller,
CellMeasurerCache,
CellMeasurer,
ListRowProps,
} from 'react-virtualized'

interface IProps {
data: any[]
renderItem: (data: any) => JSX.Element
widthBreakpoints: number[]
}
interface IState {
totalColumns: number
data: any[]
dataRows: any[][]
}

// User a measurement cache to dynamically calculate dynamic row heights based on content
const cache = new CellMeasurerCache({
fixedWidth: true,
// only use a single key for all rows (assumes all rows the same height)
keyMapper: () => 0,
})
/**
* Display a list of flex items as a virtualized list (only render what is shown),
* automatically calculating the number of rows to be displayed via breakpoints.
* Note, does not use react masonry/grid layouts to allow use with page scroller
*/
export class VirtualizedFlex extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = { data: [], dataRows: [], totalColumns: -1 }
}

componentWillReceiveProps(props: IProps) {
this.generateRowData(props)
}

/**
* Split data into rows and columns depending on breakpoints
*/
generateRowData(props: IProps) {
const oldColumns = this.state.totalColumns
const oldData = this.state.data
const { widthBreakpoints, data } = props
const currentWidth = window.innerWidth
const totalColumns = this._calcTotalColumns(currentWidth, widthBreakpoints)
// only re-render when data or columns have changed
if (oldColumns !== totalColumns || oldData.length !== data.length) {
const dataRows = this._dataToRows(data, totalColumns)
this.setState({ totalColumns, dataRows })
}
}

/**
* When rendering a row call the measurer to check the rendered
* content and update the row height to ensure fit
*/
rowRenderer(rowProps: ListRowProps) {
const { index, key, style, parent } = rowProps
const { renderItem } = this.props
const row = this.state.dataRows![index]
return (
<CellMeasurer
cache={cache}
key={key}
parent={parent}
// simply measure first cell as all will be the same
columnIndex={0}
rowIndex={0}
>
<Flex key={key} style={style}>
{row.map((rowData: any, i: number) => (
<Box key={key + i} width={[1, 1 / 2, 1 / 3]}>
{renderItem(rowData)}
</Box>
))}
</Flex>
</CellMeasurer>
)
}
/**
* Takes an array and subdivides into row/column array
* of arrays for a specified number of columns
*/
private _dataToRows(data: any[], columns: number) {
const totalRows = Math.ceil(data.length / columns)
const rows: (typeof data)[] = []
for (let i = 0; i < totalRows; i++) {
rows.push(data.slice(columns * i, columns * (i + 1)))
}
return rows
}
/**
* Use theme breakpoints to decide how many columns
*/
private _calcTotalColumns(containerWidth: number, breakpoints: number[]) {
return Math.min(
[0, ...breakpoints, Infinity].findIndex(width => containerWidth < width),
breakpoints.length,
)
}

render() {
const { dataRows } = this.state
return dataRows.length > 0 ? (
<WindowScroller onResize={() => this.generateRowData(this.props)}>
{({ onChildScroll, isScrolling, height, width, scrollTop }) => (
<List
autoHeight
height={height}
width={width}
isScrolling={isScrolling}
onScroll={onChildScroll}
scrollTop={scrollTop}
rowCount={dataRows.length}
overscanRowCount={2}
rowRenderer={rowProps => this.rowRenderer(rowProps)}
deferredMeasurementCache={cache}
rowHeight={cache.rowHeight}
/>
)}
</WindowScroller>
) : null
}

static defaultProps: IProps = {
widthBreakpoints: themes.breakpoints.map(
// Convert theme em string to px number
width => Number(width.replace('em', '')) * 16,
),
data: [],
renderItem: data => <div>RenderItem {data}</div>,
}
}
85 changes: 11 additions & 74 deletions src/pages/Howto/Content/HowtoList/HowtoList.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
import * as React from 'react'
import { Flex } from 'rebass'
import { List, WindowScroller } from 'react-virtualized'
// TODO add loader (and remove this material-ui dep)
import { Flex, Box } from 'rebass'
import { Link } from 'src/components/Links'
import TagsSelect from 'src/components/Tags/TagsSelect'

import { inject, observer } from 'mobx-react'
import { HowtoStore } from 'src/stores/Howto/howto.store'
import { Button } from 'src/components/Button'
import { IHowtoDB } from 'src/models/howto.models'
import { AuthWrapper } from 'src/components/Auth/AuthWrapper'
import MoreContainer from 'src/components/MoreContainer/MoreContainer'
import HowToCard from 'src/components/HowToCard/HowToCard'
import Heading from 'src/components/Heading'
import { Loader } from 'src/components/Loader'
import themes from 'src/themes/styled.theme'
import { VirtualizedFlex } from 'src/components/VirtualizedFlex/VirtualizedFlex'

interface InjectedProps {
howtoStore?: HowtoStore
}

interface IState {
isLoading: boolean
// totalHowtoColumns: number
}

const breakpointsInPX = themes.breakpoints.map(
// From em string to px number
breakpoint => Number(breakpoint.replace('em', '')) * 16,
)

// First we use the @inject decorator to bind to the howtoStore state
@inject('howtoStore')
// Then we can use the observer component decorator to automatically tracks observables and re-renders on change
Expand All @@ -46,52 +38,6 @@ export class HowtoList extends React.Component<any, IState> {
return this.props as InjectedProps
}

/**
* Split Howtos into chunks depending on the current window width.
* This determines how many Howtos are displayed in one row. For small screen
* the function won´t split the arrays, as there can only be one Howto displayed in every row
*/
get filteredHowtoChunks(): IHowtoDB[] | IHowtoDB[][] {
const { filteredHowtos } = this.props.howtoStore
const windowWidth = window.innerWidth
let chunk = 1
if (windowWidth > breakpointsInPX[0] && windowWidth <= breakpointsInPX[1]) {
chunk = 2
} else if (windowWidth > breakpointsInPX[1]) {
chunk = 3
}
const howToChunks: IHowtoDB[][] = []
let i = 0
let j = 0
if (chunk > 1) {
for (i = 0, j = filteredHowtos.length; i < j; i += chunk) {
howToChunks.push(filteredHowtos.slice(i, i + chunk))
}
return howToChunks
} else {
return filteredHowtos
}
}

rowRenderer(chunks: IHowtoDB[][] | IHowtoDB[], { index, key, style }) {
const row: IHowtoDB[] | IHowtoDB = chunks[index]
return (
<Flex key={key} style={style}>
{Array.isArray(row) ? (
row.map((howto: IHowtoDB) => (
<Flex key={howto._id} px={4} py={4} width={[1, 1 / 2, 1 / 3]}>
<HowToCard howto={howto} />
</Flex>
))
) : (
<Flex key={row._id} px={4} py={4} width={[1, 1 / 2, 1 / 3]}>
<HowToCard howto={row} />
</Flex>
)}
</Flex>
)
}

public render() {
const { filteredHowtos, selectedTags } = this.props.howtoStore
return (
Expand Down Expand Up @@ -142,24 +88,15 @@ export class HowtoList extends React.Component<any, IState> {
</Heading>
</Flex>
) : (
<Flex flexWrap="wrap" mx={-4}>
<WindowScroller>
{({ height, isScrolling, onChildScroll, scrollTop }) => (
<List
autoHeight
height={height}
width={window.innerWidth}
isScrolling={isScrolling}
onScroll={onChildScroll}
scrollTop={scrollTop}
rowCount={this.filteredHowtoChunks.length}
rowHeight={410}
rowRenderer={data =>
this.rowRenderer(this.filteredHowtoChunks, data)
}
/>
<Flex justifyContent={'center'} mx={-4}>
<VirtualizedFlex
data={filteredHowtos}
renderItem={data => (
<Box px={4} py={4}>
<HowToCard howto={data} />
</Box>
)}
</WindowScroller>
/>
</Flex>
)}
<Flex justifyContent={'center'} mt={20}>
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"declaration": true,
"outDir": "./lib"
},
"include": ["src", "types"],
"include": ["src/**/*", "types"],
"exclude": [
"node_modules",
"build",
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3821,6 +3821,14 @@
dependencies:
"@types/react" "*"

"@types/react-virtualized@^9.21.10":
version "9.21.10"
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.10.tgz#cd072dc9c889291ace2c4c9de8e8c050da8738b7"
integrity sha512-f5Ti3A7gGdLkPPFNHTrvKblpsPNBiQoSorOEOD+JPx72g/Ng2lOt4MYfhvQFQNgyIrAro+Z643jbcKafsMW2ag==
dependencies:
"@types/prop-types" "*"
"@types/react" "*"

"@types/react@*", "@types/react@16.8.4":
version "16.8.4"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.4.tgz#134307f5266e866d5e7c25e47f31f9abd5b2ea34"
Expand Down

0 comments on commit 8262953

Please sign in to comment.