Skip to content

Instantly share code, notes, and snippets.

@mdarse
Last active May 2, 2017 09:34
Show Gist options
  • Save mdarse/e1a2e791886bde678713f1ab1115a2c6 to your computer and use it in GitHub Desktop.
Save mdarse/e1a2e791886bde678713f1ab1115a2c6 to your computer and use it in GitHub Desktop.
Scaler component to display some inner tree like a full page preview would do (including media queries).
import { Component, PropTypes } from 'react';
import stylePropType from 'react-style-proptype';
// TODO For SSR support, use this: https://github.com/bvaughn/react-virtualized/blob/78910b626b3c0d31d2a791a7a3f1c0dcaa56a25e/source/vendor/detectElementResize.js
// TODO Debounce window resize handler
function resetMeasurements() {
return {
windowWidth: document.documentElement.offsetWidth,
scalerWidth: null,
contentHeight: null,
};
}
function px(value) {
return `${value}px`;
}
function outerDivStyle(contentHeight, scale) {
const style = { overflow: 'hidden' };
if (contentHeight !== null && scale !== null) {
// third render (and each time after second render step)
style.height = px(contentHeight * scale);
}
return style;
}
function innerDivStyle(windowWidth, scale) {
return {
width: px(windowWidth),
transform: `scale(${scale})`,
transformOrigin: 'top left',
};
}
export default class Scaler extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
style: stylePropType,
};
static defaultProps = {
style: {},
};
constructor(props) {
super(props);
this.state = resetMeasurements();
this.lastScale = null;
this.handleResize = this.handleResize.bind(this);
this.outerDiv = null;
this.innerDiv = null;
}
componentDidMount() {
window.addEventListener('resize', this.handleResize);
this.updateScalerWidth();
}
componentWillReceiveProps(nextProps) {
if (this.props.children !== nextProps.children) {
this.setState({ contentHeight: null });
}
}
componentDidUpdate() {
if (this.state.scalerWidth === null) {
this.updateScalerWidth();
}
if (this.state.contentHeight === null && this.innerDiv !== null) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ contentHeight: this.innerDiv.offsetHeight });
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
handleResize() {
this.setState(resetMeasurements);
}
updateScalerWidth() {
this.setState({ scalerWidth: this.outerDiv.offsetWidth });
}
render() {
const { children, style } = this.props;
const { windowWidth, scalerWidth, contentHeight } = this.state;
// We are doing here a three-step render here:
// - *first* without content since we don't know the scale until we measure width of the
// outer div element (no explicit height on the outer div element yet)
// - a *second time* with content since we know the scale from outer div width (no explicit
// height on the outer div element neither)
// - and a *third time* with an explicit height set on the outer element. This height is set
// according to the measurment we made on the inner div element, after the above render.
//
// The first render step is only done once after mount, each measurement reset (after
// resize, or children change) begins at second render step. In that case, the last known
// rendered scale is used (to prevent the blink we would have on first step).
//
// We assume here that the height set on the third render doesn't impact the height of the
// inner div element. This should be guaranteed by the "overflow: hidden" herebelow. We also
// assume that the content doesn't impact the width of the outer div element.
if (scalerWidth !== null) {
// second render (and after each measurement reset)
this.lastScale = scalerWidth / windowWidth;
}
const scale = this.lastScale;
const outerStyle = {
...style,
...outerDivStyle(contentHeight, scale),
};
return (
<div ref={(ref) => { this.outerDiv = ref; }} style={outerStyle}>
{scale !== null &&
<div
ref={(ref) => { this.innerDiv = ref; }}
style={innerDivStyle(windowWidth, scale)}
>
{children}
</div>
}
</div>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment