refactor(client): add trim API for Input and Variable.TextArea (#6624)

* refactor(client): add trim API for Input and Variable.TextArea

* fix(client): avoid trim property to be passed to inner component
This commit is contained in:
Junyi 2025-04-06 09:58:46 +08:00 committed by GitHub
parent 8f5ae04743
commit 393b46a3bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 68 additions and 18 deletions

View File

@ -11,22 +11,40 @@ import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty } from '@formily/react'; import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Input as AntdInput } from 'antd'; import { Input as AntdInput } from 'antd';
import { InputProps, TextAreaProps } from 'antd/es/input'; import { InputProps, TextAreaProps } from 'antd/es/input';
import React from 'react'; import React, { useCallback } from 'react';
import { JSONTextAreaProps, Json } from './Json'; import { JSONTextAreaProps, Json } from './Json';
import { InputReadPrettyComposed, ReadPretty } from './ReadPretty'; import { InputReadPrettyComposed, ReadPretty } from './ReadPretty';
export { ReadPretty as InputReadPretty } from './ReadPretty'; export { ReadPretty as InputReadPretty } from './ReadPretty';
type ComposedInput = React.FC<InputProps> & { type ComposedInput = React.FC<NocoBaseInputProps> & {
ReadPretty: InputReadPrettyComposed['Input']; ReadPretty: InputReadPrettyComposed['Input'];
TextArea: React.FC<TextAreaProps> & { ReadPretty: InputReadPrettyComposed['TextArea'] }; TextArea: React.FC<TextAreaProps> & { ReadPretty: InputReadPrettyComposed['TextArea'] };
URL: React.FC<InputProps> & { ReadPretty: InputReadPrettyComposed['URL'] }; URL: React.FC<InputProps> & { ReadPretty: InputReadPrettyComposed['URL'] };
JSON: React.FC<JSONTextAreaProps> & { ReadPretty: InputReadPrettyComposed['JSON'] }; JSON: React.FC<JSONTextAreaProps> & { ReadPretty: InputReadPrettyComposed['JSON'] };
}; };
export type NocoBaseInputProps = InputProps & {
trim?: boolean;
};
function InputInner(props: NocoBaseInputProps) {
const { onChange, trim, ...others } = props;
const handleChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
if (trim) {
ev.target.value = ev.target.value.trim();
}
onChange?.(ev);
},
[onChange, trim],
);
return <AntdInput {...others} onChange={handleChange} />;
}
export const Input: ComposedInput = Object.assign( export const Input: ComposedInput = Object.assign(
connect( connect(
AntdInput, InputInner,
mapProps((props, field) => { mapProps((props, field) => {
return { return {
...props, ...props,

View File

@ -1,4 +1,3 @@
import React from 'react'; import React from 'react';
import { mockApp } from '@nocobase/client/demo-utils'; import { mockApp } from '@nocobase/client/demo-utils';
import { SchemaComponent, Plugin, ISchema } from '@nocobase/client'; import { SchemaComponent, Plugin, ISchema } from '@nocobase/client';
@ -15,15 +14,25 @@ const schema: ISchema = {
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
}, },
trim: {
type: 'string',
title: `Trim heading and tailing spaces`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
trim: true,
},
},
}, },
} };
const Demo = () => { const Demo = () => {
return <SchemaComponent schema={schema} />; return <SchemaComponent schema={schema} />;
}; };
class DemoPlugin extends Plugin { class DemoPlugin extends Plugin {
async load() { async load() {
this.app.router.add('root', { path: '/', Component: Demo }) this.app.router.add('root', { path: '/', Component: Demo });
} }
} }

View File

@ -10,6 +10,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useForm } from '@formily/react'; import { useForm } from '@formily/react';
import { Space, theme } from 'antd'; import { Space, theme } from 'antd';
import type { CascaderProps, DefaultOptionType } from 'antd/lib/cascader';
import useInputStyle from 'antd/es/input/style'; import useInputStyle from 'antd/es/input/style';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { renderToString } from 'react-dom/server'; import { renderToString } from 'react-dom/server';
@ -110,7 +111,7 @@ function renderHTML(exp: string, keyLabelMap, delimiters: [string, string] = ['{
}); });
} }
function createOptionsValueLabelMap(options: any[], fieldNames = { value: 'value', label: 'label' }) { function createOptionsValueLabelMap(options: any[], fieldNames: CascaderProps['fieldNames'] = defaultFieldNames) {
const map = new Map<string, string[]>(); const map = new Map<string, string[]>();
for (const option of options) { for (const option of options) {
map.set(option[fieldNames.value], [option[fieldNames.label]]); map.set(option[fieldNames.value], [option[fieldNames.label]]);
@ -220,10 +221,24 @@ function useVariablesFromValue(value: string, delimiters: [string, string] = ['{
}, [value, delimitersString]); }, [value, delimitersString]);
} }
export function TextArea(props) { export type TextAreaProps = {
value?: string;
scope?: Partial<DefaultOptionType>[] | (() => Partial<DefaultOptionType>[]);
onChange?(value: string): void;
disabled?: boolean;
changeOnSelect?: CascaderProps['changeOnSelect'];
style?: React.CSSProperties;
fieldNames?: CascaderProps['fieldNames'];
trim?: boolean;
delimiters?: [string, string];
addonBefore?: React.ReactNode;
};
export function TextArea(props: TextAreaProps) {
const { wrapSSR, hashId, componentCls } = useStyles(); const { wrapSSR, hashId, componentCls } = useStyles();
const { scope, onChange, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'], addonBefore } = props; const { scope, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'], addonBefore, trim = true } = props;
const value = typeof props.value === 'string' ? props.value : props.value == null ? '' : props.value.toString(); const value =
typeof props.value === 'string' ? props.value : props.value == null ? '' : (props.value as any).toString();
const variables = useVariablesFromValue(value, delimiters); const variables = useVariablesFromValue(value, delimiters);
const inputRef = useRef<HTMLDivElement>(null); const inputRef = useRef<HTMLDivElement>(null);
const [options, setOptions] = useState([]); const [options, setOptions] = useState([]);
@ -241,6 +256,14 @@ export function TextArea(props) {
const { token } = theme.useToken(); const { token } = theme.useToken();
const delimitersString = delimiters.join(' '); const delimitersString = delimiters.join(' ');
const onChange = useCallback(
(target: HTMLDivElement) => {
const v = getValue(target, delimiters);
props.onChange?.(trim ? v.trim() : v);
},
[delimitersString, props.onChange, trim],
);
useEffect(() => { useEffect(() => {
preloadOptions(scope, variables) preloadOptions(scope, variables)
.then((preloaded) => { .then((preloaded) => {
@ -324,9 +347,9 @@ export function TextArea(props) {
setChanged(true); setChanged(true);
setRange(getCurrentRange(current)); setRange(getCurrentRange(current));
onChange(getValue(current, delimiters)); onChange(current);
}, },
[keyLabelMap, onChange, range, delimitersString], [keyLabelMap, onChange, range],
); );
const onInput = useCallback( const onInput = useCallback(
@ -336,9 +359,9 @@ export function TextArea(props) {
} }
setChanged(true); setChanged(true);
setRange(getCurrentRange(currentTarget)); setRange(getCurrentRange(currentTarget));
onChange(getValue(currentTarget, delimiters)); onChange(currentTarget);
}, },
[ime, onChange, delimitersString], [ime, onChange],
); );
const onBlur = useCallback(function ({ currentTarget }) { const onBlur = useCallback(function ({ currentTarget }) {
@ -360,9 +383,9 @@ export function TextArea(props) {
setIME(false); setIME(false);
setChanged(true); setChanged(true);
setRange(getCurrentRange(currentTarget)); setRange(getCurrentRange(currentTarget));
onChange(getValue(currentTarget, delimiters)); onChange(currentTarget);
}, },
[onChange, delimitersString], [onChange],
); );
const onPaste = useCallback( const onPaste = useCallback(
@ -393,9 +416,9 @@ export function TextArea(props) {
setChanged(true); setChanged(true);
pasteHTML(ev.currentTarget, sanitizedHTML); pasteHTML(ev.currentTarget, sanitizedHTML);
setRange(getCurrentRange(ev.currentTarget)); setRange(getCurrentRange(ev.currentTarget));
onChange(getValue(ev.currentTarget, delimiters)); onChange(ev.currentTarget);
}, },
[onChange, delimitersString], [onChange],
); );
const disabled = props.disabled || form.disabled; const disabled = props.disabled || form.disabled;
return wrapSSR( return wrapSSR(