diff --git a/README.md b/README.md index 3f07633d..1c38aa02 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,11 @@ Each one of these image props accepts normal `image` props, for example: | `style` | object | no | Additional styles to apply to the image | | ... | ... | no | ... | +## Controlled vs Uncontrolled Modes +Similar to how an `` works in React, if the consumer initially chooses to control the `isZoomed` value, then this means the consumer is now responsible for telling the component the value of `isZoomed`. If the consumer instantiates the component with a non-null `isZoomed` value and subsequently does _not_ pass a value for it on updates, then an error will be thrown notifying the consumer that this is a controlled component. + +The reverse is true, as well. If the component is instantiated without an `isZoomed` value, then the component will handle its own `isZoomed` state. If a non-null `isZoomed` prop is passed _after_ instantiation, then an error will be thrown notifying the consumer that this component controls its own state. + ## Browser Support Currently, this has only been tested on the latest modern browsers. Pull requests are welcome. diff --git a/example/build/app.js b/example/build/app.js index 22adc282..779e26f6 100644 --- a/example/build/app.js +++ b/example/build/app.js @@ -26,40 +26,13 @@ function _inherits(subClass, superClass) { if (typeof superClass !== "function" var App = function (_Component) { _inherits(App, _Component); - function App(props) { + function App() { _classCallCheck(this, App); - var _this = _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).call(this, props)); - - _this.state = { firstActive: false }; - return _this; + return _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).apply(this, arguments)); } _createClass(App, [{ - key: 'componentDidMount', - value: function componentDidMount() { - /** - * This is an example demonstrating how to manually - * control the `isZoomed` state of the first image - * using the `j` and `k` keys - */ - document.addEventListener('keyup', this.handleKeyup.bind(this)); - } - }, { - key: 'handleKeyup', - value: function handleKeyup(e) { - switch (e.keyCode) { - case 74: - return this.setState({ firstActive: true }); - - case 75: - return this.setState({ firstActive: false }); - - default: - return; - } - } - }, { key: 'render', value: function render() { return _react2.default.createElement( @@ -93,9 +66,7 @@ var App = function (_Component) { src: 'bridge-big.jpg', alt: 'Golden Gate Bridge', className: 'img--zoomed' - }, - isZoomed: this.state.firstActive, - shouldPreload: true + } }) ), _react2.default.createElement( @@ -278,6 +249,9 @@ var defaults = { } }; +var uncontrolledControlledError = 'A component is changing a react-medium-image-zoom component from an uncontrolled component to a controlled one. ImageZoom elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled image zoom element for the lifetime of the component.'; +var controlledUncontrolledError = 'A component is changing a react-medium-image-zoom component from a controlled component to an uncontrolled one. ImageZoom elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled image zoom element for the lifetime of the component.'; + var ImageZoom = function (_Component) { _inherits(ImageZoom, _Component); @@ -287,7 +261,7 @@ var ImageZoom = function (_Component) { var _this = _possibleConstructorReturn(this, (ImageZoom.__proto__ || Object.getPrototypeOf(ImageZoom)).call(this, props)); _this.state = { - isZoomed: props.isZoomed, + isZoomed: false, image: props.image, hasAlreadyLoaded: false }; @@ -300,7 +274,7 @@ var ImageZoom = function (_Component) { _createClass(ImageZoom, [{ key: 'componentDidMount', value: function componentDidMount() { - if (this.state.isZoomed) this.renderZoomed(); + if (this.state.isZoomed || this.props.isZoomed) this.renderZoomed(); } // Clean up any mess we made of the DOM before we unmount @@ -310,34 +284,36 @@ var ImageZoom = function (_Component) { value: function componentWillUnmount() { this.removeZoomed(); } - - /** - * We need to check to see if any changes are being - * mandated by the consumer and if so, update accordingly - */ - }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { - var imageChanged = this.props.image.src !== nextProps.image.src; - var isZoomedChanged = this.state.isZoomed !== nextProps.isZoomed; - var changes = _extends({}, imageChanged && { image: nextProps.image }, isZoomedChanged && { isZoomed: nextProps.isZoomed }); + if (this.props.isZoomed == null && nextProps.isZoomed != null) { + throw new Error(uncontrolledControlledError); + } else if (this.props.isZoomed != null && nextProps.isZoomed == null) { + throw new Error(controlledUncontrolledError); + } - if (Object.keys(changes).length) { - this.setState(changes); + // If the consumer wants to change the image's src, then so be it. + if (this.props.image.src !== nextProps.image.src) { + this.setState({ image: nextProps.image }); } } /** * When the component's state updates, check for changes - * and either zoom or start the unzoom procedure + * and either zoom or start the unzoom procedure. + * NOTE: We need to differentiate whether this is a + * controlled or uncontrolled component and do the check + * based off of that. */ }, { key: 'componentDidUpdate', - value: function componentDidUpdate(_, prevState) { - if (prevState.isZoomed !== this.state.isZoomed) { - if (this.state.isZoomed) this.renderZoomed();else if (this.portalInstance) this.portalInstance.handleUnzoom(); + value: function componentDidUpdate(prevProps, prevState) { + var prevIsZoomed = prevProps.isZoomed != null ? prevProps.isZoomed : prevState.isZoomed; + var isZoomed = this.props.isZoomed != null ? this.props.isZoomed : this.state.isZoomed; + if (prevIsZoomed !== isZoomed) { + if (isZoomed) this.renderZoomed();else if (this.portalInstance) this.portalInstance.handleUnzoom(); } } }, { @@ -360,26 +336,21 @@ var ImageZoom = function (_Component) { return image; } - - // Side-effects! - }, { key: 'renderZoomed', value: function renderZoomed() { this.portal = createPortal('div'); this.portalInstance = _reactDom2.default.render(_react2.default.createElement(Zoom, { - zoomImage: this.props.zoomImage, - image: _reactDom2.default.findDOMNode(this.refs.image), defaultStyles: this.props.defaultStyles, + image: _reactDom2.default.findDOMNode(this.refs.image), + isUncontrolledComponent: this.props.isZoomed == null, hasAlreadyLoaded: this.state.hasAlreadyLoaded, shouldRespectMaxDimension: this.props.shouldRespectMaxDimension, + zoomImage: this.props.zoomImage, zoomMargin: this.props.zoomMargin, onClick: this.handleUnzoom }), this.portal); } - - // Side-effects! - }, { key: 'removeZoomed', value: function removeZoomed() { @@ -400,8 +371,10 @@ var ImageZoom = function (_Component) { }, { key: 'handleZoom', value: function handleZoom(event) { - if (this.props.shouldHandleZoom(event)) { + if (this.props.isZoomed == null && this.props.shouldHandleZoom(event)) { this.setState({ isZoomed: true }, this.props.onZoom); + } else { + this.props.onZoom(); } } @@ -420,7 +393,9 @@ var ImageZoom = function (_Component) { var _this2 = this; return function () { - var changes = _extends({}, { isZoomed: false }, { hasAlreadyLoaded: true }, _this2.props.shouldReplaceImage && { image: _extends({}, _this2.state.image, { src: src }) }); + var changes = _extends({}, { hasAlreadyLoaded: true, isZoomed: false }, _this2.props.shouldReplaceImage && { + image: _extends({}, _this2.state.image, { src: src }) + }); /** * Lamentable but necessary right now in order to @@ -438,7 +413,6 @@ var ImageZoom = function (_Component) { key: 'defaultProps', get: function get() { return { - isZoomed: false, shouldReplaceImage: true, shouldRespectMaxDimension: false, zoomMargin: 40, @@ -587,7 +561,6 @@ var Zoom = function (_Component2) { }, { key: 'addListeners', value: function addListeners() { - this.isTicking = false; window.addEventListener('resize', this.handleResize); window.addEventListener('scroll', this.handleScroll, true); window.addEventListener('keyup', this.handleKeyUp); @@ -649,15 +622,18 @@ var Zoom = function (_Component2) { }, { key: 'handleUnzoom', value: function handleUnzoom(e) { - var _this5 = this; - if (e) { e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); } - this.setState({ isZoomed: false }, function () { - return setTimeout(_this5.props.onClick(_this5.state.src), transitionDuration); - }); + var onClick = this.props.onClick(this.state.src); + if (this.props.isUncontrolledComponent) { + this.setState({ isZoomed: false }, function () { + return setTimeout(onClick, transitionDuration); + }); + } else { + onClick(); + } } }, { key: 'getZoomImageStyle', @@ -767,12 +743,12 @@ var Overlay = function (_Component3) { function Overlay(props) { _classCallCheck(this, Overlay); - var _this6 = _possibleConstructorReturn(this, (Overlay.__proto__ || Object.getPrototypeOf(Overlay)).call(this, props)); + var _this5 = _possibleConstructorReturn(this, (Overlay.__proto__ || Object.getPrototypeOf(Overlay)).call(this, props)); - _this6.state = { + _this5.state = { isVisible: false }; - return _this6; + return _this5; } _createClass(Overlay, [{ diff --git a/example/src/app.js b/example/src/app.js index 087dbaa7..a0f8cb9c 100644 --- a/example/src/app.js +++ b/example/src/app.js @@ -3,34 +3,6 @@ import ReactDOM from 'react-dom' import ImageZoom from '../../lib/react-medium-image-zoom' class App extends Component { - constructor(props) { - super(props) - - this.state = { firstActive: false } - } - - componentDidMount() { - /** - * This is an example demonstrating how to manually - * control the `isZoomed` state of the first image - * using the `j` and `k` keys - */ - document.addEventListener('keyup', this.handleKeyup.bind(this)) - } - - handleKeyup(e) { - switch (e.keyCode) { - case 74: - return this.setState({ firstActive: true }) - - case 75: - return this.setState({ firstActive: false }) - - default: - return - } - } - render() { return (
@@ -49,8 +21,6 @@ class App extends Component { alt: 'Golden Gate Bridge', className: 'img--zoomed' }} - isZoomed={ this.state.firstActive } - shouldPreload={true} />

Thundercats freegan Truffaut, four loko twee Austin scenester lo-fi seitan High Life paleo quinoa cray. Schlitz butcher ethical Tumblr, pop-up DIY keytar ethnic iPhone PBR sriracha. Tonx direct trade bicycle rights gluten-free flexitarian asymmetrical. Whatever drinking vinegar PBR XOXO Bushwick gentrify. Cliche semiotics banjo retro squid Wes Anderson. Fashion axe dreamcatcher you probably haven't heard of them bicycle rights. Tote bag organic four loko ethical selfies gastropub, PBR fingerstache tattooed bicycle rights.

diff --git a/src/react-medium-image-zoom.js b/src/react-medium-image-zoom.js index b8fe295d..1cbded82 100644 --- a/src/react-medium-image-zoom.js +++ b/src/react-medium-image-zoom.js @@ -40,12 +40,15 @@ const defaults = { } } +const uncontrolledControlledError = 'A component is changing a react-medium-image-zoom component from an uncontrolled component to a controlled one. ImageZoom elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled image zoom element for the lifetime of the component.' +const controlledUncontrolledError = 'A component is changing a react-medium-image-zoom component from a controlled component to an uncontrolled one. ImageZoom elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled image zoom element for the lifetime of the component.' + export default class ImageZoom extends Component { constructor(props) { super(props) this.state = { - isZoomed: props.isZoomed, + isZoomed: false, image: props.image, hasAlreadyLoaded: false } @@ -56,7 +59,6 @@ export default class ImageZoom extends Component { static get defaultProps() { return { - isZoomed: false, shouldReplaceImage: true, shouldRespectMaxDimension: false, zoomMargin: 40, @@ -73,7 +75,7 @@ export default class ImageZoom extends Component { } componentDidMount() { - if (this.state.isZoomed) this.renderZoomed() + if (this.state.isZoomed || this.props.isZoomed) this.renderZoomed() } // Clean up any mess we made of the DOM before we unmount @@ -81,30 +83,31 @@ export default class ImageZoom extends Component { this.removeZoomed() } - /** - * We need to check to see if any changes are being - * mandated by the consumer and if so, update accordingly - */ componentWillReceiveProps(nextProps) { - const imageChanged = this.props.image.src !== nextProps.image.src - const isZoomedChanged = this.state.isZoomed !== nextProps.isZoomed - const changes = Object.assign({}, - imageChanged && { image: nextProps.image }, - isZoomedChanged && { isZoomed : nextProps.isZoomed } - ) + if (this.props.isZoomed == null && nextProps.isZoomed != null) { + throw new Error(uncontrolledControlledError) + } else if (this.props.isZoomed != null && nextProps.isZoomed == null) { + throw new Error(controlledUncontrolledError) + } - if (Object.keys(changes).length) { - this.setState(changes) + // If the consumer wants to change the image's src, then so be it. + if (this.props.image.src !== nextProps.image.src) { + this.setState({ image: nextProps.image }) } } /** * When the component's state updates, check for changes - * and either zoom or start the unzoom procedure + * and either zoom or start the unzoom procedure. + * NOTE: We need to differentiate whether this is a + * controlled or uncontrolled component and do the check + * based off of that. */ - componentDidUpdate(_, prevState) { - if (prevState.isZoomed !== this.state.isZoomed) { - if (this.state.isZoomed) this.renderZoomed() + componentDidUpdate(prevProps, prevState) { + const prevIsZoomed = prevProps.isZoomed != null ? prevProps.isZoomed : prevState.isZoomed + const isZoomed = this.props.isZoomed != null ? this.props.isZoomed : this.state.isZoomed + if (prevIsZoomed !== isZoomed) { + if (isZoomed) this.renderZoomed() else if (this.portalInstance) this.portalInstance.handleUnzoom() } } @@ -135,23 +138,22 @@ export default class ImageZoom extends Component { return image } - // Side-effects! renderZoomed() { this.portal = createPortal('div') this.portalInstance = ReactDOM.render( , this.portal) } - // Side-effects! removeZoomed() { if (this.portal) { ReactDOM.unmountComponentAtNode(this.portal) @@ -176,8 +178,10 @@ export default class ImageZoom extends Component { } handleZoom(event) { - if (this.props.shouldHandleZoom(event)) { + if (this.props.isZoomed == null && this.props.shouldHandleZoom(event)) { this.setState({ isZoomed: true }, this.props.onZoom) + } else { + this.props.onZoom() } } @@ -191,10 +195,10 @@ export default class ImageZoom extends Component { */ handleUnzoom(src) { return () => { - const changes = Object.assign({}, - { isZoomed: false }, - { hasAlreadyLoaded: true }, - this.props.shouldReplaceImage && { image: Object.assign({}, this.state.image, { src }) } + const changes = Object.assign({}, { hasAlreadyLoaded: true, isZoomed: false }, + this.props.shouldReplaceImage && { + image: Object.assign({}, this.state.image, { src }) + } ) /** @@ -326,7 +330,6 @@ class Zoom extends Component { } addListeners() { - this.isTicking = false window.addEventListener('resize', this.handleResize) window.addEventListener('scroll', this.handleScroll, true) window.addEventListener('keyup', this.handleKeyUp) @@ -382,7 +385,12 @@ class Zoom extends Component { e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); } - this.setState({ isZoomed: false }, () => setTimeout(this.props.onClick(this.state.src), transitionDuration)) + const onClick = this.props.onClick(this.state.src) + if (this.props.isUncontrolledComponent) { + this.setState({ isZoomed: false }, () => setTimeout(onClick, transitionDuration)) + } else { + onClick() + } } getZoomImageStyle() {