hc-components-test
Version:
基于react的通用组件库
838 lines (804 loc) • 26.2 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 '../../localeContext';
import './index.less';
class CustomFormBase extends React.PureComponent {
static contextTypes = {
router: PropTypes.object
}
static propTypes = {
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,
noButton: PropTypes.bool,
noFlattern: PropTypes.bool,
normalize: PropTypes.bool,
loading: PropTypes.bool,
className: PropTypes.string,
keep: PropTypes.bool,
form: PropTypes.object,
compact: PropTypes.bool,
disabled: PropTypes.bool
}
static defaultProps = {
layout: 'horizontal',
dataSource: {},
compact: true,
rules: {},
normalize: true,
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;
}
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 = [];
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)
});
}
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;
}
getInitialValue(option, defaultValue) {
let initialValue = this._dataSource[option.name];
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[option.name]) {
this._dataSource[option.name] = initialValue;
this.optionsMap[option.name] = option;
}
return initialValue;
}
renderInput(option, key, onChange) {
if (!option.name) {
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[option.name] || [];
const tabs = [];
const indexes = [];
dataSources.forEach((d, index) => {
indexes.push(index + '');
tabs.push({
index: index + '',
data: d
});
});
// 已经赋值过了
if (this.refsMap[option.name]) {
// 发现传入的dataSources有值,说明外部dataSource有变化,需要重新赋值,提取是之前的值不一样
if (dataSources.length && dataSources !== this.refsMap[option.name].dataSources) {
this.refsMap[option.name] = {
indexes: indexes,
activeKey: indexes[0],
tabs: tabs,
dataSources: dataSources
};
}
} else {
// 赋值
this.refsMap[option.name] = {
indexes: indexes,
activeKey: indexes[0],
tabs: tabs,
dataSources: dataSources
};
}
var obj = this.refsMap[option.name];
return (
<Card
className="j-com-customForm-card"
key={key}
title={option.title || option.name}>
<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 = option.name + '@' + 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: option.name,
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] = this.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] = this.getValueFromEvent(e, o);
onChange(arr, option);
}
}}
options={option.children}
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={option.name}
title={option.title}
name={option.embed ? '' : option.name}
onChange={(e, o) => {
if (onChange) {
const obj = this.refsMap[option.name] ? this.refsMap[option.name] : (this.refsMap[option.name] = {});
obj[o.name] = this.getValueFromEvent(e, o);
onChange(obj, option);
}
}}
options={option.children}
parentDataSource={this._dataSource}
dataSource={dataSource[option.name]}
noButton={true}
noFlattern={noFlattern} />);
return option.embed ? (
<Form.Item key={key} {...this.props.formLayout} label={option.title || option.name}><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 || option.name}>{option
.children
.map(renderInput)}</Card>
);
}
}
const propsByState = option.getProps ? option.getProps.call(this, this._dataSource, () => 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);
let rules = [].concat(this.props.rules[option.name] || []);
const defaultProps = {
disabled: option.disabled || this.props.disabled,
placeholder: option.placeholder
};
const decoratorProps = {
initialValue: initialValue,
rules: rules.concat(option.rules || propsByState.rules || []),
getValueFromEvent: (e) => {
const v = this.getValueFromEvent(e, option);
this._dataSource[option.name] = v;
if (onChange) {
onChange(v, option);
}
if (option.onChange) {
option.onChange(this._dataSource, (nextState) => {
this
.props
.form
.setFieldsValue(nextState);
});
}
return v;
}
};
let input;
if (option.render) {
return option.render(option, (node, dpros) => {
return (
<Form.Item key={key} {...formItemProps}>{this
.props
.form
.getFieldDecorator(option.name, Object.assign(decoratorProps, dpros))(node)}</Form.Item>
);
}, Object.assign(defaultProps, propsByState), this.props.form);
} else if (option.enum) {
input = (
<Radio.Group {...defaultProps} {...propsByState}>
{option
.enum
.map(item => (
<Radio key={item} value={item}>{item}</Radio>
))}
</Radio.Group>
);
} else {
if (option.format) {
switch (option.format) {
case 'DATE_TIME':
decoratorProps.initialValue = moment(decoratorProps.initialValue);
input = (<DatePicker
style={{
width: 250
}}
showTime
format="YYYY-MM-DD HH:mm:ss"
{...defaultProps}
{...propsByState} />);
break;
case 'DATE':
decoratorProps.initialValue = moment(decoratorProps.initialValue);
input = (<DatePicker
style={{
width: 250
}}
{...defaultProps}
{...propsByState} />);
break;
case 'TIME':
decoratorProps.initialValue = moment(decoratorProps.initialValue);
input = (<TimePicker
style={{
width: 250
}}
{...defaultProps}
{...propsByState} />);
break;
case 'DATE_RANGE':
input = (<DatePicker.RangePicker {...defaultProps} {...propsByState} />);
break;
case 'CDN_PIC':
decoratorProps.valuePropName = option.valuePropName = 'fileList';
if (option.dragger) {
input = (
<Upload.Dragger
{...defaultProps}
{...propsByState}
listType="picture"
action={option.action}>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">{this.getLocale('dragDesc')}</p>
</Upload.Dragger>
);
} else {
input = (
<Upload
{...defaultProps}
{...propsByState}
action={option.action}
listType="picture">
<Button>
<Icon type="upload" /> {this.getLocale('upload')}
</Button>
</Upload>
);
}
break;
case 'FILE':
decoratorProps.valuePropName = option.valuePropName = 'fileList';
if (option.dragger) {
input = (
<Upload.Dragger
{...defaultProps}
{...propsByState}
listType="text"
action={option.action}>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">{this.getLocale('dragDesc')}</p>
</Upload.Dragger>
);
} else {
input = (
<Upload
{...defaultProps}
{...propsByState}
action={option.action}
listType="text">
<Button>
<Icon type="upload" /> {this.getLocale('upload')}
</Button>
</Upload>
);
}
break;
case 'SELECT':
var options = propsByState.options || option.options;
if (option.mode === 'radio') {
input = (
<Radio.Group {...defaultProps} {...propsByState} >
{options.map(item => (
<Radio
key={item.value}
value={item.value}
disabled={item.disabled}>{this.getLabel(item, option)}</Radio>
))}
</Radio.Group>
);
} else if (option.mode === 'checkbox') {
input = (<Checkbox.Group {...defaultProps} {...propsByState} options={options} />);
} else {
input = (
<Select mode={option.mode} {...defaultProps} {...propsByState}>
{options.map(item => (
<Select.Option
key={item.value || item.id}
value={item.value || item.id}
disabled={item.disabled}>{this.getLabel(item, option)}</Select.Option>
))}
</Select>
);
}
break;
case 'Component':
input = (<option.Component {...defaultProps} {...propsByState} />);
break;
case 'Element':
case 'TEXT':
case 'Function':
case 'JSON':
input = (<Input.TextArea
autosize={{
minRows: 4,
maxRows: 10
}}
{...defaultProps}
onPressEnter={decoratorProps.getValueFromEvent}
onBlur={decoratorProps.getValueFromEvent}
{...propsByState} />);
break;
default:
input = (<Input {...defaultProps} {...propsByState} />);
}
} else {
switch (option.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={{
width: 250
}}
min={option.min ? option.min : 0}
max={max}
{...defaultProps}
{...propsByState} />);
break;
case 'boolean':
decoratorProps.valuePropName = option.valuePropName = 'checked';
var placeholder = [].concat(option.placeholder);
input = (<Switch
unCheckedChildren={placeholder[0]}
checkedChildren={placeholder[1]}
{...defaultProps}
{...propsByState} />);
break;
case 'string':
default:
input = (<Input {...defaultProps} {...propsByState} />);
break;
}
}
}
return (
<Form.Item key={key} {...formItemProps}>{this
.props
.form
.getFieldDecorator(option.name, decoratorProps)(input)}</Form.Item>
);
}
clear() {
this
.props
.form
.resetFields();
this.refsMap = {};
for (let name in this.refs) {
this.refs[name].clear();
}
}
getLabel(item, option) {
if (option.getLabel) {
return option.getLabel(item);
} else {
return option.lableKey ? item[option.lableKey] : item.label || item.name;
}
}
render() {
let buttons = this.props.buttons;
if (buttons) {
buttons = (
<Form.Item
wrapperCol={{
offset: this.props.formLayout.labelCol.span,
span: this.props.formLayout.wrapperCol.span
}}>{buttons}</Form.Item>
);
} else if (!this.props.noButton) {
buttons = (
<Form.Item
wrapperCol={{
offset: this.props.formLayout.labelCol.span,
span: this.props.formLayout.wrapperCol.span
}}>
<Button
onClick={() => this.handleSubmit(null, this.props.keep)}
style={{
marginRight: 10
}}
type="primary">{this.getLocale('save')}</Button>
<Button
onClick={() => this.handleCancel()}
style={{
marginRight: 10
}}
type="default">{this.getLocale('cancel')}</Button>
</Form.Item>
);
}
const compact = this.props.compact ? 'j-com-customForm-compact ' : '';
const className = compact + (this.props.className || '');
const formElm = (
<Form
className={'j-com-customForm ' + className}
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.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};