import * as React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
/**
* Form component
* @class
*/
class Form extends React.Component {
_value = {};
_controls = {};
_buttons = {};
_errors = {};
_initialValueApplied = false;
static propTypes = {
onSubmit: PropTypes.func,
onSubmitFailed: PropTypes.func,
onChange: PropTypes.func,
preventSubmit: PropTypes.bool,
stopPropagation: PropTypes.bool,
className: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.array
])
}
static defaultProps = {
className: null,
preventSubmit: true,
stopPropagation: false,
initialValue: {}
}
static childContextTypes = {
onControlMount: PropTypes.func,
onControlUnmount: PropTypes.func,
onControlChange: PropTypes.func,
onErrorMount: PropTypes.func,
onErrorUnmount: PropTypes.func,
onButtonMount: PropTypes.func,
onButtonUnmount: PropTypes.func,
}
componentWillMount() {
let { initialValue, value } = this.props;
if (value) {
this._value = value;
} else if (initialValue && Object.keys(initialValue).length) {
this._initialValueApplied = true;
this._value = this.props.initialValue;
}
}
async componentWillReceiveProps(nextProps) {
let { initialValue, value } = nextProps;
if (value) {
this._value = value;
this.updateControls();
this.updateButtons();
} else if (!this._initialValueApplied && initialValue && Object.keys(initialValue).length) {
this._value = initialValue;
this.updateControls();
this.updateButtons();
}
}
updateControls() {
let promises = [];
Object.entries(this._controls)
.forEach(([id, c]) => {
promises.push(c.updateControl());
});
return Promise.all(promises);
}
getChildContext() {
return {
onControlMount: ::this.handleControlMount,
onControlUnmount: ::this.handleControlUnmount,
onControlChange: ::this.handleControlChange,
onErrorMount: ::this.handleErrorMount,
onErrorUnmount: ::this.handleErrorUnmount,
onButtonMount: ::this.handleButtonMount,
onButtonUnmount: ::this.handleButtonUnmount,
};
}
updateButtons() {
let isValid = this.isValid();
for (let [id, btn] of Object.entries(this._buttons)) {
btn.setValidated(isValid);
}
}
setErrors(errs) {
for (let [fName, errors] of Object.entries(errs)) {
let controls = Object.values(this._controls).filter(c => c.name == fName);
controls.forEach(c => c.setError(errors[0]));
}
}
handleControlMount(control, id, controlValue, controlInitialValue) {
let { value: formValue } = this.props;
this._controls[id] = control;
control.setForm(this);
let name = control.name;
if (controlValue === undefined) {
if (this._value.hasOwnProperty(name)) {
controlValue = this._value[name];
} else {
controlValue = controlInitialValue;
}
}
this._value[name] = controlValue;
this.updateButtons();
}
getControlValue(name) {
return this._value[name];
}
async handleControlChange(id, val, change=true, silent=false) {
if (this.props.value) {
change = false;
}
let control = this._controls[id];
let name = control.name;
let changes = {[name]: val};
if (change) {
this._value[name] = val;
}
if (this.props.onChange && !silent) {
this.props.onChange(changes, {...this.getValue(), ...changes});
}
await this.updateControls();
this.updateButtons();
}
handleControlUnmount(id) {
let name = this._controls[id].name;
delete this._controls[id];
if (Object.values(this._controls).filter(c => c.name == name).length == 0) {
delete this._value[name];
}
this.updateButtons();
}
handleErrorMount(id, component) {
this._errors[id] = component;
}
handleErrorUnmount(id) {
delete this._errors[id];
}
handleButtonMount(id, button) {
this._buttons[id] = button;
this.updateButtons();
}
handleButtonUnmount(id, component) {
delete this._buttons[id];
}
/**
* Method for checking if [Form]{@link Form} is valid
* @returns {bool}
*/
isValid() {
let valid = true;
for (let [id, control] of Object.entries(this._controls)) {
if (control.getErrors().length) {
valid = false;
break;
}
}
return valid;
}
/**
* Make all errors visible for {@link Form} controls
*/
showErrors() {
for (let [id, control] of Object.entries(this._controls)) {
control.showErrors();
}
}
handleSubmit(e) {
if (this.props.stopPropagation) {
e.stopPropagation();
}
e.stopPropagation();
if (this.props.preventSubmit) {
e.preventDefault();
}
let err = !this.isValid();
this.showErrors();
if (!err) {
if (this.props.onSubmit) {
this.props.onSubmit(this.getValue(), e);
}
} else {
if (this.props.onSubmitFailed) {
this.props.onSubmitFailed(this.getValue(), e);
}
}
}
/**
* Get {@link Form} value
* @returns {Object}
*/
getValue() {
let res = {};
for (let name of Object.values(this._controls).map(v => v.name)) {
res[name] = this._value[name];
}
return res;
}
/**
* Set Form value externally
* @param {Object} value New value for {@link Form}
*/
async setValue(value) {
this._value = value;
await this.updateControls()
this.updateButtons();
}
/**
* Resets [controlState]{@link Base#controlState} of {@link Form} controls and updates its values to initial
* @param {Object} value New value for {@link Form}
*/
reset(restoreInitial=true) {
for (let [id, control] of Object.entries(this._controls)) {
control.reset();
}
if (restoreInitial) {
this.setValue(this.props.initialValue);
}
}
render() {
let props = {
...this.props,
onSubmit: ::this.handleSubmit,
className: classnames(['q-form', this.props.className]),
};
delete props['initialValue'];
delete props['onChange'];
delete props['preventSubmit'];
return (
<form {...props}>
{this.props.children}
</form>
);
}
}
export default Form;