Skip to content

Instantly share code, notes, and snippets.

@Craga89
Created February 21, 2018 16:40
Show Gist options
  • Save Craga89/acaf15f25122b9ed4bf29ad7e90ed722 to your computer and use it in GitHub Desktop.
Save Craga89/acaf15f25122b9ed4bf29ad7e90ed722 to your computer and use it in GitHub Desktop.
Generic withGridFunctionality HOC
import PropTypes from 'prop-types';
/**
* Generates a map of query parameters and their associated values when given
* a `queryProps` map and some `props` values.
*
* @param queryProps {Object} Map of prop names to associated queryProp definitions
* @param props {Object} Current props to generate values from
* @returns {Object} Query parameters map
*/
const generateQueryValues = (queryProps, props) =>
Object.keys(queryProps).reduce((memo, propName) => {
const { param, defaultValue } = queryProps[propName];
const value = props[propName];
if (value !== defaultValue && value !== null) {
memo[param] = value;
}
return memo;
}, {});
/**
* Generates a new component with common Grid functionality, such as fetch and toastr handling.
*
* @method withGridFunctionality
* @param options {Object} Options object
* @param options.url {String} URL the Grid collection is addressable by
* @param [options.resetProps=[]] {Array<String>} Array of prop names that when changed, cause `fetchReset` to be called
* @param [options.queryProps={}] {Object} Map of props to reflect in the URL as query parameters
* @returns {Object} Higher Order Component
*/
export default ({
url,
resetProps = [],
queryProps = {}
}) => compose(
// Used by `withHandlers` below to show toaster popups
getContext({
showToaster: PropTypes.func
}),
injectData({
url,
dataProp: 'items',
requestProp: 'requestItems',
loadingProp: 'isItemsLoading',
resetProp: 'resetItems',
initialData: []
}),
// Introduces new `fetchReset` and `fetchNext` props, which can be used by lower components
// to programatically reset the grid to initial results, or fetch the next page when needed
withHandlers(() => {
let index = 0;
let pageSize = 30;
let isFetching = false;
// Stop React Virtualized from trying to fetch the next set of records if number of results is within the threshold.
let lastFetchWithinThreshold = false;
const fetch = ({ data, requestData }) => {
isFetching = true;
return requestData('get', {
data
})
.then((data) => {
isFetching = false;
lastFetchWithinThreshold = data.items.length <= 5;
return data;
})
.catch((e) => {
isFetching = false;
lastFetchWithinThreshold = false;
throw e;
});
};
return {
fetchReset: (props) => debounce(async() => {
index = 0;
pageSize = 30;
await fetch(props);
}, 300),
fetchNext: (props) => debounce(async(force = false) => {
const shouldBypass = !force && (isFetching || lastFetchWithinThreshold);
lastFetchWithinThreshold = false;
if (shouldBypass) {
return;
}
// Move to the next page index
index += pageSize;
// Fetch the new data
const data = await fetch(props);
// Account for scenario where we are given less items than we asked for
index -= pageSize - data.items.length;
// Merge the items into the existing items array, de-duping
props.setItems(mergeData(props.items, data.items));
// No more records? Show the toaster
if (data && data.items.length < 1) {
props.showToaster({
text: 'No more records are currently available'
});
}
},
300)
};
}),
// Handles `fetchReset` calls when resetProps change, and updates query parameters
// defined in the `queryProps` to reflect internal prop state when props update.
lifecycle({
componentDidMount() {
this.props.fetchReset();
},
componentDidUpdate(prevProps) {
// Do we need to reset?
Object.keys(resetProps).forEach((propName) => {
if (this.props[propName] !== prevProps[propName]) {
this.props.fetchReset();
}
});
// Do we need to update the query string?
const shouldQueryUpdate = Object.keys(queryProps).some((key) =>
this.props[key] !== prevProps[key]
);
if (shouldQueryUpdate) {
const queryValues = generateQueryValues(queryProps, this.props);
history.pushState(queryValues, null, '?' + querystring.stringify(queryValues));
}
}
}),
// Passes down the exposed `list.scrollToRow` functionality as a prop
// to the rest of the Grid so it's easy to programatically scroll later on
lifecycle({
componentDidMount() {
this.setState({
onListReady: (list) => {
this._list = list;
const { onListReady } = this.props;
if (onListReady) {
onListReady(list);
}
},
scrollToRow: (index) =>
this._list.scrollToRow(index)
});
},
componentWillUnmount() {
this._list = null;
}
}),
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment