Merge pull request #3585 from blue-squirrel/feat-option-event-action

下拉选择类组件新增事件动作&demo
This commit is contained in:
hsm-lv 2022-02-16 09:35:26 +08:00 committed by GitHub
commit 97c624ae48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 446 additions and 85 deletions

View File

@ -91,6 +91,7 @@ test('Renderer:list:multiple clearable', async () => {
fireEvent.click(getByText(/Option A/));
await wait(100);
fireEvent.click(getByText(/Option B/));
await wait(100);
expect(container).toMatchSnapshot();
await wait(100);
fireEvent.click(getByText(/Option B/));

View File

@ -56,5 +56,6 @@ test('Renderer:radios', async () => {
);
expect(container).toMatchSnapshot();
fireEvent.click(getByText(/Option A/));
await wait(100);
expect(container).toMatchSnapshot();
});

View File

@ -1,3 +1,61 @@
const change = {
actions: [
{
actionType: 'dialog',
args: {
val: '${event.data.value}'
},
dialog: {
title: '派发change事件',
data: {
val: '${val}'
},
body: [
{
type: 'tpl',
tpl: '值更新:${val}'
}
]
}
}
]
};
const blur = {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发blur事件'
}
]
};
const focus = {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发focus事件'
}
]
};
const options = [
{
label: '选项A',
value: 'A'
},
{
label: '选项B',
value: 'B'
},
{
label: '选项C',
value: 'C'
}
];
export default {
type: 'page',
title: '下拉框组件事件',
@ -43,24 +101,234 @@ export default {
value: 'A,B,C',
multiple: true,
checkAll: true,
options: [
{
label: '选项A',
value: 'A'
},
{
label: '选项B',
value: 'B'
},
{
label: '选项C',
value: 'C'
}
]
options,
onEvent: {
change,
blur,
focus
}
}
]
}
]
}
]
},
{
type: 'tpl',
tpl: 'inputTag标签选择器',
inline: false,
wrapperComponent: 'h2'
},
{
type: 'form',
debug: true,
body: [
{
type: 'group',
body: [
{
name: 'trigger2',
id: 'trigger2',
type: 'action',
label: 'clear触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'clear-input-tag',
description: '点击清空指定下拉框选中值'
}
]
}
}
},
{
name: 'clear-input-tag',
id: 'clear-input-tag',
type: 'input-tag',
label: 'clear动作测试',
mode: 'row',
value: 'A,B',
multiple: true,
options,
onEvent: {
change,
blur,
focus
}
},
]
}
]
},
{
type: 'tpl',
tpl: 'matrix-checkboxes矩阵勾选',
inline: false,
wrapperComponent: 'h2'
},
{
type: 'form',
debug: true,
body: [
{
type: 'group',
body: [
{
name: 'trigger3',
id: 'trigger3',
type: 'action',
label: 'clear触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'clear-matrix-checkboxes',
description: '点击清空指定下拉框选中值'
}
]
}
}
},
{
name: 'clear-matrix-checkboxes',
id: 'clear-matrix-checkboxes',
type: 'matrix-checkboxes',
rowLabel: "行标题说明",
columns: [
{
label: '列1'
},
{
label: '列2'
}
],
rows: [
{
label: '行1'
},
{
label: '行2'
}
],
onEvent: {
change
}
},
]
}
]
},
{
type: 'tpl',
tpl: 'radios单选框',
inline: false,
wrapperComponent: 'h2'
},
{
type: 'form',
debug: true,
body: [
{
type: 'group',
body: [
{
name: 'trigger4',
id: 'trigger4',
type: 'action',
label: 'clear触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'clear-radios',
description: '点击清空指定下拉框选中值'
}
]
}
}
},
{
name: 'clear-radios',
id: 'clear-radios',
type: "radios",
options,
onEvent: {
change
}
},
]
}
]
},
{
type: 'tpl',
tpl: 'options类',
inline: false,
wrapperComponent: 'h2'
},
{
type: 'form',
debug: true,
body: [
{
type: 'group',
body: [
{
name: 'trigger5',
id: 'trigger5',
type: 'action',
label: 'clear触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'clear-options',
description: '点击清空指定下拉框选中值'
}
]
}
}
},
{
name: 'clear-options',
id: 'clear-options',
type: 'checkboxes',
options,
onEvent: {
change
}
},
{
name: 'clear-options',
id: 'clear-options',
type: 'button-group-select',
options,
onEvent: {
change
}
},
{
name: 'clear-options',
id: 'clear-options',
type: 'list-select',
options,
onEvent: {
change
}
},
]
}
]
},
],
};

View File

@ -556,7 +556,7 @@ export const examples = [
component: makeSchemaRenderer(UploadEventSchema)
},
{
label: '下拉',
label: '下拉选择类',
path: '/examples/event/select',
component: makeSchemaRenderer(SelectEventActionSchema)
},

View File

@ -9,11 +9,12 @@ import Downshift from 'downshift';
import find from 'lodash/find';
import {findDOMNode} from 'react-dom';
import ResultBox from '../../components/ResultBox';
import {autobind, filterTree} from '../../utils/helper';
import {autobind, filterTree, createObject} from '../../utils/helper';
import Spinner from '../../components/Spinner';
import Overlay from '../../components/Overlay';
import PopOver from '../../components/PopOver';
import ListMenu from '../../components/ListMenu';
import {Action} from '../../types';
/**
* Tag
@ -80,46 +81,86 @@ export default class TagControl extends React.PureComponent<
}
}
addItem(option: Option) {
doAction(action: Action, data: object, throwErrors: boolean) {
const {resetValue, onChange} = this.props;
if (action.actionType === 'clear') {
onChange(resetValue ?? '');
}
}
@autobind
async dispatchEvent(eventName: string, eventData: any = {}) {
const {dispatchEvent, options, data} = this.props;
const rendererEvent = await dispatchEvent(
eventName,
createObject(data, {
options,
...eventData
})
);
// 返回阻塞标识
return !!rendererEvent?.prevented;
}
@autobind
getValue(type: 'push' | 'pop' = 'pop', option: any = {}) {
const {
selectedOptions,
onChange,
joinValues,
extractValue,
delimiter,
valueField
} = this.props;
const newValue = selectedOptions.concat();
if (type === 'push') {
newValue.push(option);
} else {
newValue.pop();
}
const newValueRes = joinValues
? newValue
.map(item => item[valueField || 'value'])
.join(delimiter || ',')
: extractValue
? newValue.map(item => item[valueField || 'value'])
: newValue;
return newValueRes;
}
async addItem(option: Option) {
const {
selectedOptions,
onChange
} = this.props;
const newValue = selectedOptions.concat();
if (find(newValue, item => item.value == option.value)) {
return;
}
newValue.push(option);
const newValueRes = this.getValue('push', option);
onChange(
joinValues
? newValue
.map(item => item[valueField || 'value'])
.join(delimiter || ',')
: extractValue
? newValue.map(item => item[valueField || 'value'])
: newValue
);
const isPrevented = await this.dispatchEvent('change', {
value: newValueRes
});
isPrevented || onChange(newValueRes);
}
@autobind
handleFocus(e: any) {
async handleFocus(e: any) {
this.setState({
isFocused: true,
isOpened: true
});
this.props.onFocus?.(e);
const isPrevented = await this.dispatchEvent('focus', e);
isPrevented || this.props.onFocus?.(e);
}
@autobind
handleBlur(e: any) {
async handleBlur(e: any) {
const {
selectedOptions,
onChange,
@ -130,7 +171,9 @@ export default class TagControl extends React.PureComponent<
} = this.props;
const value = this.state.inputValue.trim();
this.props.onBlur?.(e);
const isPrevented = await this.dispatchEvent('blur', e);
isPrevented || this.props.onBlur?.(e);
this.setState(
{
isFocused: false,
@ -176,7 +219,7 @@ export default class TagControl extends React.PureComponent<
}
@autobind
handleChange(value: Array<Option>) {
async handleChange(value: Array<Option>) {
const {joinValues, extractValue, delimiter, valueField, onChange} =
this.props;
@ -190,7 +233,10 @@ export default class TagControl extends React.PureComponent<
newValue = newValue.join(delimiter || ',');
}
onChange(newValue);
const isPrevented = await this.dispatchEvent('change', {
value: newValue
});
isPrevented || onChange(newValue);
}
@autobind
@ -200,51 +246,35 @@ export default class TagControl extends React.PureComponent<
}
@autobind
handleKeyDown(evt: React.KeyboardEvent<HTMLInputElement>) {
async handleKeyDown(evt: React.KeyboardEvent<HTMLInputElement>) {
const {
selectedOptions,
onChange,
joinValues,
extractValue,
delimiter,
valueField
delimiter
} = this.props;
const value = this.state.inputValue.trim();
if (selectedOptions.length && !value && evt.key == 'Backspace') {
const newValue = selectedOptions.concat();
newValue.pop();
onChange(
joinValues
? newValue
.map(item => item[valueField || 'value'])
.join(delimiter || ',')
: extractValue
? newValue.map(item => item[valueField || 'value'])
: newValue
);
const newValueRes = this.getValue('pop');
const isPrevented = await this.dispatchEvent('change', {
value: newValueRes
});
isPrevented || onChange(newValueRes);
} else if (value && (evt.key === 'Enter' || evt.key === delimiter)) {
evt.preventDefault();
evt.stopPropagation();
const newValue = selectedOptions.concat();
if (!find(newValue, item => item.value == value)) {
newValue.push({
const newValueRes = this.getValue('push', {
label: value,
value: value
});
onChange(
joinValues
? newValue
.map(item => item[valueField || 'value'])
.join(delimiter || ',')
: extractValue
? newValue.map(item => item[valueField || 'value'])
: newValue
);
const isPrevented = await this.dispatchEvent('change', {
value: newValueRes
});
isPrevented || onChange(newValueRes);
}
this.setState({

View File

@ -7,8 +7,8 @@ import React from 'react';
import {FormBaseControl, FormControlProps, FormItem} from './Item';
import {buildApi, isValidApi, isEffectiveApi} from '../../utils/api';
import {Checkbox, Spinner} from '../../components';
import {autobind, setVariable} from '../../utils/helper';
import {ApiObject} from '../../types';
import {autobind, setVariable, createObject} from '../../utils/helper';
import {ApiObject, Action} from '../../types';
import {SchemaApi} from '../../Schema';
/**
@ -147,6 +147,13 @@ export default class MatrixCheckbox extends React.Component<
removeHook?.(this.initOptions, 'init');
}
doAction(action: Action, data: object, throwErrors: boolean) {
const {resetValue, onChange} = this.props;
if (action.actionType === 'clear') {
onChange(resetValue ?? '');
}
}
async initOptions(data: any) {
await this.reload();
const {formItem, name} = this.props;
@ -225,9 +232,9 @@ export default class MatrixCheckbox extends React.Component<
});
}
toggleItem(checked: boolean, x: number, y: number) {
async toggleItem(checked: boolean, x: number, y: number) {
const {columns, rows} = this.state;
const {multiple, singleSelectMode} = this.props;
const {multiple, singleSelectMode, dispatchEvent, data} = this.props;
const value = this.props.value || buildDefaultValue(columns, rows);
@ -262,6 +269,15 @@ export default class MatrixCheckbox extends React.Component<
}
}
const rendererEvent = await dispatchEvent('change',
createObject(data, {
value: value.concat(),
})
);
if (rendererEvent?.prevented) {
return;
}
this.props.onChange(value.concat());
}

View File

@ -2,7 +2,7 @@
* @file SelectRadiosCheckboxes
* ListButtonGroup
*/
import {Api, PlainObject, Schema} from '../../types';
import {Api, PlainObject, Schema, Action} from '../../types';
import {isEffectiveApi, isApiOutdated, isValidApi} from '../../utils/api';
import {isAlive} from 'mobx-state-tree';
import {
@ -454,6 +454,26 @@ export function registerOptionsControl(config: OptionsConfig) {
this.toDispose = [];
}
async dispatchChangeEvent(eventData: any = '') {
const {dispatchEvent, options, data} = this.props;
const rendererEvent = await dispatchEvent(
'change',
createObject(data, {
value: eventData,
options,
})
);
// 返回阻塞标识
return !!rendererEvent?.prevented;
}
doAction(action: Action, data: object, throwErrors: boolean) {
const {resetValue, onChange} = this.props;
if (action.actionType === 'clear') {
onChange(resetValue ?? '');
}
}
syncAutoFill(value: any) {
const {autoFill, multiple, onBulkChange, data} = this.props;
const formItem = this.props.formItem as IFormItemStore;
@ -561,7 +581,7 @@ export function registerOptionsControl(config: OptionsConfig) {
}
@autobind
handleToggle(
async handleToggle(
option: Option,
submitOnChange?: boolean,
changeImmediately?: boolean
@ -577,7 +597,8 @@ export function registerOptionsControl(config: OptionsConfig) {
value
);
onChange && onChange(newValue, submitOnChange, changeImmediately);
const isPrevented = await this.dispatchChangeEvent(newValue);
isPrevented || (onChange && onChange(newValue, submitOnChange, changeImmediately));
}
/**
@ -632,7 +653,7 @@ export function registerOptionsControl(config: OptionsConfig) {
}
@autobind
handleToggleAll() {
async handleToggleAll() {
const {value, onChange, formItem} = this.props;
if (!formItem) {
@ -644,7 +665,8 @@ export function registerOptionsControl(config: OptionsConfig) {
? []
: formItem.filteredOptions.concat();
const newValue = this.formatValueArray(valueArray);
onChange && onChange(newValue);
const isPrevented = await this.dispatchChangeEvent(newValue);
isPrevented || (onChange && onChange(newValue));
}
toggleValue(option: Option, originValue?: any) {

View File

@ -8,8 +8,9 @@ import {
Option,
FormOptionsControl
} from './Options';
import {autobind, isEmpty} from '../../utils/helper';
import {autobind, isEmpty, createObject} from '../../utils/helper';
import {dataMapping} from '../../utils/tpl-builtin';
import {Action} from '../../types';
/**
* Radio
@ -36,14 +37,31 @@ export default class RadiosControl extends React.Component<RadiosProps, any> {
columnsCount: 1
};
doAction(action: Action, data: object, throwErrors: boolean) {
const {resetValue, onChange} = this.props;
if (action.actionType === 'clear') {
onChange(resetValue ?? '');
}
}
@autobind
handleChange(option: Option) {
const {joinValues, extractValue, valueField, onChange} = this.props;
async handleChange(option: Option) {
const {joinValues, extractValue, valueField, onChange, dispatchEvent, options, data} = this.props;
if (option && (joinValues || extractValue)) {
option = option[valueField || 'value'];
}
const rendererEvent = await dispatchEvent('change',
createObject(data, {
value: option,
options,
})
);
if (rendererEvent?.prevented) {
return;
}
onChange && onChange(option);
}

View File

@ -151,20 +151,21 @@ export default class SelectControl extends React.Component<SelectProps, any> {
this.input && this.input.focus();
}
async dispatchEvent(eventName: SelectRendererEvent, e: any = {}) {
async dispatchEvent(eventName: SelectRendererEvent, eventData: any = {}) {
const event = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
const {dispatchEvent, options} = this.props;
const {dispatchEvent, options, data} = this.props;
// 触发渲染器事件
const rendererEvent = await dispatchEvent(
eventName,
createObject(e, {
options
createObject(data, {
options,
...eventData
})
);
if (rendererEvent?.prevented) {
return;
}
this.props[event](e);
this.props[event](eventData);
}
async changeValue(value: Option | Array<Option> | string | void) {
@ -178,6 +179,7 @@ export default class SelectControl extends React.Component<SelectProps, any> {
onChange,
setOptions,
options,
data,
dispatchEvent
} = this.props;
@ -222,10 +224,13 @@ export default class SelectControl extends React.Component<SelectProps, any> {
// 不设置没法回显
additonalOptions.length && setOptions(options.concat(additonalOptions));
const rendererEvent = await dispatchEvent('change', {
value: newValue,
options
});
const rendererEvent = await dispatchEvent(
'change',
createObject(data, {
value: newValue,
options,
})
);
if (rendererEvent?.prevented) {
return;
}