Created
July 3, 2017 20:46
-
-
Save bradennapier/b76eee038d3a8d438c4f769524562e93 to your computer and use it in GitHub Desktop.
This file contains 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, { Component } from 'react'; | |
import PropTypes from 'prop-types'; | |
import componentQueries from 'react-component-queries'; | |
import { connect } from 'react-redux'; | |
import { connectProcesses } from 'redux-saga-process'; | |
import _ from 'lodash'; | |
const DEFAULT_CONFIG = { | |
provider: false, | |
throttle: true, | |
// TO DO : Adjust this if rendering is too slow! | |
throttleTimeout: 1000, | |
measure: { | |
config: { | |
monitorWidth: true, | |
monitorHeight: true, | |
monitorPosition: true, | |
sizePassthrough: 'componentSize', | |
}, | |
queries: [], | |
}, | |
// extra queries? | |
// modify the default breakpoints? | |
breakpoints: undefined, | |
}; | |
const buildConfig = config => _.merge(Object.create(null), DEFAULT_CONFIG, config); | |
const ConnectedWidget = config => WrappedComponent => { | |
class WidgetWrapperComponent extends Component { | |
constructor(props, context) { | |
super(props, context); | |
this.dashboard = context.dashboard; | |
this.timeouts = {}; | |
this.childProps = {}; | |
this.status = undefined; | |
} | |
componentWillMount() { | |
// console.log('Connector Mount') | |
this.status = 'mounting'; | |
this.childProps = Object.assign( | |
this.childProps, | |
this.dashboard.subscribe(this, 'will_mount', config), | |
); | |
} | |
componentDidMount() { | |
this.status = 'mounted'; | |
this.childProps = Object.assign( | |
this.childProps, | |
this.dashboard.subscribe(this, 'did_mount', config), | |
); | |
} | |
componentWillUnmount() { | |
// console.log('Connector Unmount') | |
this.dashboard.subscribe(this, 'will_unmount', config); | |
this.status = 'unmounted'; | |
for (const timeoutID in this.timeouts) { | |
clearTimeout(this.timeouts[timeoutID]); | |
} | |
} | |
dashboardDidUpdate = grid => { | |
if (this.status !== 'mounted') { | |
return; | |
} | |
// console.log('Dashboard Did Update: ', grid) | |
this.childProps.grid = grid; | |
this.forceUpdate(); | |
}; | |
/* | |
When our context updates, we will check if our child has provided | |
the dashboardContextDidUpdate function. If it has, we allow the | |
child to handle the update logic. If not, every update to context | |
will update the given widget. | |
*/ | |
dashboardContextDidUpdate = context => { | |
// console.log('Should Update Conneector?') | |
if (this.status !== 'mounted') { | |
return; | |
} | |
if (this.wrappedRef) { | |
if (this.wrappedRef.dashboardContextDidUpdate) { | |
// Allow the child to handle the update | |
return this.wrappedRef.dashboardContextDidUpdate(context); | |
} | |
} | |
return this.forceUpdate(); | |
}; | |
// Throttle Re-Renders that might occur on a widget. This includes updates | |
// from other widgets updating the context of the dashboard. | |
shouldComponentUpdate(np) { | |
if (config.throttle === true) { | |
if (!this.lastRender) { | |
return true; | |
} | |
if (Date.now() - this.lastRender <= config.throttleTimeout) { | |
return this.throttledUpdate(); | |
} | |
return true; | |
} | |
return true; | |
} | |
// When this is called we will schedule an update of the component that | |
// will occur when the throttle period has completed. This helps us to stop | |
// updates from occurring too quickly while allowing us to confirm all requested | |
// updates will be rendered. | |
throttledUpdate = () => { | |
if (this.timeouts.throttler) { | |
// Our throttler has already been scheduled, do nothing. | |
return false; | |
} | |
this.timeouts.throttler = setTimeout(() => { | |
delete this.timeouts.throttler; | |
this.forceUpdate(); | |
}, config.throttleTime); | |
return false; | |
}; | |
render() { | |
if (config.throttle === true) { | |
this.lastRender = Date.now(); | |
} | |
const wrappedProps = { | |
...this.childProps, | |
shared: this.dashboard.getSharedContext(), | |
ref: ref => (this.wrappedRef = ref), | |
...this.props, | |
}; | |
return <WrappedComponent {...wrappedProps} />; | |
} | |
} | |
WidgetWrapperComponent.contextTypes = { | |
dashboard: PropTypes.shape({ | |
getGrid: PropTypes.func.isRequired, | |
subscribe: PropTypes.func.isRequired, | |
setSharedContext: PropTypes.func.isRequired, | |
getSharedContext: PropTypes.func.isRequired, | |
}).isRequired, | |
}; | |
return WidgetWrapperComponent; | |
}; | |
const BREAKPOINTS = { | |
xs: 300, | |
sm: 400, | |
md: 600, | |
lg: 900, | |
}; | |
function withConnectedProcesses(config, connect) { | |
return connectProcesses(config.processes, connect); | |
} | |
function withConnectedRedux(config) { | |
return (selected = {}) => | |
connect( | |
state => config.connect(state, selected), | |
// does the widget define a function to receive and return | |
// action creators? | |
config.actions && config.actions(selected), | |
); | |
} | |
function withConnectedSizeQueries(config) { | |
const { measure } = config; | |
return componentQueries({ | |
queries: [ | |
({ height, width, position }, props) => { | |
const breakpoints = config.breakpoints | |
? { ...BREAKPOINTS, ...config.breakpoints } | |
: BREAKPOINTS; | |
const response = { breakpointHeight: '', breakpointWidth: '' }; | |
for (const prop of ['Width', 'Height']) { | |
const op = prop === 'Width' ? width : height; | |
if (op <= breakpoints.xs) { | |
response[`breakpoint${prop}`] = 1; | |
} else if (op <= breakpoints.sm) { | |
response[`breakpoint${prop}`] = 2; | |
} else if (op <= breakpoints.md) { | |
response[`breakpoint${prop}`] = 3; | |
} else if (op <= breakpoints.lg) { | |
response[`breakpoint${prop}`] = 4; | |
} else { | |
response[`breakpoint${prop}`] = 5; | |
} | |
} | |
if (props.device && props.device.isMobile) { | |
response._update = {}; | |
} | |
return response; | |
}, | |
...(measure.queries || []), | |
], | |
config: measure.config || {}, | |
}); | |
} | |
export function connectWidget(_config) { | |
/* | |
Merge our received config with the default configuration (deeply). | |
This configuration will determine the component that we will output. | |
*/ | |
const config = buildConfig(_config); | |
const { | |
// what is the component that will be rendered? | |
component, | |
// do we want to import selectors/actions from processes? | |
processes, | |
// do we want to connect the widget to redux? | |
connect, | |
// which imported actions should we connect to dispatch? | |
actions, | |
// do we want to measure the component? | |
measure, | |
// is this a WidgetProvider ? | |
provider, | |
} = config; | |
// console.log('Config: ', config); | |
const connectedWidget = ConnectedWidget(config)(component); | |
let connectedComponent; | |
if (processes && connect) { | |
// connect to processes && redux -- returns the result of | |
// connect() (a function) | |
connectedComponent = withConnectedProcesses(config, withConnectedRedux(config)); | |
} else if (connect) { | |
// connect to redux but not processes. We call the connector function | |
// then call the return function with no selected values. This way we | |
// get back the result of connect() | |
connectedComponent = withConnectedRedux(config)(); | |
} | |
if (config.measure) { | |
// If we want to measure the widgets changes in size: | |
if (connectedComponent) { | |
// we either have redux or redux && processes connected! | |
connectedComponent = connectedComponent(withConnectedSizeQueries(config)(connectedWidget)); | |
} else { | |
connectedComponent = withConnectedSizeQueries(config)(connectedWidget); | |
} | |
} else if (connectedComponent) { | |
connectedComponent = connectedComponent(connectedWidget); | |
} else { | |
connectedComponent = connectedWidget; | |
} | |
// finally, return our configured component | |
return connectedComponent; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment