import * as React from 'react';
import * as PropTypes from 'prop-types';
import classnames from 'classnames';
import uuid from 'uuid';
let opositeProperty = function(obj, key, targetKey) {
Object.defineProperty(obj, key, {
get: () => !obj[targetKey],
set: val => obj[targetKey] = !val,
enumerable: true
});
}
/**
* Base form control component
* @class
*/
class Base extends React.Component {
/**
* @static
* @property {*} component - Component to render as a control
* @property {Function} isEmpty - Check if value is empty
* @property {Array} validators - List of validators
* @property {Array} showErrorOn - List of [controlState]{@link Base#controlState} keys. Display error only if all of values of specified keys are `true`
* @property {bool} multipleError - If `true` shows all errors
* @property {*} initialValue - Initial value for control
* @property {Object.<string, Function>} mapProps - Add new props to component
* @property {Function} getValue - Accepts event after control changed and returns value
* @property {String} controlClassName - `className` prop passes to control
*/
static propTypes = {
component: PropTypes.oneOfType([
PropTypes.func,
PropTypes.instanceOf(React.Component)
]),
isEmpty: PropTypes.func,
validators: PropTypes.array,
showErrorOn: PropTypes.array.isRequired,
multipleErrors: PropTypes.bool.isRequired,
initialValue: PropTypes.any,
mapProps: PropTypes.object,
getValue: PropTypes.func.isRequired,
controlClassName: PropTypes.string,
}
static defaultProps = {
isEmpty: v => v === undefined,
validators: [],
showErrorOn: ['touched', 'dirty'],
multipleErrors: false,
initialValue: undefined,
mapProps: {},
getValue: e => e,
renderErrors: true,
controlClassName: '',
}
/**
* @constructor
* @param {Object} props @see Allowed [propTypes]{@link Base#propTypes}
*/
constructor(props) {
super(props);
this.initialControlState = {
focus: false,
touched: false,
dirty: false,
invalid: false,
pending: false,
empty: true,
}
this.initControlState();
this._value = props.hasOwnProperty('value') ? props.value : props.initialValue;
}
initControlState() {
this.controlState = {...this.initialControlState};
opositeProperty(this.controlState, 'blur', 'focus');
opositeProperty(this.controlState, 'untouched', 'touched');
opositeProperty(this.controlState, 'pristine', 'dirty');
opositeProperty(this.controlState, 'valid', 'invalid');
opositeProperty(this.controlState, 'with-value', 'empty');
}
/**
* The current UI-state of control
* @private
* @property {bool} focus - Is the control in focus
* @property {bool} blur - Opposite to `focus`
* @property {bool} touched - The control has been visited
* @property {bool} untouched - Opposite to `touched`
* @property {bool} valid - The control's value is valid
* @property {bool} invalid - The control's value is invalid
* @property {bool} empty - The control's value is empty
* @property {bool} with-value - The control's value is not empty
*/
controlState = {}
/**
* Control errors
* @type {Array}
* @private
*/
_errors = [];
inited = false;
/**
* Storing external errors set by method [setError]{@link Base#setError}
* @type {Array}
* @private
*/
_externalErrors = [];
defaultMapProps = {}
static contextTypes = {
onControlMount: PropTypes.func.isRequired,
onControlUnmount: PropTypes.func.isRequired,
onControlChangeID: PropTypes.func,
onControlChange: PropTypes.func.isRequired,
}
componentDidMount() {
this._mounted = true;
if (this.props.getRef) {
this.props.getRef(this);
}
this._id = this.props.id ? this.props.id : uuid.v4();
let value = undefined;
if (this.props.hasOwnProperty('value')) {
value = this.props.value;
}
this.context.onControlMount(this, this.id, value, this.props.initialValue);
this.inited = true;
this.forceUpdate();
}
componentWillUnmount() {
this._mounted = false;
this.context.onControlUnmount(this.id, this);
}
componentDidUpdate(prevProps) {
if (prevProps.index != this.props.index) {
if (this.context.onControlChangeID) {
this.context.onControlChangeID(this.props.index, this);
}
}
}
get id() {
return this.props.hasOwnProperty('index') ? this.props.index : this._id;
}
/**
* Current value of control
*/
get value() {
if (this.props.hasOwnProperty('value')) {
return this.props.value;
}
if (!this.inited) {
return this.props.initialValue;
}
let val = this._form.getControlValue(this.name, this.id)
return val !== undefined ? val : this.props.initialValue;
}
/**
* Set control value
* @param val {*} Value to set
*/
setValue(val) {
this.context.onControlChange(this.id, val, true);
}
/**
* Get control value
* @return Current control value
*/
get name() {
return this.props.name;
}
setForm(f) {
this._form = f;
}
/**
* Resets [value]{@link Base#value} and [controlState]{@link Base#controlState} of control
* @type {Function}
*/
reset() {
this.initControlState();
this._showErrors = false;
this.setValue(this.props.initialValue);
}
getControlState() {
return this.controlState;
}
handleFocus(e) {
this.controlState.focus = true;
this.forceUpdate();
if (this.props.onFocus) {
this.props.onFocus(e);
}
}
handleBlur(e) {
this.controlState.focus = false;
this.controlState.touched = true;
this.forceUpdate();
if (this.props.onBlur) {
this.props.onBlur(e);
}
}
handleChange(e) {
let newValue = this.props.getValue(e);
this._externalErrors = [];
let changeFormValue = true;
if (this.props.hasOwnProperty('value')) {
changeFormValue = false;
}
this.context.onControlChange(this.id, newValue, changeFormValue);
if (this.props.onChange) {
this.props.onChange(e, newValue);
}
this.controlState.dirty = true;
this.forceUpdate();
}
showErrors() {
this._showErrors = true;
this.forceUpdate();
}
/**
* Set [externalErrors]{@link Base#_externalErrors} to controls
* @param {*} errs Accepts array, object or string
*/
setError(errs) {
if (!Array.isArray(errs)) {
errs = [errs];
}
this._externalErrors = errs;
this.forceUpdate();
}
getValidatorsErrors() {
if (!this._form) {
return [];
}
let data = this.getFormValue();
this._errors = this.props.validators.filter(v => !v.f(this.value, data));
return this._errors;
}
getExternalErrors() {
let { errorMessages } = this.props;
return this._externalErrors.map(e => {
let msg = e;
if (typeof e == 'string') {
msg = { msg: errorMessages[e] || e };
}
return msg;
});
}
/**
* Get all errors for current value
* @returns {Array}
*/
getErrors() {
return this.getValidatorsErrors().concat(this.getExternalErrors());
}
updateControl() {
return new Promise(resolve => {
this.forceUpdate(resolve);
});
}
getVisibleErrors() {
let errors = [...this.getErrors()];
if (!this._showErrors) {
if (this.props.showErrorOn.find(s => !this.controlState[s])) {
return [];
}
}
return errors;
}
getFormValue() {
if (this._form.getValue) {
return this._form.getValue();
} else {
return this._form.getFormValue();
}
}
render() {
let props = {...this.props};
let visibleErrors = this.getVisibleErrors();
let errorMessages = visibleErrors.filter(e => e.hasOwnProperty('msg'));
if (!props.multipleErrors) {
errorMessages = errorMessages.slice(0, 1);
}
let errElement = null;
errorMessages = errorMessages.map((e, i) => {
let msg = e.msg;
if (typeof msg == 'function') {
msg = msg(this.value, this.getFormValue());
}
return msg;
});
if (errorMessages.length && props.renderErrors) {
errElement = (
<div className='q-form-group__errors'>
{errorMessages.map((m, i) => (
<div className='q-form-group-error' key={i}>{m}</div>
))}
</div>
);
}
this.controlState.invalid = this.getErrors().length > 0;
this.controlState.empty = this.props.isEmpty(this.value);
let controlState = {
...this.controlState,
error: this.getVisibleErrors().length > 0,
};
let modifiers = [
...Object.entries(controlState).filter(v => v[1]).map(v => v[0]),
];
let baseCls = 'q-form-group';
let classes = classnames(['q-form-group', this.props.controlClassName, ...modifiers]);
let { mapProps } = props;
mapProps = {
...this.defaultMapProps,
...mapProps
};
props = {
...props,
errors: errorMessages,
value: this.value,
onChange: ::this.handleChange,
onBlur: ::this.handleBlur,
onFocus: ::this.handleFocus,
controlState
};
let updatedProps = {};
for (let [key, f] of Object.entries(mapProps)) {
updatedProps[key] = f(props);
}
delete props['validators'];
delete props['component'];
delete props['showErrorOn'];
delete props['errorMessages'];
delete props['isEmpty'];
delete props['multipleErrors'];
delete props['initialValue'];
delete props['getRef'];
delete props['mapProps'];
delete props['getValue'];
delete props['controlClassName'];
delete props['cid'];
delete props['controlState'];
delete props['renderErrors'];
props = { ...props, ...updatedProps };
let component = null;
if (this.props.component.prototype && this.props.component.prototype.isReactComponent) {
component = React.createElement(this.props.component, props, props.children);
} else {
component = this.props.component(props);
}
return (
<div className={classes}>
{component}
{errElement}
</div>
);
}
}
export default Base;