Skip to content

Commit

Permalink
Controlled vs uncontrolled
Browse files Browse the repository at this point in the history
  • Loading branch information
rpearce committed Aug 14, 2017
1 parent 691d252 commit 30ae8c1
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 129 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<input />` 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.

Expand Down
114 changes: 45 additions & 69 deletions example/build/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);

Expand All @@ -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
};
Expand All @@ -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
Expand All @@ -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();
}
}
}, {
Expand All @@ -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() {
Expand All @@ -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();
}
}

Expand All @@ -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
Expand All @@ -438,7 +413,6 @@ var ImageZoom = function (_Component) {
key: 'defaultProps',
get: function get() {
return {
isZoomed: false,
shouldReplaceImage: true,
shouldRespectMaxDimension: false,
zoomMargin: 40,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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, [{
Expand Down
30 changes: 0 additions & 30 deletions example/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="container">
Expand All @@ -49,8 +21,6 @@ class App extends Component {
alt: 'Golden Gate Bridge',
className: 'img--zoomed'
}}
isZoomed={ this.state.firstActive }
shouldPreload={true}
/>
</div>
<p>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.</p>
Expand Down
Loading

0 comments on commit 30ae8c1

Please sign in to comment.