semantic-ui-react
Version:
The official Semantic-UI-React integration.
253 lines (206 loc) • 10.6 kB
JavaScript
import _extends from 'babel-runtime/helpers/extends';
import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
import _createClass from 'babel-runtime/helpers/createClass';
import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
import _inherits from 'babel-runtime/helpers/inherits';
import _difference from 'lodash/difference';
import _isUndefined from 'lodash/isUndefined';
import _startsWith from 'lodash/startsWith';
import _filter from 'lodash/filter';
import _isEmpty from 'lodash/isEmpty';
import _keys from 'lodash/keys';
import _intersection from 'lodash/intersection';
import _has from 'lodash/has';
import _each from 'lodash/each';
import _invoke from 'lodash/invoke'; /* eslint-disable no-console */
/**
* Why choose inheritance over a HOC? Multiple advantages for this particular use case.
* In short, we need identical functionality to setState(), unless there is a prop defined
* for the state key. Also:
*
* 1. Single Renders
* Calling trySetState() in constructor(), componentWillMount(), or componentWillReceiveProps()
* does not cause two renders. Consumers and tests do not have to wait two renders to get state.
* See www.react.run/4kJFdKoxb/27 for an example of this issue.
*
* 2. Simple Testing
* Using a HOC means you must either test the undecorated component or test through the decorator.
* Testing the undecorated component means you must mock the decorator functionality.
* Testing through the HOC means you can not simply shallow render your component.
*
* 3. Statics
* HOC wrap instances, so statics are no longer accessible. They can be hoisted, but this is more
* looping over properties and storing references. We rely heavily on statics for testing and sub
* components.
*
* 4. Instance Methods
* Some instance methods may be exposed to users via refs. Again, these are lost with HOC unless
* hoisted and exposed by the HOC.
*/
import { Component } from 'react';
var getDefaultPropName = function getDefaultPropName(prop) {
return 'default' + (prop[0].toUpperCase() + prop.slice(1));
};
/**
* Return the auto controlled state value for a give prop. The initial value is chosen in this order:
* - regular props
* - then, default props
* - then, initial state
* - then, `checked` defaults to false
* - then, `value` defaults to '' or [] if props.multiple
* - else, undefined
*
* @param {string} propName A prop name
* @param {object} [props] A props object
* @param {object} [state] A state object
* @param {boolean} [includeDefaults=false] Whether or not to heed the default props or initial state
*/
export var getAutoControlledStateValue = function getAutoControlledStateValue(propName, props, state) {
var includeDefaults = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
// regular props
var propValue = props[propName];
if (propValue !== undefined) return propValue;
if (includeDefaults) {
// defaultProps
var defaultProp = props[getDefaultPropName(propName)];
if (defaultProp !== undefined) return defaultProp;
// initial state - state may be null or undefined
if (state) {
var initialState = state[propName];
if (initialState !== undefined) return initialState;
}
}
// React doesn't allow changing from uncontrolled to controlled components,
// default checked/value if they were not present.
if (propName === 'checked') return false;
if (propName === 'value') return props.multiple ? [] : '';
// otherwise, undefined
};
var AutoControlledComponent = function (_Component) {
_inherits(AutoControlledComponent, _Component);
function AutoControlledComponent() {
var _ref;
_classCallCheck(this, AutoControlledComponent);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var _this = _possibleConstructorReturn(this, (_ref = AutoControlledComponent.__proto__ || Object.getPrototypeOf(AutoControlledComponent)).call.apply(_ref, [this].concat(args)));
_initialiseProps.call(_this);
var autoControlledProps = _this.constructor.autoControlledProps;
var state = _invoke(_this, 'getInitialAutoControlledState', _this.props) || {};
if (process.env.NODE_ENV !== 'production') {
var _this$constructor = _this.constructor,
defaultProps = _this$constructor.defaultProps,
name = _this$constructor.name,
propTypes = _this$constructor.propTypes;
// require static autoControlledProps
if (!autoControlledProps) {
console.error('Auto controlled ' + name + ' must specify a static autoControlledProps array.');
}
// require propTypes
_each(autoControlledProps, function (prop) {
var defaultProp = getDefaultPropName(prop);
// regular prop
if (!_has(propTypes, defaultProp)) {
console.error(name + ' is missing "' + defaultProp + '" propTypes validation for auto controlled prop "' + prop + '".');
}
// its default prop
if (!_has(propTypes, prop)) {
console.error(name + ' is missing propTypes validation for auto controlled prop "' + prop + '".');
}
});
// prevent autoControlledProps in defaultProps
//
// When setting state, auto controlled props values always win (so the parent can manage them).
// It is not reasonable to decipher the difference between props from the parent and defaultProps.
// Allowing defaultProps results in trySetState always deferring to the defaultProp value.
// Auto controlled props also listed in defaultProps can never be updated.
//
// To set defaults for an AutoControlled prop, you can set the initial state in the
// constructor or by using an ES7 property initializer:
// https://babeljs.io/blog/2015/06/07/react-on-es6-plus#property-initializers
var illegalDefaults = _intersection(autoControlledProps, _keys(defaultProps));
if (!_isEmpty(illegalDefaults)) {
console.error(['Do not set defaultProps for autoControlledProps. You can set defaults by', 'setting state in the constructor or using an ES7 property initializer', '(https://babeljs.io/blog/2015/06/07/react-on-es6-plus#property-initializers)', 'See ' + name + ' props: "' + illegalDefaults + '".'].join(' '));
}
// prevent listing defaultProps in autoControlledProps
//
// Default props are automatically handled.
// Listing defaults in autoControlledProps would result in allowing defaultDefaultValue props.
var illegalAutoControlled = _filter(autoControlledProps, function (prop) {
return _startsWith(prop, 'default');
});
if (!_isEmpty(illegalAutoControlled)) {
console.error(['Do not add default props to autoControlledProps.', 'Default props are automatically handled.', 'See ' + name + ' autoControlledProps: "' + illegalAutoControlled + '".'].join(' '));
}
}
// Auto controlled props are copied to state.
// Set initial state by copying auto controlled props to state.
// Also look for the default prop for any auto controlled props (foo => defaultFoo)
// so we can set initial values from defaults.
var initialAutoControlledState = autoControlledProps.reduce(function (acc, prop) {
acc[prop] = getAutoControlledStateValue(prop, _this.props, state, true);
if (process.env.NODE_ENV !== 'production') {
var defaultPropName = getDefaultPropName(prop);
var _name = _this.constructor.name;
// prevent defaultFoo={} along side foo={}
if (!_isUndefined(_this.props[defaultPropName]) && !_isUndefined(_this.props[prop])) {
console.error(_name + ' prop "' + prop + '" is auto controlled. Specify either ' + defaultPropName + ' or ' + prop + ', but not both.');
}
}
return acc;
}, {});
_this.state = _extends({}, state, initialAutoControlledState);
return _this;
}
_createClass(AutoControlledComponent, [{
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
var _this2 = this;
var autoControlledProps = this.constructor.autoControlledProps;
// Solve the next state for autoControlledProps
var newState = autoControlledProps.reduce(function (acc, prop) {
var isNextUndefined = _isUndefined(nextProps[prop]);
var propWasRemoved = !_isUndefined(_this2.props[prop]) && isNextUndefined;
// if next is defined then use its value
if (!isNextUndefined) acc[prop] = nextProps[prop];
// reinitialize state for props just removed / set undefined
else if (propWasRemoved) acc[prop] = getAutoControlledStateValue(prop, nextProps);
return acc;
}, {});
if (Object.keys(newState).length > 0) this.setState(newState);
}
/**
* Safely attempt to set state for props that might be controlled by the user.
* Second argument is a state object that is always passed to setState.
* @param {object} maybeState State that corresponds to controlled props.
* @param {object} [state] Actual state, useful when you also need to setState.
*/
}]);
return AutoControlledComponent;
}(Component);
var _initialiseProps = function _initialiseProps() {
var _this3 = this;
this.trySetState = function (maybeState, state) {
var autoControlledProps = _this3.constructor.autoControlledProps;
if (process.env.NODE_ENV !== 'production') {
var name = _this3.constructor.name;
// warn about failed attempts to setState for keys not listed in autoControlledProps
var illegalKeys = _difference(_keys(maybeState), autoControlledProps);
if (!_isEmpty(illegalKeys)) {
console.error([name + ' called trySetState() with controlled props: "' + illegalKeys + '".', 'State will not be set.', 'Only props in static autoControlledProps will be set on state.'].join(' '));
}
}
var newState = Object.keys(maybeState).reduce(function (acc, prop) {
// ignore props defined by the parent
if (_this3.props[prop] !== undefined) return acc;
// ignore props not listed in auto controlled props
if (autoControlledProps.indexOf(prop) === -1) return acc;
acc[prop] = maybeState[prop];
return acc;
}, {});
if (state) newState = _extends({}, newState, state);
if (Object.keys(newState).length > 0) _this3.setState(newState);
};
};
export default AutoControlledComponent;