Prechádzať zdrojové kódy

chore: setValue ts interface support recur path (#2245)

* chore: getValue recur path, #2243, #1737
pointhalo 1 rok pred
rodič
commit
599618a8c7

+ 85 - 103
content/input/autocomplete/index.md

@@ -27,7 +27,7 @@ import { AutoComplete } from '@douyinfe/semi-ui';
 
 ### 基本用法
 
-通过 onSearch 监听用户输入,将输入建议通过 data 传入,通过 onChange 保持受控,当输入框变化/选中输入项时会触发 onChange
+通过 onSearch 监听用户输入,将输入建议通过更新 props.data 传入。通过 onChange 保持受控,当输入框变化/选中输入项时会触发 onChange
 
 ```jsx live=true
 import React from 'react';
@@ -76,34 +76,29 @@ import React from 'react';
 import { AutoComplete, Avatar } from '@douyinfe/semi-ui';
 import { IconSearch } from '@douyinfe/semi-icons';
 
-class CustomOptionDemo extends React.Component {
-    constructor() {
-        super();
-        this.state = {
-            data: [],
-            color: ['amber', 'indigo', 'cyan'],
-            list: [
-                { name: '夏可漫', email: '[email protected]', abbr: 'XK', color: 'amber' },
-                { name: '申悦', email: '[email protected]', abbr: 'SY', color: 'indigo' },
-                { name: '曲晨一', email: '[email protected]', abbr: 'CY', color: 'blue' },
-                { name: '文嘉茂', email: '[email protected]', abbr: 'JM', color: 'cyan' },
-            ],
-        };
-    }
-
-    search(value) {
+() => {
+    const color = ['amber', 'indigo', 'cyan'];
+    const [data, setData] = useState([
+        { name: '夏可漫', email: '[email protected]', abbr: 'XK', color: 'amber' },
+        { name: '申悦', email: '[email protected]', abbr: 'SY', color: 'indigo' },
+        { name: '曲晨一', email: '[email protected]', abbr: 'CY', color: 'blue' },
+        { name: '文嘉茂', email: '[email protected]', abbr: 'JM', color: 'cyan' },
+    ]);
+    const [value, setValue] = useState('');
+    
+    const handleStringSearch = (value) => {
         let result;
         if (value) {
-            result = this.state.list.map(item => {
+            result = data.map(item => {
                 return { ...item, value: item.name, label: item.email };
             });
         } else {
             result = [];
         }
-        this.setState({ data: result });
-    }
+        setData(result);
+    };
 
-    renderOption(item) {
+    const renderOption = (item) => {
         let optionStyle = {
             display: 'flex',
         };
@@ -120,103 +115,90 @@ class CustomOptionDemo extends React.Component {
         );
     }
 
-    render() {
-        return (
-            <AutoComplete
-                data={this.state.data}
-                prefix={<IconSearch />}
-                style={{ width: '250px' }}
-                renderSelectedItem={option => option.email}
-                renderItem={this.renderOption}
-                onSearch={this.search.bind(this)}
-                onSelect={v => console.log(v)}
-            ></AutoComplete>
-        );
-    }
-}
+
+    return (
+        <AutoComplete
+            data={data}
+            showClear
+            prefix={<IconSearch />}
+            onSearch={handleStringSearch}
+            renderItem={renderOption}
+            renderSelectedItem={option => option.email}
+            style={{ width: 280 }}
+        />
+    );
+};
 ```
 
 ### 远程搜索
 
-从 onSearch 中获取用户输入值,动态更新 data 值,更新 loading
+从 onSearch 中获取用户输入值,动态更新 data 值
 
 ```jsx live=true
 import React from 'react';
 import { AutoComplete } from '@douyinfe/semi-ui';
 import { IconSearch } from '@douyinfe/semi-icons';
+import { IconSelect, IconForm, IconTable, IconInput, IconButton } from '@douyinfe/semi-icons-lab';
 
-class ObjectDemo extends React.Component {
-    constructor() {
-        super();
-        this.state = {
-            list: [
-                { value: 'abc', label: 'douyin', email: '[email protected]', type: 2 },
-                { value: 'hotsoon', label: 'huoshan', email: '[email protected]', type: 3 },
-                { value: 'pipixia', label: 'pip', email: '[email protected]' },
-            ],
-            loading: false,
-        };
-        this.onSearch = this.onSearch.bind(this);
-        this.handleSelect = this.handleSelect.bind(this);
-        this.renderItem = this.renderItem.bind(this);
-        this.renderSelectedItem = this.renderSelectedItem.bind(this);
-        this.search = debounce(this.search.bind(this), 200);
-    }
+() => {
+    let initList = [
+        { value: 'select', label: '选择器', icon: <IconSelect/> },
+        { value: 'input', label: '输入框', icon: <IconInput/> },
+        { value: 'form', label: '表单', icon: <IconForm /> },
+        { value: 'button', label: '按钮', icon: <IconButton /> },
+        { value: 'table', label: '表格', icon: <IconTable /> },
+    ];
 
-    onSearch(inputValue) {
-        this.setState({ loading: true });
-        this.search(inputValue);
-    }
+    const [loading, setLoading] = useState(false);
+    const [list, setList] = useState(initList);
 
-    search(inputValue) {
-        let { list } = this.state;
-        const newList = list.map(item => {
-            let num = Math.random()
-                .toString()
-                .slice(2, 5);
-            let option = inputValue + '-' + num;
-            return { ...item, label: '名称:' + option, value: option };
-        });
-        this.setState({ list: newList, loading: false });
-    }
+    const handleSearch = (inputValue) => {
+        setLoading(true);
+        let newList = initList;
+        if (inputValue) {
+            newList = list.filter(item => item.value.includes(inputValue));
+        }
+        setTimeout(() => {
+            setList(newList);
+            setLoading(false);
+        }, 1000);
+    };
 
-    handleSelect(value) {
+    const search = debounce(handleSearch, 200);
+
+    const handleSelect = () => {
         console.log(value);
-    }
+    };
 
-    renderItem(item) {
+    const renderItem = (item) => {
         return (
-            <div>
-                <div>{item.label}</div>
-                <div>email: {item.email}</div>
-                <div style={{ color: 'pink' }}>value: {item.value}</div>
+            <div style={{ display: 'flex', alignItems: 'center' }}>
+                <div style={{ fontSize: 32 }}>{item.icon}</div>
+                <div style={{ marginLeft: 12 }}>
+                    <p>{item.value}</p>
+                    <p>{item.label}</p>
+                </div>
             </div>
         );
-    }
+    };
 
-    renderSelectedItem(item) {
-        // 注意:与Select不同,此处只能返回String类型的值,不能返回ReactNode
+    const renderSelectedItem = (item) => {
+        // 注意:与其他组件如Select不同,此处只能返回String类型的值,不能返回ReactNode
         return item.value;
-    }
+    };
 
-    render() {
-        const { loading } = this.state;
-        return (
-            <div>
-                <AutoComplete
-                    data={this.state.list}
-                    style={{ width: 250 }}
-                    prefix={<IconSearch />}
-                    onSearch={this.onSearch}
-                    loading={loading}
-                    onChangeWithObject
-                    renderItem={this.renderItem}
-                    renderSelectedItem={this.renderSelectedItem}
-                    onSelect={this.handleSelect}
-                ></AutoComplete>
-            </div>
-        );
-    }
+    return (
+        <AutoComplete
+            data={list}
+            style={{ width: 250 }}
+            prefix={<IconSearch />}
+            onSearch={search}
+            loading={loading}
+            renderItem={renderItem}
+            renderSelectedItem={renderSelectedItem}
+            onSelect={handleSelect}
+        ></AutoComplete>
+    );
 }
 ```
 
@@ -373,9 +355,9 @@ import { IllustrationNoContent } from '@douyinfe/semi-illustrations';
 | 属性 | 说明                                                                                                         | 类型 | 默认值 | 版本|
 | --- |------------------------------------------------------------------------------------------------------------| --- | --- |--- |
 | autoFocus | 是否自动聚焦                                                                                                     | bool | false | 1.16.0|
-| autoAdjustOverflow | 浮层被遮挡时是否自动调整方向                                                                                             | bool | true | 0.27.0|
+| autoAdjustOverflow | 浮层被遮挡时是否自动调整方向                                                                                             | bool | true | |
 | className | 样式类名                                                                                                       | string | |
-| clearIcon | 可用于自定义清除按钮, showClear为true时有效                                                                              | ReactNode | 2.25.0  |
+| clearIcon | 可用于自定义清除按钮, showClear为true时有效                                                                              | ReactNode |   | 2.25.0
 | data | 候选项的数据源,可以为字符串数组或对象数组                                                                                      | array | [] |
 | defaultActiveFirstOption | 是否默认高亮第一个选项(按回车可直接选中)                                                                                      | bool | false |
 | defaultOpen | 是否默认展开下拉菜单                                                                                                 | boolean | false |
@@ -384,20 +366,20 @@ import { IllustrationNoContent } from '@douyinfe/semi-illustrations';
 | dropdownClassName | 下拉列表的 CSS 类名                                                                                               | string |  |
 | dropdownStyle | 下拉列表的内联样式                                                                                                  | object |  |
 | emptyContent | data 为空时自定义下拉内容                                                                                            | ReactNode | null | 1.16.0 |
-| getPopupContainer | 指定父级 DOM,下拉列表浮层将会渲染至该 DOM 中,自定义需要设置 `position: relative` 这会改变浮层 DOM 树位置,但不会改变视图渲染位置 | () => HTMLElement | () => document.body |
+| getPopupContainer | 指定下拉列表浮层的父级容器,浮层将会渲染至该 DOM 中。自定义该项时需给容器设置 `position: relative` 这会改变浮层 DOM 树位置,但不会改变视图渲染位置 | () => HTMLElement | () => document.body |
 | loading | 下拉列表是否展示加载动画                                                                                               | boolean | false |
 | maxHeight | 下拉列表的最大高度                                                                                                  | number\|string | 300 |
 | motion | 下拉列表出现/隐藏时,是否有动画                                                                                           | boolean | true |
 | onSelectWithObject | 点击候选项时,是否将选中项 option 的其他属性也作为回调入参。设为 true 时,onSelect 的入参类型会从 `string` 变为 object: { value, label, ...rest } | boolean | false |1.23.0 |
-| placeholder | 输入框提示                                                                                                      | string | |
+| placeholder | 输入框默认提示文案                                                                                                      | string | |
 | position | 下拉菜单的显示位置,可选值同 tooltip 组件                                                                                  | string | 'bottomLeft' |
 | prefix | 选择框的前缀标签                                                                                                   | ReactNode |  | 0.23.0|
 | renderItem | 控制下拉列表候选项的渲染                                                                                               | (option: string\|Item)=> React.Node |  |
-| renderSelectedItem | 通过 renderSelectedItem 自定义下拉列表候选项被点击选中后,在选择框中的渲染内容<br/>**仅支持 String 类型的返回值**<br/>                           | (option: string\|Item) => string |  |0.23.0 |
+| renderSelectedItem | 通过 renderSelectedItem 自定义下拉列表候选项被点击选中后,在选择框中的渲染内容<br/>**仅支持 String 类型的返回值**<br/>                           | (option: string\|Item) => string |  | |
 | showClear | 是否展示清除按钮                                                                                                   | boolean | false |
 | size | 尺寸,可选`small`, `default`, `large`                                                                           | string | `default` |
 | style | 样式                                                                                                         | object |  |
-| suffix | 选择框的前缀标签                                                                                                   | ReactNode |  |0.23.0 |
+| suffix | 选择框的前缀标签                                                                                                   | ReactNode |  | |
 | validateStatus | 校验状态,可选值`default`、`error`、`warning`,默认 default。仅影响展示样式                                                     | string | 'default' | 1.14.0|
 | value | 当前值                                                                                                        | string\|number | 无 |
 | zIndex | 下拉菜单的 zIndex                                                                                               | number |  |

+ 11 - 1
packages/semi-foundation/form/interface.ts

@@ -36,12 +36,22 @@ export interface setValuesConfig {
     isOverride: boolean
 }
 
+type ExcludeStringNumberKeys<T> = {
+    [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]
+}
+
+type CustomKeys<T> = { [K in keyof T]: string extends K ? never : number extends K ? never : K; } extends { [K in keyof T]: infer U } ? U : never;
+
+type FieldPath<T, K extends CustomKeys<T> = CustomKeys<T>> = K extends string ? T[K] extends Record<string, any> ? `${K}.${FieldPath<T[K], CustomKeys<T[K]>>}` | K : K : never;
+
 // use object replace Record<string, any>, fix issue 933
 export interface BaseFormApi<T extends object = any> {
+// export interface BaseFormApi<T extends object = any> {
     /** get value of field */
     getValue: <K extends keyof T>(field?: K) => T[K];
     /** set value of field */
-    setValue: <K extends keyof T>(field: K, newFieldValue: T[K]) => void;
+    setValue: <K extends CustomKeys<T>>(field: FieldPath<T, K> | FieldPath<K>, newFieldValue: any) => void;
+    // setValue: <K extends keyof T>(field: K, newFieldValue: T[K]) => void;
     /** get error of field */
     getError: <K extends keyof T>(field: K) => any;
     /** set error of field */

+ 0 - 1
packages/semi-foundation/tsconfig.json

@@ -9,7 +9,6 @@
         "lib": ["es7", "dom", "es2017"],
         "moduleResolution": "node",
         "noImplicitAny": false,
-        "suppressImplicitAnyIndexErrors": true,
         "forceConsistentCasingInFileNames": true,
         "allowSyntheticDefaultImports": true,
         "experimentalDecorators": true,

+ 0 - 1
packages/semi-icons/tsconfig.json

@@ -10,7 +10,6 @@
         "moduleResolution": "node",
         "noImplicitAny": false,
         "jsx": "react",
-        "suppressImplicitAnyIndexErrors": true,
         "forceConsistentCasingInFileNames": true,
         "allowSyntheticDefaultImports": true,
         "experimentalDecorators": true,

+ 22 - 3
packages/semi-ui/form/_story/form.stories.tsx

@@ -76,7 +76,7 @@ const Fields: FunctionComponent<FormFCChild> = ({ formState, values, formApi })
         <Input size='default' showClear insetLabel />
         <FieldB insetLabel placeholder='fe' fieldClassName='fefe' field='custom' />
 
-        <Button onClick={() => formApi.setValue('fieldA', 'fe')}>set</Button>
+        {/* <Button onClick={() => formApi.setValue('fieldA', 'fe')}>set</Button> */}
         <Form.Select field='test' ref={ref}>
             <Form.Select.Option value="f1"></Form.Select.Option>
             <Form.Select.Option value="f2"></Form.Select.Option>
@@ -155,17 +155,36 @@ interface FData {
     test4: {
         event: string,
     },
+    test5: {
+        kkk: {
+            jjj: string
+        }
+    }
     testK: boolean;
     // [x: string]: any;
 }
 class Demo extends React.Component<IProps, IState> {
+
+    formApi: FormApi<FData>
+
     constructor(props:any) {
       super(props);
       this.state = { visible: false};
     }
 
-    getFormApi(formApi: FormApi<FData>) {
-        formApi.getValue()
+    getFormApi(formApi) {
+        this.formApi = formApi;
+    }
+
+    setData() {
+        const formApi = this.formApi;
+        formApi.setValue('test3', 123);
+        formApi.setValue('test8', 123);
+        formApi.setValue('test4.event', 123);
+        formApi.setValue('test5.kkk', 123);
+        formApi.setValue('test5.kkk.jjj', 123);
+        formApi.setValue('test5.kkk.ppp', 123);
+        formApi.setValue('test4.5', 123);
     }
 
     render() {

+ 1 - 0
packages/semi-ui/form/hooks/useFieldApi.tsx

@@ -9,6 +9,7 @@ const buildFieldApi = (formApi: FormApi, field: string) => ({
     getTouched: () => formApi.getTouched(field),
     setTouched: (isTouched: boolean) => formApi.setTouched(field, isTouched),
     getValue: () => formApi.getValue(field),
+    // @ts-ignore
     setValue: (value: any) => formApi.setValue(field, value),
 });
 

+ 0 - 1
packages/semi-ui/tsconfig.json

@@ -16,7 +16,6 @@
             "@douyinfe/semi-theme-default/*": ["../semi-theme-default/*"],
             "@douyinfe/semi-icons": ["../semi-icons/src"]
         },
-        "suppressImplicitAnyIndexErrors": true,
         "forceConsistentCasingInFileNames": true,
         "allowSyntheticDefaultImports": true,
         "experimentalDecorators": true,