react-hold
Version:
Hold the empty presentational components in react.js
278 lines (243 loc) • 7.9 kB
JavaScript
import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
import PropTypes from 'prop-types'
import hoistNonReactStatic from 'hoist-non-react-statics'
import {
isNull, isObject, isFunction,
getNodeSize, getComputedStyle, getDisplayName,
addHandler, removeHandler,
} from './utils'
import Fill from './holders/Fill'
import createRefiter from './createRefiter'
const $nbsp = '\u00A0'
const blankLength = 10
const envDefaultStyle = {
position: 'relative',
padding: '0px',
margin: '0px',
width: '100%',
height: '100%',
border: 'none',
overflow: 'visible',
}
/**
* Create a higher-order component to manage
* original component and placeholder component.
*
* @param {Component|String} targetComponent
* @param {Function} condition
* @param {Component} defaultHolder
* @param {Object} holderDefaultProps
* @returns {Component}
*/
export default function (targetComponent, condition,
defaultHolder = Fill, holderDefaultProps = {}) {
if (!isFunction(targetComponent) && typeof targetComponent !== 'string') {
throw new TypeError('Expected the target component to be a string or class/function.')
}
if (!isFunction(condition)) {
throw new TypeError('Expected the hold condition to be a function.')
}
if (isObject(defaultHolder)) {
holderDefaultProps = defaultHolder
defaultHolder = Fill
}
const targetComponentName = getDisplayName(targetComponent)
const refiter = createRefiter(targetComponent)
class Hold extends Component {
constructor(...args) {
super(...args)
this.state = {
hold: true,
copy: true,
holderAutoSize: {
width: null,
height: null,
},
}
// The style of original node
this.originNodeStyle = null
// window resize handler
this.resizeHandler = () => {
if (this.state.hold) {
this.updateHolderSizeIfNecessary()
}
}
this.cancelHold = this.cancelHold.bind(this)
}
componentWillMount() {
if (condition.call(null, this.props, {})) {
refiter.refit()
} else {
this.cancelHold()
}
}
componentDidMount() {
if (this.state.hold) {
this.originNodeStyle = this.computeOriginNodeStyle()
this.setState({ copy: false })
}
addHandler(window, 'resize', this.resizeHandler)
}
componentWillReceiveProps(nextProps) {
if (condition.call(null, nextProps, this.props)) {
this.setState({
hold: true,
copy: true,
})
} else {
this.cancelHold()
}
}
componentDidUpdate() {
if (this.state.hold) {
if (this.state.copy) {
refiter.refit()
this.originNodeStyle = this.computeOriginNodeStyle()
this.setState({ copy: false })
} else if (!isNull(this.originNodeStyle)) {
this.setFakeNodeStyle(this.originNodeStyle)
this.updateHolderSizeIfNecessary()
this.originNodeStyle = null
}
}
}
componentWillUnmount() {
removeHandler(window, 'resize', this.resizeHandler)
}
setFakeNodeStyle(style) {
if (!isObject(style)) return
const { fake, env } = this.refs
const isInline = style.display && style.display.indexOf('inline') > -1
// hidden element
fake.style.display = 'none'
// set style
Object.keys(style).forEach((name) => {
if (name !== 'display') {
fake.style[name] = style[name]
}
})
// fix fake style
fake.style.opacity = 1
fake.style.background = 'transparent'
fake.style.borderColor = 'transparent'
// fix env style
if (isInline) {
env.style.overflow = 'hidden'
} else {
env.style.overflow = 'visible'
}
// display fake
fake.style.display = isInline ? 'inline-block' : style.display
}
computeOriginNodeStyle() {
let result = null
const originNode = findDOMNode(this)
// store original display property
let computedStyle = getComputedStyle(originNode, null)
const originDisplay = computedStyle.getPropertyValue('display')
// set display to 'none' before recompute is very **important**,
// don't remove or move this step!
originNode.style.display = 'none'
// compute node style
computedStyle = getComputedStyle(originNode, null)
// copy style
Object.keys(computedStyle).forEach((key) => {
if (/[0-9]+/.test(key)) {
const name = computedStyle[key]
result = result || {}
if (name === 'display') {
result[name] = originDisplay
} else {
result[name] = computedStyle.getPropertyValue(name)
}
}
})
return result
}
cancelHold() {
const { fake, env } = this.refs
// manual restore fake and env node style,
// because their style had been modified by method 'setFakeNodeStyle'
if (fake) fake.removeAttribute('style')
if (env) env.style.overflow = 'visible'
// restore component lifecycle methods
refiter.undo()
// clear origin node style
this.originNodeStyle = null
// exit hold state
this.setState({
hold: false,
copy: false,
})
}
updateHolderSizeIfNecessary() {
const { env } = this.refs
if (!env) return
const { holderAutoSize } = this.state
const holderProps = this.props.holderProps || this.props.props || {}
const customWidth = isNull(holderDefaultProps.width) ?
holderProps.width : holderDefaultProps.width
const customHeight = isNull(holderDefaultProps.height) ?
holderProps.height : holderDefaultProps.height
if (!isNull(customWidth) && !isNull(customHeight)) return
const size = getNodeSize(env)
const width = isNull(customWidth) ? size.width : null
const height = isNull(customHeight) ? size.height : null
if (holderAutoSize.width !== width || holderAutoSize.height !== height) {
this.setState({
holderAutoSize: {
width,
height,
},
})
}
}
render() {
const { hold, copy, holderAutoSize } = this.state
const { innerRef, holder, holderProps, props, ...propsForElement } = this.props
if (!hold || copy) {
if (innerRef && !hold) propsForElement.ref = innerRef
return React.createElement(targetComponent, propsForElement)
}
const propsForHolder = {
color: '#eee',
width: null,
height: null,
...holderDefaultProps,
...props,
...holderProps,
cancelHold: this.cancelHold,
targetProps: propsForElement,
}
isNull(propsForHolder.width) && (propsForHolder.width = holderAutoSize.width)
isNull(propsForHolder.height) && (propsForHolder.height = holderAutoSize.height)
if (typeof propsForHolder.children === 'string') {
propsForHolder.children = propsForHolder.children.replace(/ /g, $nbsp)
}
propsForHolder.children = propsForHolder.children || $nbsp.repeat(blankLength)
return (
<div ref="fake">
<div ref="env" style={envDefaultStyle}>
{ React.createElement(holder, propsForHolder) }
</div>
</div>
)
}
}
hoistNonReactStatic(Hold, targetComponent)
Hold.displayName = `Hold(${targetComponentName})`
Hold.propTypes = {
holder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
holderProps: PropTypes.object,
props: PropTypes.object, // The alias of 'holderProps'
innerRef: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
}
Hold.defaultProps = {
holder: defaultHolder,
holderProps: null,
props: null,
innerRef: null,
}
return Hold
}