hc-components
Version:
基于react的通用组件库
880 lines (842 loc) • 26.5 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import Form from 'antd/lib/form';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
import Select from 'antd/lib/select';
import DatePicker from 'antd/lib/date-picker';
import TimePicker from 'antd/lib/time-picker';
import Radio from 'antd/lib/radio';
import Checkbox from 'antd/lib/checkbox';
import Switch from 'antd/lib/switch';
import Card from 'antd/lib/card';
import Upload from 'antd/lib/upload';
import Button from 'antd/lib/button';
import Tabs from 'antd/lib/tabs';
import Icon from 'antd/lib/icon';
import message from 'antd/lib/message';
import isPlainObject from 'lodash/isPlainObject';
import moment from 'moment';
import {localeContext} from '../../core/localeContext';
import './index.less';
class CustomFormBase extends React.PureComponent {
static contextTypes = {
router: PropTypes.object
}
static propTypes = {
className: PropTypes.string,
style: PropTypes.object,
dataSource: PropTypes.object,
parentDataSource: PropTypes.object,
options: PropTypes.array.isRequired,
formLayout: PropTypes.object,
rules: PropTypes.object,
onSubmit: PropTypes.func,
onChange: PropTypes.func,
layout: PropTypes.string,
buttons: PropTypes.object,
name: PropTypes.string,
title: PropTypes.string,
noLabel: PropTypes.bool,
noButton: PropTypes.bool,
noFlattern: PropTypes.bool,
normalize: PropTypes.bool,
loading: PropTypes.bool,
keep: PropTypes.bool,
form: PropTypes.object,
compact: PropTypes.bool,
disabled: PropTypes.bool
}
static defaultProps = {
className: '',
layout: 'horizontal',
dataSource: {},
compact: true,
rules: {},
normalize: true,
options: [],
formLayout: {
labelCol: {
span: 4
},
wrapperCol: {
span: 19
},
hasFeedback: true,
required: true
}
}
constructor(props) {
super(props);
this._dataSource = this._asMutable(props.dataSource);
this.optionsMap = {};
this.refsMap = {};
this._submitting = false;
this._validating = false;
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
componentWillReceiveProps(nextProps) {
if (nextProps.dataSource !== this.props.dataSource) {
this._dataSource = this._asMutable(nextProps.dataSource);
}
}
_asMutable(obj) {
if (Array.isArray(obj)) {
const arr = [];
obj.forEach(d => {
arr.push(this._asMutable(d));
});
return arr;
} else if (Object(obj) === obj) {
const newObj = {};
for (let key in obj) {
if (Object(obj[key]) === obj[key]) {
newObj[key] = this._asMutable(obj[key]);
} else {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
}
return newObj;
} else {
return obj;
}
}
get submitting() {
return this._submitting;
}
get validating() {
return this._validating;
}
validate(keep, attrName) {
this._validating = true;
return new Promise((resolve, reject) => {
this
.props
.form
.validateFields((err, body) => {
if (err) {
this._validating = false;
reject(err);
message.error(this.getLocale('illegal'));
} else {
body = this.parseBody(body);
const promises = [];
/* eslint-disable react/no-string-refs */
for (let name in this.refs) {
promises.push(this.refs[name].validate(keep, name));
}
Promise
.all(promises)
.then(bodys => {
this._validating = false;
if (bodys.length) {
body = bodys.reduce((p, c) => {
if (Array.isArray(c)) {
if (p[c[0]]) {
p[c[0]].push(c[1]);
} else {
p[c[0]] = [c[1]];
}
return p;
} else {
return Object.assign(p, c);
}
}, body);
}
if (keep && attrName) {
const arr = attrName.split('@');
// 数组形式
if (arr.length === 2) {
resolve([arr[0], body]);
} else {
resolve({
[attrName]: body
});
}
} else {
resolve(body);
}
}, errs => {
this._validating = false;
message.error(this.getLocale('illegal'));
reject(errs);
});
}
});
});
}
handleSubmit(onSubmit, keep) {
if (this.props.disabled) {
return Promise.resolve(true);
}
this._submitting = true;
return this
.validate(keep)
.then(body => {
let result;
try {
if (onSubmit) {
result = onSubmit(body, this.props.dataSource);
} else if (this.props.onSubmit) {
result = this
.props
.onSubmit(body, this.props.dataSource);
}
} catch (e) {
window.console.error(e);
}
if (result && result.then) {
result.then(() => {
this._submitting = false;
}, () => {
this._submitting = false;
});
} else {
this._submitting = false;
}
return result;
});
}
parseBody(body) {
if (this.props.normalize) {
for (let name in body) {
if (body[name] !== undefined) {
// 组合成数组
if (this.optionsMap[name].getValue) {
body[name] = this.optionsMap[name].getValue(body[name]);
}
if (this.optionsMap[name].originName) {
let v = body[name];
delete body[name];
name = this.optionsMap[name].originName;
body[name] = body[name] || [];
body[name].push(v);
}
}
}
}
if (this.props.name) {
return {
[this.props.name]: body
};
} else {
return body;
}
}
handleCancel() {
this
.context
.router
.goBack();
}
getFormLayout(option, extra) {
return Object.assign({}, this.props.formLayout, {
label: option.title || option.name || extra.title,
required: option.required || extra.required,
hasFeedback: option.hasFeedback || extra.hasFeedback,
help: option.help || extra.help,
className: option.className || extra.className,
extra: option.extra ? option.extra : (option.icon || null)
});
}
getInitialValue(option, defaultValue) {
const dataIndex = option.name || option.dataIndex;
let initialValue = this._dataSource[dataIndex];
if (this.props.normalize && initialValue !== undefined) {
switch (option.type) {
case 'boolean':
if (typeof initialValue !== 'boolean') {
initialValue = [false, true][parseInt(initialValue, 10)];
option.getValue = (value) => value + 0;
option.builtIn = true;
}
break;
case 'array':
if (!Array.isArray(initialValue)) {
initialValue = initialValue.split(',');
option.getValue = (value) => Array.isArray(value) ? value.join(',') : String(value);
option.builtIn = true;
}
break;
}
}
if (initialValue === undefined && !this.props.loading) {
initialValue = defaultValue;
}
if (!this.optionsMap[dataIndex]) {
this._dataSource[dataIndex] = initialValue;
this.optionsMap[dataIndex] = option;
}
return initialValue;
}
renderInput(option, key, onChange) {
const dataIndex = option.name || option.dataIndex;
if (!dataIndex) {
return null;
}
const noFlattern = option.noFlattern === undefined ? this.props.noFlattern : option.noFlattern;
const dataSource = noFlattern ? (option.dataSource || this._dataSource) : this._dataSource;
if (option.children) {
if (option.type === 'array') {
const isArr = option.children[0].children;
const dataSources = dataSource[dataIndex] || [];
const tabs = [];
const indexes = [];
dataSources.forEach((d, index) => {
indexes.push(index + '');
tabs.push({
index: index + '',
data: d
});
});
// 已经赋值过了
if (this.refsMap[dataIndex]) {
// 发现传入的dataSources有值,说明外部dataSource有变化,需要重新赋值,提取是之前的值不一样
if (dataSources.length && dataSources !== this.refsMap[dataIndex].dataSources) {
this.refsMap[dataIndex] = {
indexes: indexes,
activeKey: indexes[0],
tabs: tabs,
dataSources: dataSources
};
}
} else {
// 赋值
this.refsMap[dataIndex] = {
indexes: indexes,
activeKey: indexes[0],
tabs: tabs,
dataSources: dataSources
};
}
var obj = this.refsMap[dataIndex];
return (
<Card
className="j-com-customForm-card"
key={key}
title={option.title || dataIndex}>
<Tabs
onChange={(activeKey) => {
obj.activeKey = activeKey;
if (this.mounted) {
this.forceUpdate();
}
}}
onEdit={(targetKey, action) => {
let idx;
if (action === 'add') {
idx = obj.indexes.length ? 1 + parseInt(obj.indexes[obj.indexes.length - 1], 10) + '' : '0';
obj.indexes.push(idx);
obj.tabs.push({
index: idx,
data: {}
});
obj.activeKey = idx + '';
} else {
idx = obj.indexes.indexOf(targetKey);
obj.indexes.splice(idx, 1);
obj.tabs.splice(idx, 1);
// 选中项小于触发项
if (obj.activeKey >= targetKey) {
if (idx > 0) {
obj.activeKey = idx - 1;
} else if (obj.indexes.length) {
obj.activeKey = idx + 1;
} else {
obj.activeKey = '';
}
obj.activeKey = obj.activeKey + '';
}
}
if (this.mounted) {
this.forceUpdate();
}
}}
activeKey={obj.activeKey}
type="editable-card">{obj
.tabs
.map((item) => {
const key = dataIndex + '@' + obj.indexes[item.index];
return (
<Tabs.TabPane
tab={this.getLocale('dataIndex') + item.index}
key={item.index}
closable={true}
forceRender={true}>
{isArr ?
(this.optionsMap[key] = Object.assign({
name: key,
dataSource: dataSources[item.index],
originName: dataIndex,
title: this.getLocale('legend')
}, option.children[item.index])) && this.renderInput(this.optionsMap[key], key, (e) => {
if (onChange) {
const arr = obj
.tabs
.map(d => d.data);
const o = Object.assign({}, this.optionsMap[key]);
obj.tabs[item.index].data = arr[item.index] = CustomForm.getValueFromEvent(e, o);
o.name = o.originName;
onChange(arr, o);
}
}) :
(<CustomForm
formLayout={this.props.formLayout}
disabled={this.props.disabled}
ref={key}
onChange={(e, o) => {
if (onChange) {
const arr = obj
.tabs
.map(d => d.data);
arr[item.index][o.name] = CustomForm.getValueFromEvent(e, o);
onChange(arr, option);
}
}}
options={option.children}
noLabel={this.props.noLabel}
parentDataSource={this._dataSource}
dataSource={dataSources[item.index]}
noButton={true}
noFlattern={noFlattern} />)}
</Tabs.TabPane>
);
})}
</Tabs>
</Card>
);
} else if (noFlattern) {
const formElm = (<CustomForm
formLayout={this.props.formLayout}
disabled={this.props.disabled}
key={key}
ref={dataIndex}
title={option.title}
name={option.embed ? '' : dataIndex}
onChange={(e, o) => {
if (onChange) {
const obj = this.refsMap[dataIndex] ? this.refsMap[dataIndex] : (this.refsMap[dataIndex] = {});
obj[o.name] = CustomForm.getValueFromEvent(e, o);
onChange(obj, option);
}
}}
options={option.children}
noLabel={this.props.noLabel}
parentDataSource={this._dataSource}
dataSource={dataSource[dataIndex]}
noButton={true}
noFlattern={noFlattern} />);
return option.embed ? (
<Form.Item key={key} {...this.props.formLayout} label={option.title || dataIndex}><div style={{
border: '1px solid #ddd',
padding: 10
}}>{formElm}</div></Form.Item>
) : formElm;
} else {
const renderInput = this
.renderInput
.bind(this);
return (
<Card
className="j-com-customForm-card"
key={key}
title={option.title || dataIndex}>{option
.children
.map(renderInput)}</Card>
);
}
}
const propsByState = option.getProps ? option.getProps.call(this, this._dataSource, (nextState) => {
Object.assign(this._dataSource, nextState);
this.forceUpdate();
}, this.props.form) : {};
if (propsByState.name === false) {
return null;
}
const initialValue = this.getInitialValue(option, propsByState.default || option.default);
const formItemProps = this.getFormLayout(option, propsByState);
if (this.props.noLabel) {
formItemProps.label = null;
}
let rules = [].concat(this.props.rules[dataIndex] || []);
const defaultProps = {
disabled: option.disabled || this.props.disabled,
placeholder: option.placeholder
};
const decoratorProps = {
defaultValue: initialValue,
rules: rules.concat(option.rules || propsByState.rules || []),
getValueFromEvent: (e) => {
const v = CustomForm.getValueFromEvent(e, option);
this._dataSource[dataIndex] = v;
if (onChange) {
onChange(v, option);
}
if (option.onChange) {
option.onChange(this._dataSource, (nextState) => {
this
.props
.form
.setFieldsValue(nextState);
});
}
return v;
}
};
if (option.renderInput) {
return option.renderInput(option, (node, dpros) => {
return (
<Form.Item key={key} {...formItemProps}>{this
.props
.form
.getFieldDecorator(dataIndex, Object.assign(decoratorProps, dpros))(node)}</Form.Item>
);
}, Object.assign(defaultProps, propsByState), this.props.form);
} else {
const input = CustomForm.getFieldInput(option, defaultProps, propsByState, decoratorProps);
return (
<Form.Item key={key} {...formItemProps}>{this
.props
.form
.getFieldDecorator(dataIndex, decoratorProps)(input)}</Form.Item>
);
}
}
clear() {
this
.props
.form
.resetFields();
this.refsMap = {};
/* eslint-disable react/no-string-refs */
for (let name in this.refs) {
this.refs[name].clear();
}
}
render() {
const _buttons = CustomForm.getButtons(this.props.buttons, {
noButton: this.props.noButton,
save: this.getLocale('save'),
cancel: this.getLocale('cancel'),
onSubmit: () => this.handleSubmit(null, this.props.keep),
onCancel: () => this.handleCancel()
});
const buttons = _buttons ? (<Form.Item
wrapperCol={{
offset: this.props.formLayout.labelCol.span,
span: this.props.formLayout.wrapperCol.span
}}>{_buttons}</Form.Item>) : null;
const compact = this.props.compact ? 'j-com-customForm-compact ' : '';
const className = compact + this.props.className;
const formElm = (
<Form
className={'j-com-customForm ' + className}
style={this.props.style}
onSubmit={e => {
e.preventDefault();
this.handleSubmit(null, this.props.keep);
}}
layout={this.props.layout}>{this
.props
.options
.map((option, key) => this.renderInput(option, key, this.props.onChange))} {buttons}
</Form>
);
if (this.props.name) {
return (
<Card
className={'j-com-customForm-card ' + className}
title={this.props.title || this.props.name}>
{formElm}
</Card>
);
} else {
return formElm;
}
}
}
const CustomForm = Form.create({withRef: true})(CustomFormBase);
CustomForm.getLabel = function getLabel(item, option) {
if (option.getLabel) {
return option.getLabel(item);
} else {
return option.lableKey ? item[option.lableKey] : item.label || item.name;
}
};
CustomForm.getValueFromEvent = function getValueFromEvent(e, option) {
let v;
if (Object(e) === e) {
if ((isPlainObject(e) || isPlainObject(e.__proto__))) {
if (option.valuePropName) {
v = e[option.valuePropName];
} else {
v = e.target ? e.target.value : e;
}
} else {
v = e.target && e.target.nodeName ? e.target.value : e;
}
} else {
v = e;
}
return option.builtIn && option.getValue ? option.getValue(v) : v;
};
CustomForm.getButtons = function getButtons(buttons, props) {
if (!buttons && !props.noButton) {
buttons = [];
if (props.save) {
buttons.push(<Button
key={0}
onClick={props.onSubmit}
style={{
marginRight: 10
}}
type="primary">{props.save || 'save'}</Button>);
}
if (props.cancel) {
buttons.push((<Button key={1}
onClick={props.onCancel}
style={{
marginRight: 10
}}
type="default">{props.cancel || 'cancel'}</Button>));
}
}
return buttons;
};
/**
* option = {
* dataType | type,
* // 枚举值
* enum,
* options,
*
* // 特殊组件
* format,
* mode,
* // 上传
* dragger,
* action,
* valuePropName,
*
* // 指定组件
* Component,
*
* // 数字
* max,
* maxLength,
* maximum,
* // 置灰
* disabled,
* // 提示
* placeholder
* }
*/
CustomForm.getFieldInput = function getFieldInput(option, props, stateProps, decorator) {
let input;
let dataFormat;
if (option.enum) {
input = (
<Radio.Group {...props} {...stateProps}>
{option
.enum
.map(item => (
<Radio key={item} value={item}>{item}</Radio>
))}
</Radio.Group>
);
} else {
if (option.format) {
switch (option.format) {
case 'DATE_TIME':
dataFormat = option.dataFormat || 'YYYY-MM-DD HH:mm:ss';
decorator.initialValue = moment(decorator.defaultValue);
input = (<DatePicker
style={{
maxWidth: 250
}}
showTime
format={dataFormat}
{...props}
{...stateProps} />);
break;
case 'DATE':
dataFormat = option.dataFormat || 'YYYY-MM-DD';
decorator.initialValue = moment(decorator.defaultValue);
input = (<DatePicker
style={{
maxWidth: 250
}}
format={dataFormat}
{...props}
{...stateProps} />);
break;
case 'TIME':
dataFormat = option.dataFormat || 'HH:mm:ss';
decorator.initialValue = moment(decorator.defaultValue);
input = (<TimePicker
style={{
maxWidth: 250
}}
format={dataFormat}
{...props}
{...stateProps} />);
break;
case 'DATE_RANGE':
input = (<DatePicker.RangePicker {...props} {...stateProps} />);
break;
case 'CDN_PIC':
case 'FILE':
dataFormat = option.description || 'Click or drag files to the upload area';
decorator.valuePropName = option.valuePropName = 'fileList';
if (option.dragger) {
input = (
<Upload.Dragger
{...props}
{...stateProps}
listType={option.format === 'FILE' ? 'text' : 'picture'}
action={option.action}>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">{dataFormat}</p>
</Upload.Dragger>
);
} else {
input = (
<Upload
{...props}
{...stateProps}
action={option.action}
listType={option.format === 'FILE' ? 'text' : 'picture'}>
<Button>
<Icon type="upload" /> {option.description || 'Upload'}
</Button>
</Upload>
);
}
break;
case 'SELECT':
var options = stateProps.options || option.options;
if (option.mode === 'radio') {
input = (
<Radio.Group {...props} {...stateProps} >
{options.map(item => (
<Radio
key={item.value}
value={item.value}
disabled={item.disabled}>{CustomForm.getLabel(item, option)}</Radio>
))}
</Radio.Group>
);
} else if (option.mode === 'checkbox') {
input = (<Checkbox.Group {...props} {...stateProps} options={options} />);
} else {
input = (
<Select mode={option.mode} {...props} {...stateProps}>
{options.map(item => (
<Select.Option
key={item.value || item.id}
value={item.value || item.id}
disabled={item.disabled}>{CustomForm.getLabel(item, option)}</Select.Option>
))}
</Select>
);
}
break;
case 'Component':
input = (<option.Component {...props} {...stateProps} />);
break;
case 'Element':
case 'TEXT':
case 'Function':
case 'JSON':
input = (<Input.TextArea
autosize={{
minRows: 4,
maxRows: 10
}}
{...props}
onPressEnter={decorator.getValueFromEvent}
onBlur={decorator.getValueFromEvent}
{...stateProps} />);
break;
default:
input = (<Input {...props} {...stateProps} />);
}
} else {
const type = option.dataType || option.type;
switch (type) {
case 'number':
case 'integer':
var max = option.max;
if (!max) {
var maxLength = maxLength = option.maxLength ? option.maxLength : 10;
max = +new Array(maxLength).join(9);
if (option.maximum) {
max = Number(maxLength + '0.' + option.maximum);
}
}
input = (<InputNumber
style={{
maxWidth: 250
}}
min={option.min ? option.min : 0}
max={max}
{...props}
{...stateProps} />);
break;
case 'boolean':
decorator.valuePropName = option.valuePropName || 'checked';
var placeholder = [].concat(option.placeholder);
input = (<Switch
unCheckedChildren={placeholder[0]}
checkedChildren={placeholder[1]}
{...props}
{...stateProps} />);
break;
case 'string':
default:
input = (<Input {...props} {...stateProps} />);
break;
}
}
}
decorator.initialValue = decorator.initialValue || decorator.defaultValue;
return input;
};
CustomForm.prototype.getRealInstance = function () {
return this.refs.wrappedComponent;
};
['handleSubmit', 'clear', 'validate'].forEach(method => {
CustomForm.prototype[method] = function (...args) {
return this.refs.wrappedComponent[method](...args);
};
});
Object.defineProperties(CustomForm.prototype, {
submitting: {
get() {
return this.refs.wrappedComponent.submitting;
}
},
validating: {
get() {
return this.refs.wrappedComponent.validating;
}
}
});
export {CustomForm};