feat: input-text支持事件动作 & demo doc

This commit is contained in:
lurunze1226 2022-01-24 16:13:22 +08:00
parent 8cc8283ded
commit 5a5cc4f2cb
11 changed files with 363 additions and 112 deletions

View File

@ -449,7 +449,7 @@ exports[`Renderer:text type is password 1`] = `
placeholder=""
size="10"
type="password"
value="abcd"
value=""
/>
</div>
</div>
@ -1243,12 +1243,12 @@ exports[`Renderer:text with counter 2`] = `
placeholder=""
size="10"
type="text"
value="abcd"
value=""
/>
<span
class="cxd-TextControl-counter"
>
4
0
</span>
</div>
</div>
@ -1507,12 +1507,12 @@ exports[`Renderer:text with counter and maxLength 2`] = `
placeholder=""
size="10"
type="text"
value="abcd"
value=""
/>
<span
class="cxd-TextControl-counter"
>
4/10
0/10
</span>
</div>
</div>
@ -1776,10 +1776,11 @@ exports[`Renderer:text with options and multiple: first option selected 1`] = `
class="cxd-Form-control cxd-TextControl is-focused"
>
<div
aria-expanded="false"
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="downshift-1-label"
class="cxd-TextControl-input cxd-TextControl-input--withAC cxd-TextControl-input--multiple"
aria-owns="downshift-1-menu"
class="cxd-TextControl-input cxd-TextControl-input--withAC is-opened cxd-TextControl-input--multiple"
role="combobox"
>
<div
@ -1798,6 +1799,7 @@ exports[`Renderer:text with options and multiple: first option selected 1`] = `
</div>
<input
aria-autocomplete="list"
aria-controls="downshift-1-menu"
aria-labelledby="downshift-1-label"
autocomplete="off"
id="downshift-1-input"
@ -1806,6 +1808,40 @@ exports[`Renderer:text with options and multiple: first option selected 1`] = `
type="text"
value=""
/>
<div
class="cxd-TextControl-sugs"
>
<div
aria-selected="false"
class="cxd-TextControl-sugItem"
id="downshift-1-item-0"
role="option"
>
<span>
OptionB
</span>
</div>
<div
aria-selected="false"
class="cxd-TextControl-sugItem"
id="downshift-1-item-1"
role="option"
>
<span>
OptionC
</span>
</div>
<div
aria-selected="false"
class="cxd-TextControl-sugItem"
id="downshift-1-item-2"
role="option"
>
<span>
OptionD
</span>
</div>
</div>
</div>
</div>
</div>
@ -2304,10 +2340,11 @@ exports[`Renderer:text with options and multiple: second option selected 1`] = `
class="cxd-Form-control cxd-TextControl is-focused"
>
<div
aria-expanded="false"
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="downshift-1-label"
class="cxd-TextControl-input cxd-TextControl-input--withAC cxd-TextControl-input--multiple"
aria-owns="downshift-1-menu"
class="cxd-TextControl-input cxd-TextControl-input--withAC is-opened cxd-TextControl-input--multiple"
role="combobox"
>
<div
@ -2340,6 +2377,7 @@ exports[`Renderer:text with options and multiple: second option selected 1`] = `
</div>
<input
aria-autocomplete="list"
aria-controls="downshift-1-menu"
aria-labelledby="downshift-1-label"
autocomplete="off"
id="downshift-1-input"
@ -2348,6 +2386,30 @@ exports[`Renderer:text with options and multiple: second option selected 1`] = `
type="text"
value=""
/>
<div
class="cxd-TextControl-sugs"
>
<div
aria-selected="false"
class="cxd-TextControl-sugItem"
id="downshift-1-item-0"
role="option"
>
<span>
OptionC
</span>
</div>
<div
aria-selected="false"
class="cxd-TextControl-sugItem"
id="downshift-1-item-1"
role="option"
>
<span>
OptionD
</span>
</div>
</div>
</div>
</div>
</div>
@ -2636,14 +2698,16 @@ exports[`Renderer:text with options: select first option 1`] = `
class="cxd-Form-control cxd-TextControl is-focused"
>
<div
aria-expanded="false"
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="downshift-0-label"
class="cxd-TextControl-input cxd-TextControl-input--withAC"
aria-owns="downshift-0-menu"
class="cxd-TextControl-input cxd-TextControl-input--withAC is-opened"
role="combobox"
>
<input
aria-autocomplete="list"
aria-controls="downshift-0-menu"
aria-labelledby="downshift-0-label"
autocomplete="off"
id="downshift-0-input"

View File

@ -55,6 +55,7 @@ test('Control:onChange', async () => {
value: '123'
}
});
await wait(100);
expect(onChange).toBeCalledTimes(1);
fireEvent.click(getByText('Submit'));

View File

@ -36,7 +36,9 @@ const setup = (inputOptions: any = {}, formOptions: any = {}) => {
'input[name="text"]'
) as HTMLInputElement;
const submitBtn = utils.container.querySelector('button[type="submit"]');
const submitBtn = utils.container.querySelector(
'button[type="submit"]'
) as HTMLElement;
return {
input,
@ -48,13 +50,14 @@ const setup = (inputOptions: any = {}, formOptions: any = {}) => {
/**
* 使
*/
test('Renderer:text', () => {
test('Renderer:text', async () => {
const {container, input} = setup();
expect(container).toMatchSnapshot();
// 输入是否正常
fireEvent.change(input, {target: {value: 'AbCd'}});
// 事件机制导致hanleChange变为异步
await wait(100);
expect(input.value).toBe('AbCd');
});
@ -128,10 +131,12 @@ test('Renderer:text with clearable', async () => {
clearable: true
});
fireEvent.change(input, {target: {value: 'abcd'}}); // 有值之后才会显示clear的icon
await wait(100);
expect(container).toMatchSnapshot();
fireEvent.click(container.querySelector('a.cxd-TextControl-clear'));
fireEvent.click(
container.querySelector('a.cxd-TextControl-clear') as HTMLElement
);
await wait(100);
expect(input.value).toBe('');
});
@ -158,13 +163,19 @@ test('Renderer:text with options', async () => {
expect(container).toMatchSnapshot();
// 展开 options
fireEvent.click(container.querySelector('.cxd-TextControl-input'));
fireEvent.click(
container.querySelector('.cxd-TextControl-input') as HTMLElement
);
await wait(100);
expect(container).toMatchSnapshot('options is open');
// 选中一项
fireEvent.click(
container.querySelector('.cxd-TextControl-sugs .cxd-TextControl-sugItem')
container.querySelector(
'.cxd-TextControl-sugs .cxd-TextControl-sugItem'
) as HTMLElement
);
await wait(100);
// expect(input.value).toBe('a');
expect(container).toMatchSnapshot('select first option');
});
@ -198,29 +209,39 @@ test('Renderer:text with options and multiple', async () => {
{debug: true}
);
const textControl = container.querySelector('.cxd-TextControl-input');
const textControl = container.querySelector(
'.cxd-TextControl-input'
) as HTMLElement;
// 展开 options
fireEvent.click(textControl);
await wait(100);
expect(container).toMatchSnapshot('options is opened');
// 选中第一项
fireEvent.click(
container.querySelector('.cxd-TextControl-sugs .cxd-TextControl-sugItem')
container.querySelector(
'.cxd-TextControl-sugs .cxd-TextControl-sugItem'
) as HTMLElement
);
await wait(100);
// expect(input.value).toBe('a');
expect(container).toMatchSnapshot('first option selected');
// 再次打开 options
fireEvent.click(textControl);
await wait(100);
expect(container).toMatchSnapshot(
'options is opened again, and first option already selected'
);
// 选中 options 中的第一项
fireEvent.click(
container.querySelector('.cxd-TextControl-sugs .cxd-TextControl-sugItem')
container.querySelector(
'.cxd-TextControl-sugs .cxd-TextControl-sugItem'
) as HTMLElement
);
await wait(100);
// expect(input.value).toBe('a,b');
expect(container).toMatchSnapshot('second option selected');
});
@ -267,19 +288,21 @@ test('Renderer:text with counter and maxLength', () => {
/**
*
*/
test('Renderer:text with transform lowerCase', () => {
test('Renderer:text with transform lowerCase', async () => {
const {input} = setup({transform: {lowerCase: true}});
fireEvent.change(input, {target: {value: 'AbCd'}});
await wait(100);
expect(input.value).toBe('abcd');
});
/**
*
*/
test('Renderer:text with transform upperCase', () => {
test('Renderer:text with transform upperCase', async () => {
const {input} = setup({transform: {upperCase: true}});
fireEvent.change(input, {target: {value: 'AbCd'}});
await wait(100);
expect(input.value).toBe('ABCD');
});

View File

@ -0,0 +1,81 @@
export default {
type: 'page',
title: '输入类组件事件',
regions: ['body', 'toolbar', 'header'],
body: [
{
type: 'tpl',
tpl: 'InputText输入框',
inline: false,
wrapperComponent: 'h2'
},
{
type: 'form',
debug: true,
api: '/api/mock2/form/saveForm',
body: [
{
type: 'group',
body: [
{
name: 'trigger1',
id: 'trigger1',
type: 'action',
label: 'clear触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'clear-receiver',
description: '点击清空指定输入框的内容'
}
]
}
}
},
{
name: 'clear-receiver',
id: 'clear-receiver',
type: 'input-text',
label: 'clear动作测试',
mode: 'row',
value: 'chunk of text ready to be cleared.'
}
]
},
{
type: 'group',
body: [
{
name: 'trigger2',
id: 'trigger2',
type: 'action',
label: 'focus触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'focus',
componentId: 'focus-receiver',
description: '点击使指定输入框聚焦'
}
]
}
}
},
{
name: 'focus-receiver',
id: 'focus-receiver',
type: 'input-text',
label: 'focus动作测试',
mode: 'row'
}
]
}
]
}
]
};

View File

@ -73,6 +73,7 @@ import CustomEventActionSchema from './EventAction/Custom';
import LogicEventActionSchema from './EventAction/Logic';
import StopEventActionSchema from './EventAction/Stop';
import DataFlowEventActionSchema from './EventAction/DataFlow';
import InputEventSchema from './EventAction/InputEvent';
import WizardSchema from './Wizard';
import ChartSchema from './Chart';
import EChartsEditorSchema from './ECharts';
@ -508,22 +509,29 @@ export const examples = [
{
label: '事件动作机制',
icon: 'fa fa-bolt',
icon: 'fa fa-bullhorn',
children: [
{
label: '执行通用动作',
label: '执行通用动作',
path: '/examples/event-action/common',
component: makeSchemaRenderer(CommonEventActionSchema)
},
{
label: '广播(自定义事件)',
label: '广播(自定义事件)',
path: '/examples/event-action/broadcat',
component: makeSchemaRenderer(BroadcastEventActionSchema)
},
{
label: '执行其他组件动作',
label: '执行其他组件动作',
path: '/examples/event-action/cmpt',
component: makeSchemaRenderer(CmptEventActionSchema)
component: makeSchemaRenderer(CmptEventActionSchema),
children: [
{
label: '输入类组件',
path: '/examples/event/input',
component: makeSchemaRenderer(InputEventSchema)
}
]
},
{
label: '自定义JS',
@ -531,7 +539,7 @@ export const examples = [
component: makeSchemaRenderer(CustomEventActionSchema)
},
{
label: '执行逻辑编排动作',
label: '执行逻辑编排动作',
path: '/examples/event-action/logic',
component: makeSchemaRenderer(LogicEventActionSchema)
},

View File

@ -18,9 +18,10 @@ import {Schema, SchemaNode} from './types';
import {DebugWrapper, enableAMISDebug} from './utils/debug';
import getExprProperties from './utils/filter-schema';
import {anyChanged, chainEvents, autobind} from './utils/helper';
import {RendererEvent} from './utils/renderer-event';
import {SimpleMap} from './utils/SimpleMap';
import type {RendererEvent} from './utils/renderer-event';
interface SchemaRendererProps extends Partial<RendererProps> {
schema: Schema;
$path: string;

View File

@ -21,7 +21,10 @@ export class CmptAction implements Action {
renderer: ListenerContext,
event: RendererEvent<any>
) {
// 根据唯一ID查找指定组件
/**
* ID查找指定组件
* id或未指定响应组件componentId使
*/
const component =
action.componentId && renderer.props.$schema.id !== action.componentId
? event.context.scoped?.getComponentById(action.componentId)

View File

@ -21,6 +21,11 @@ import {ActionSchema} from '../Action';
import {SchemaApi} from '../../Schema';
import {generateIcon} from '../../utils/icon';
import type {Option} from '../../components/Select';
import type {RendererEvent} from '../../utils/renderer-event';
import type {IScopedContext} from '../../Scoped';
import type {ListenerAction} from '../../actions/Action';
// declare function matchSorter(items:Array<any>, input:any, options:any): Array<any>;
/**
@ -97,6 +102,71 @@ export interface TextState {
isFocused?: boolean;
}
export type InputTextRendererEvent =
| 'blur'
| 'focus'
| 'click'
| 'change'
| 'clear'
| 'enter';
/**
*
*/
async function rendererEventDispatcher<T extends OptionsControlProps>(
props: T,
e: InputTextRendererEvent,
ctx: Record<string, any> = {}
): Promise<RendererEvent<any> | undefined> {
const {dispatchEvent, data} = props;
return dispatchEvent(e, createObject(data, ctx));
}
/**
*
*
* @param {InputTextRendererEvent} e
* @returns {Function}
*/
export function bindRendererEvent<T extends OptionsControlProps, P = any>(
e: InputTextRendererEvent,
ctx: Record<string, any> = {}
) {
return function (
target: any,
propertyKey: string,
descriptor: TypedPropertyDescriptor<any>
) {
let fn = descriptor.value;
if (!fn || typeof fn !== 'function') {
throw new Error(
`decorator can only be applied to methods not: ${typeof fn}`
);
}
return {
...descriptor,
value: async function boundFn(...params: any[]) {
const context = (this as TypedPropertyDescriptor<any> & {props: T})
?.props;
let value = e === 'clear' ? context?.resetValue : context?.value;
const dispatcher = await rendererEventDispatcher<T>(context, e, {
value
});
if (dispatcher?.prevented) {
return;
}
return fn.apply(this, [...params]);
}
};
};
}
export default class TextControl extends React.PureComponent<
TextProps,
TextState
@ -192,6 +262,19 @@ export default class TextControl extends React.PureComponent<
this.input = ref;
}
/**
*
*/
doAction(action: ListenerAction, args: any) {
const actionType = action?.actionType as string;
if (!!~['clear', 'reset'].indexOf(actionType)) {
this.clearValue();
} else if (actionType === 'focus') {
this.focus();
}
}
focus() {
if (!this.input) {
return;
@ -204,6 +287,7 @@ export default class TextControl extends React.PureComponent<
len && this.input.setSelectionRange(len, len);
}
@bindRendererEvent<TextProps>('clear')
clearValue() {
const {onChange, resetValue} = this.props;
@ -220,29 +304,15 @@ export default class TextControl extends React.PureComponent<
}
removeItem(index: number) {
const {
selectedOptions,
onChange,
joinValues,
extractValue,
delimiter,
valueField
} = this.props;
const {selectedOptions, onChange} = this.props;
const newValue = selectedOptions.concat();
newValue.splice(index, 1);
onChange(
joinValues
? newValue
.map(item => item[valueField || 'value'])
.join(delimiter || ',')
: extractValue
? newValue.map(item => item[valueField || 'value'])
: newValue
);
onChange(this.normalizeValue(newValue));
}
@bindRendererEvent<TextProps>('click')
handleClick() {
this.focus();
this.setState({
@ -250,6 +320,7 @@ export default class TextControl extends React.PureComponent<
});
}
@bindRendererEvent<TextProps>('focus')
handleFocus(e: any) {
this.setState({
isOpen: true,
@ -259,6 +330,7 @@ export default class TextControl extends React.PureComponent<
this.props.onFocus && this.props.onFocus(e);
}
@bindRendererEvent<TextProps>('blur')
handleBlur(e: any) {
const {onBlur, trimContents, value, onChange} = this.props;
@ -294,32 +366,16 @@ export default class TextControl extends React.PureComponent<
);
}
handleKeyDown(evt: React.KeyboardEvent<HTMLInputElement>) {
const {
selectedOptions,
onChange,
joinValues,
extractValue,
delimiter,
multiple,
valueField,
creatable
} = this.props;
async handleKeyDown(evt: React.KeyboardEvent<HTMLInputElement>) {
const {selectedOptions, onChange, multiple, creatable} = this.props;
if (selectedOptions.length && !this.state.inputValue && evt.keyCode === 8) {
evt.preventDefault();
const newValue = selectedOptions.concat();
newValue.pop();
onChange(
joinValues
? newValue
.map(item => item[valueField || 'value'])
.join(delimiter || ',')
: extractValue
? newValue.map(item => item[valueField || 'value'])
: newValue
);
onChange(this.normalizeValue(newValue));
this.setState(
{
inputValue: ''
@ -327,35 +383,39 @@ export default class TextControl extends React.PureComponent<
this.loadAutoComplete
);
} else if (
evt.keyCode === 13 &&
evt.key === 'Enter' &&
this.state.inputValue &&
typeof this.highlightedIndex !== 'number'
) {
evt.preventDefault();
const value = this.state.inputValue;
let value: string | Array<string | any> = this.state.inputValue;
if (multiple) {
if (value && !find(selectedOptions, item => item.value == value)) {
const newValue = selectedOptions.concat();
newValue.push({
label: value,
value: value
});
if (
multiple &&
value &&
!find(selectedOptions, item => item.value == value)
) {
const newValue = selectedOptions.concat();
newValue.push({
label: value,
value: value
});
onChange(
joinValues
? newValue
.map(item => item[valueField || 'value'])
.join(delimiter || ',')
: extractValue
? newValue.map(item => item[valueField || 'value'])
: newValue
);
}
} else {
onChange(value);
value = this.normalizeValue(newValue).concat();
}
const dispatcher = await rendererEventDispatcher<TextProps>(
this.props,
'enter',
{value}
);
if (dispatcher?.prevented) {
return;
}
onChange(value);
if (creatable === false || multiple) {
this.setState(
{
@ -366,7 +426,7 @@ export default class TextControl extends React.PureComponent<
);
}
} else if (
evt.keyCode === 13 &&
evt.key === 'Enter' &&
this.state.isOpen &&
typeof this.highlightedIndex !== 'number'
) {
@ -377,16 +437,7 @@ export default class TextControl extends React.PureComponent<
}
handleChange(value: any) {
const {
onChange,
multiple,
joinValues,
extractValue,
delimiter,
selectedOptions,
valueField,
creatable
} = this.props;
const {onChange, multiple, selectedOptions, creatable} = this.props;
if (multiple) {
const newValue = selectedOptions.concat();
@ -395,15 +446,7 @@ export default class TextControl extends React.PureComponent<
value: value
});
onChange(
joinValues
? newValue
.map(item => item[valueField || 'value'])
.join(delimiter || ',')
: extractValue
? newValue.map(item => item[valueField || 'value'])
: newValue
);
onChange(this.normalizeValue(newValue));
} else {
onChange(value);
}
@ -458,14 +501,32 @@ export default class TextControl extends React.PureComponent<
}
@autobind
handleNormalInputChange(e: React.ChangeEvent<HTMLInputElement>) {
async handleNormalInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const {onChange} = this.props;
let value = e.currentTarget.value;
const dispatcher = await rendererEventDispatcher<TextProps>(
this.props,
'change',
{value: this.transformValue(value)}
);
if (dispatcher?.prevented) {
return;
}
onChange(this.transformValue(value));
}
normalizeValue(value: Option[]) {
const {delimiter, joinValues, extractValue, valueField} = this.props;
return joinValues
? value.map(item => item[valueField || 'value']).join(delimiter || ',')
: extractValue
? value.map(item => item[valueField || 'value'])
: value;
}
transformValue(value: string) {
const {transform} = this.props;

View File

@ -30,10 +30,13 @@ import {
} from '../../Schema';
import {HocStoreFactory} from '../../WithStore';
import {wrapControl} from './wrapControl';
import type {OnEventProps} from '../../utils/renderer-event';
export type FormControlSchemaAlias = SchemaObject;
export interface FormBaseControl extends Omit<BaseSchema, 'type'> {
export interface FormBaseControl
extends Omit<BaseSchema, 'type'>,
OnEventProps {
/**
*
*/

View File

@ -26,6 +26,7 @@ import {
FormBaseControl
} from './Item';
import {IFormItemStore} from '../../store/formItem';
export type OptionsControlComponent = React.ComponentType<FormControlProps>;
import React from 'react';

View File

@ -21,6 +21,11 @@ export interface LinkSchema extends BaseSchema {
*/
blank?: boolean;
/**
*
*/
href?: string;
/**
*
*/