mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
fix: fix FieldArrayModel.swap state issue with doc example covered (#70)
* feat: add get values to FormModel * fix: fix FieldArrayModel.swap state issue with doc example added * doc: fix array demo code
This commit is contained in:
parent
3dc0b94dd8
commit
6dd4e2d3ab
@ -4,7 +4,7 @@ import './field-wrapper.css';
|
|||||||
|
|
||||||
interface FieldWrapperProps {
|
interface FieldWrapperProps {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
title: string;
|
title?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
error?: string;
|
error?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import './field-wrapper.css';
|
|||||||
|
|
||||||
interface FieldWrapperProps {
|
interface FieldWrapperProps {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
title: string;
|
title?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
error?: string;
|
error?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
|
|||||||
7
apps/docs/components/node-form/array/index.css
Normal file
7
apps/docs/components/node-form/array/index.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.array-item-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.icon-button-popover {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
116
apps/docs/components/node-form/array/node-registry.tsx
Normal file
116
apps/docs/components/node-form/array/node-registry.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
DataEvent,
|
||||||
|
EffectFuncProps,
|
||||||
|
Field,
|
||||||
|
FieldRenderProps,
|
||||||
|
FormMeta,
|
||||||
|
ValidateTrigger,
|
||||||
|
WorkflowNodeRegistry,
|
||||||
|
FieldArray,
|
||||||
|
FieldArrayRenderProps,
|
||||||
|
} from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { FieldWrapper } from '@flowgram.ai/demo-node-form';
|
||||||
|
import { Input, Button, Popover } from '@douyinfe/semi-ui';
|
||||||
|
import { IconPlus, IconCrossCircleStroked, IconArrowDown } from '@douyinfe/semi-icons';
|
||||||
|
import './index.css';
|
||||||
|
import '../index.css';
|
||||||
|
|
||||||
|
export const render = () => (
|
||||||
|
<div className="demo-node-content">
|
||||||
|
<div className="demo-node-title">Array Examples</div>
|
||||||
|
<FieldArray name="array">
|
||||||
|
{({ field, fieldState }: FieldArrayRenderProps<string>) => (
|
||||||
|
<FieldWrapper title={'My Array'}>
|
||||||
|
{field.map((child, index) => (
|
||||||
|
<Field name={child.name} key={child.key}>
|
||||||
|
{({ field: childField, fieldState: childState }: FieldRenderProps<string>) => (
|
||||||
|
<FieldWrapper error={childState.errors?.[0]?.message}>
|
||||||
|
<div className="array-item-wrapper">
|
||||||
|
<Input {...childField} size={'small'} />
|
||||||
|
{index < field.value!.length - 1 ? (
|
||||||
|
<Popover
|
||||||
|
content={'swap with next element'}
|
||||||
|
className={'icon-button-popover'}
|
||||||
|
showArrow
|
||||||
|
position={'topLeft'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
theme="borderless"
|
||||||
|
size={'small'}
|
||||||
|
icon={<IconArrowDown />}
|
||||||
|
onClick={() => field.swap(index, index + 1)}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
) : null}
|
||||||
|
<Popover
|
||||||
|
content={'delete current element'}
|
||||||
|
className={'icon-button-popover'}
|
||||||
|
showArrow
|
||||||
|
position={'topLeft'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
theme="borderless"
|
||||||
|
size={'small'}
|
||||||
|
icon={<IconCrossCircleStroked />}
|
||||||
|
onClick={() => field.delete(index)}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</FieldWrapper>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size={'small'}
|
||||||
|
theme="borderless"
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => field.append('default')}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FieldWrapper>
|
||||||
|
)}
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
array: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formMeta: FormMeta<FormData> = {
|
||||||
|
render,
|
||||||
|
validateTrigger: ValidateTrigger.onChange,
|
||||||
|
defaultValues: {
|
||||||
|
array: ['default'],
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
'array.*': ({ value }) =>
|
||||||
|
value.length > 8 ? 'max length exceeded: current length is ' + value.length : undefined,
|
||||||
|
},
|
||||||
|
effect: {
|
||||||
|
'array.*': [
|
||||||
|
{
|
||||||
|
event: DataEvent.onValueInit,
|
||||||
|
effect: ({ value, name }: EffectFuncProps<string, FormData>) => {
|
||||||
|
console.log(name + ' value init to ', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: DataEvent.onValueChange,
|
||||||
|
effect: ({ value, name }: EffectFuncProps<string, FormData>) => {
|
||||||
|
console.log(name + ' value changed to ', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nodeRegistry: WorkflowNodeRegistry = {
|
||||||
|
type: 'custom',
|
||||||
|
meta: {},
|
||||||
|
defaultPorts: [{ type: 'output' }, { type: 'input' }],
|
||||||
|
formMeta,
|
||||||
|
};
|
||||||
146
apps/docs/components/node-form/array/preview.tsx
Normal file
146
apps/docs/components/node-form/array/preview.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_INITIAL_DATA,
|
||||||
|
defaultInitialDataTs,
|
||||||
|
fieldWrapperCss,
|
||||||
|
fieldWrapperTs,
|
||||||
|
} from '@flowgram.ai/demo-node-form';
|
||||||
|
|
||||||
|
import { Editor } from '../editor.tsx';
|
||||||
|
import { PreviewEditor } from '../../preview-editor.tsx';
|
||||||
|
import { nodeRegistry } from './node-registry.tsx';
|
||||||
|
|
||||||
|
const nodeRegistryFile = {
|
||||||
|
code: `import {
|
||||||
|
DataEvent,
|
||||||
|
EffectFuncProps,
|
||||||
|
Field,
|
||||||
|
FieldRenderProps,
|
||||||
|
FormMeta,
|
||||||
|
ValidateTrigger,
|
||||||
|
WorkflowNodeRegistry,
|
||||||
|
FieldArray,
|
||||||
|
FieldArrayRenderProps,
|
||||||
|
} from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { FieldWrapper } from '@flowgram.ai/demo-node-form';
|
||||||
|
import { Input, Button, Popover } from '@douyinfe/semi-ui';
|
||||||
|
import { IconPlus, IconCrossCircleStroked, IconArrowDown } from '@douyinfe/semi-icons';
|
||||||
|
import './index.css';
|
||||||
|
import '../index.css';
|
||||||
|
|
||||||
|
export const render = () => (
|
||||||
|
<div className="demo-node-content">
|
||||||
|
<div className="demo-node-title">Array Examples</div>
|
||||||
|
<FieldArray name="array">
|
||||||
|
{({ field, fieldState }: FieldArrayRenderProps<string>) => (
|
||||||
|
<FieldWrapper title={'My Array'}>
|
||||||
|
{field.map((child, index) => (
|
||||||
|
<Field name={child.name} key={child.key}>
|
||||||
|
{({ field: childField, fieldState: childState }: FieldRenderProps<string>) => (
|
||||||
|
<FieldWrapper error={childState.errors?.[0]?.message}>
|
||||||
|
<div className="array-item-wrapper">
|
||||||
|
<Input {...childField} size={'small'} />
|
||||||
|
{index < field.value!.length - 1 ? (
|
||||||
|
<Popover
|
||||||
|
content={'swap with next element'}
|
||||||
|
className={'icon-button-popover'}
|
||||||
|
showArrow
|
||||||
|
position={'topLeft'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
theme="borderless"
|
||||||
|
size={'small'}
|
||||||
|
icon={<IconArrowDown />}
|
||||||
|
onClick={() => field.swap(index, index + 1)}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
) : null}
|
||||||
|
<Popover
|
||||||
|
content={'delete current element'}
|
||||||
|
className={'icon-button-popover'}
|
||||||
|
showArrow
|
||||||
|
position={'topLeft'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
theme="borderless"
|
||||||
|
size={'small'}
|
||||||
|
icon={<IconCrossCircleStroked />}
|
||||||
|
onClick={() => field.delete(index)}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</FieldWrapper>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size={'small'}
|
||||||
|
theme="borderless"
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => field.append('default')}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FieldWrapper>
|
||||||
|
)}
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
array: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formMeta: FormMeta<FormData> = {
|
||||||
|
render,
|
||||||
|
validateTrigger: ValidateTrigger.onChange,
|
||||||
|
defaultValues: {
|
||||||
|
array: ['default'],
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
'array.*': ({ value }) =>
|
||||||
|
value.length > 8 ? 'max length exceeded: current length is ' + value.length : undefined,
|
||||||
|
},
|
||||||
|
effect: {
|
||||||
|
'array.*': [
|
||||||
|
{
|
||||||
|
event: DataEvent.onValueInit,
|
||||||
|
effect: ({ value, name }: EffectFuncProps<string, FormData>) => {
|
||||||
|
console.log(name + ' value init to ', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: DataEvent.onValueChange,
|
||||||
|
effect: ({ value, name }: EffectFuncProps<string, FormData>) => {
|
||||||
|
console.log(name + ' value changed to ', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nodeRegistry: WorkflowNodeRegistry = {
|
||||||
|
type: 'custom',
|
||||||
|
meta: {},
|
||||||
|
defaultPorts: [{ type: 'output' }, { type: 'input' }],
|
||||||
|
formMeta,
|
||||||
|
};
|
||||||
|
|
||||||
|
`,
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeFormArrayPreview = () => {
|
||||||
|
const files = {
|
||||||
|
'node-registry.tsx': nodeRegistryFile,
|
||||||
|
'initial-data.ts': { code: defaultInitialDataTs, active: true },
|
||||||
|
'field-wrapper.tsx': { code: fieldWrapperTs, active: true },
|
||||||
|
'field-wrapper.css': { code: fieldWrapperCss, active: true },
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<PreviewEditor files={files} previewStyle={{ height: 500 }} editorStyle={{ height: 500 }}>
|
||||||
|
<Editor registry={nodeRegistry} initialData={DEFAULT_INITIAL_DATA} />
|
||||||
|
</PreviewEditor>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
[
|
[
|
||||||
"basic",
|
"basic",
|
||||||
"effect"
|
"effect",
|
||||||
|
"array"
|
||||||
]
|
]
|
||||||
|
|||||||
10
apps/docs/src/zh/examples/node-form/array.mdx
Normal file
10
apps/docs/src/zh/examples/node-form/array.mdx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
outline: false
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 数组
|
||||||
|
|
||||||
|
import { NodeFormArrayPreview } from '../../../../components/node-form/array/preview';
|
||||||
|
|
||||||
|
<NodeFormArrayPreview />
|
||||||
@ -4,6 +4,8 @@ import { Errors, ValidateTrigger, Warnings } from '@/types';
|
|||||||
import { FormModel } from '@/core/form-model';
|
import { FormModel } from '@/core/form-model';
|
||||||
import { type FieldArrayModel } from '@/core/field-array-model';
|
import { type FieldArrayModel } from '@/core/field-array-model';
|
||||||
|
|
||||||
|
import { FeedbackLevel } from '../src/types';
|
||||||
|
|
||||||
describe('FormArrayModel', () => {
|
describe('FormArrayModel', () => {
|
||||||
let formModel = new FormModel();
|
let formModel = new FormModel();
|
||||||
describe('children', () => {
|
describe('children', () => {
|
||||||
@ -580,53 +582,171 @@ describe('FormArrayModel', () => {
|
|||||||
|
|
||||||
it('can swap from 0 to middle index', () => {
|
it('can swap from 0 to middle index', () => {
|
||||||
const arrayField = formModel.createFieldArray('arr');
|
const arrayField = formModel.createFieldArray('arr');
|
||||||
arrayField!.append('a');
|
const a = arrayField!.append('a');
|
||||||
arrayField!.append('b');
|
const b = arrayField!.append('b');
|
||||||
arrayField!.append('c');
|
const c = arrayField!.append('c');
|
||||||
|
|
||||||
formModel.init({});
|
formModel.init({});
|
||||||
|
|
||||||
|
a.state.errors = {
|
||||||
|
'arr.0': [{ name: 'arr.0', message: 'err0', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
b.state.errors = {
|
||||||
|
'arr.1': [{ name: 'arr.1', message: 'err1', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
|
||||||
expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });
|
expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });
|
||||||
arrayField.swap(0, 1);
|
arrayField.swap(0, 1);
|
||||||
expect(formModel.values).toEqual({ arr: ['b', 'a', 'c'] });
|
expect(formModel.values).toEqual({ arr: ['b', 'a', 'c'] });
|
||||||
|
expect(formModel.getField('arr.0').state.errors).toEqual({
|
||||||
|
'arr.0': [{ name: 'arr.0', message: 'err1', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
|
expect(formModel.getField('arr.1').state.errors).toEqual({
|
||||||
|
'arr.1': [{ name: 'arr.1', message: 'err0', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can swap from 0 to last index', () => {
|
it('can swap from 0 to last index', () => {
|
||||||
const arrayField = formModel.createFieldArray('arr');
|
const arrayField = formModel.createFieldArray('arr');
|
||||||
arrayField!.append('a');
|
const a = arrayField!.append('a');
|
||||||
arrayField!.append('b');
|
const b = arrayField!.append('b');
|
||||||
arrayField!.append('c');
|
const c = arrayField!.append('c');
|
||||||
|
|
||||||
formModel.init({});
|
formModel.init({});
|
||||||
|
|
||||||
|
a.state.errors = {
|
||||||
|
'arr.0': [{ name: 'arr.0', message: 'err0', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
c.state.errors = {
|
||||||
|
'arr.2': [{ name: 'arr.2', message: 'err2', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
|
||||||
expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });
|
expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });
|
||||||
arrayField.swap(0, 2);
|
arrayField.swap(0, 2);
|
||||||
expect(formModel.values).toEqual({ arr: ['c', 'b', 'a'] });
|
expect(formModel.values).toEqual({ arr: ['c', 'b', 'a'] });
|
||||||
|
expect(formModel.getField('arr.0').state.errors).toEqual({
|
||||||
|
'arr.0': [{ name: 'arr.0', message: 'err2', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
|
expect(formModel.getField('arr.2').state.errors).toEqual({
|
||||||
|
'arr.2': [{ name: 'arr.2', message: 'err0', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('can swap from middle index to last index', () => {
|
it('can swap from middle index to last index', () => {
|
||||||
const arrayField = formModel.createFieldArray('arr');
|
const arrayField = formModel.createFieldArray('arr');
|
||||||
arrayField!.append('a');
|
const a = arrayField!.append('a');
|
||||||
arrayField!.append('b');
|
const b = arrayField!.append('b');
|
||||||
arrayField!.append('c');
|
const c = arrayField!.append('c');
|
||||||
|
|
||||||
formModel.init({});
|
formModel.init({});
|
||||||
|
|
||||||
|
b.state.errors = {
|
||||||
|
'arr.1': [{ name: 'arr.1', message: 'err1', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
c.state.errors = {
|
||||||
|
'arr.2': [{ name: 'arr.2', message: 'err2', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
|
||||||
expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });
|
expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });
|
||||||
arrayField.swap(1, 2);
|
arrayField.swap(1, 2);
|
||||||
expect(formModel.values).toEqual({ arr: ['a', 'c', 'b'] });
|
expect(formModel.values).toEqual({ arr: ['a', 'c', 'b'] });
|
||||||
|
expect(formModel.getField('arr.1').state.errors).toEqual({
|
||||||
|
'arr.1': [{ name: 'arr.1', message: 'err2', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
|
expect(formModel.getField('arr.2').state.errors).toEqual({
|
||||||
|
'arr.2': [{ name: 'arr.2', message: 'err1', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('can swap from middle index to another middle index', () => {
|
it('can swap from middle index to another middle index', () => {
|
||||||
const arrayField = formModel.createFieldArray('arr');
|
const arrayField = formModel.createFieldArray('arr');
|
||||||
arrayField!.append('a');
|
arrayField!.append('a');
|
||||||
arrayField!.append('b');
|
const b = arrayField!.append('b');
|
||||||
arrayField!.append('c');
|
const c = arrayField!.append('c');
|
||||||
arrayField!.append('d');
|
arrayField!.append('d');
|
||||||
|
|
||||||
formModel.init({});
|
formModel.init({});
|
||||||
|
|
||||||
|
b.state.errors = {
|
||||||
|
'arr.1': [{ name: 'arr.1', message: 'err1', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
c.state.errors = {
|
||||||
|
'arr.2': [{ name: 'arr.2', message: 'err2', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
|
||||||
expect(formModel.values).toEqual({ arr: ['a', 'b', 'c', 'd'] });
|
expect(formModel.values).toEqual({ arr: ['a', 'b', 'c', 'd'] });
|
||||||
arrayField.swap(1, 2);
|
arrayField.swap(1, 2);
|
||||||
expect(formModel.values).toEqual({ arr: ['a', 'c', 'b', 'd'] });
|
expect(formModel.values).toEqual({ arr: ['a', 'c', 'b', 'd'] });
|
||||||
|
expect(formModel.getField('arr.1').state.errors).toEqual({
|
||||||
|
'arr.1': [{ name: 'arr.1', message: 'err2', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
|
expect(formModel.getField('arr.2').state.errors).toEqual({
|
||||||
|
'arr.2': [{ name: 'arr.2', message: 'err1', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can swap for nested array', () => {
|
||||||
|
const arrayField = formModel.createFieldArray('arr');
|
||||||
|
const a = arrayField!.append({ x: 'x0', y: 'y0' });
|
||||||
|
const b = arrayField!.append({ x: 'x1', y: 'y1' });
|
||||||
|
const ax = formModel.createField('arr.0.x');
|
||||||
|
const ay = formModel.createField('arr.0.y');
|
||||||
|
const bx = formModel.createField('arr.1.x');
|
||||||
|
const by = formModel.createField('arr.1.y');
|
||||||
|
|
||||||
|
formModel.init({});
|
||||||
|
|
||||||
|
ax.state.errors = {
|
||||||
|
'arr.0.x': [{ name: 'arr.0.x', message: 'err0x', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
bx.state.errors = {
|
||||||
|
'arr.1.x': [{ name: 'arr.1.x', message: 'err1x', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(formModel.values).toEqual({
|
||||||
|
arr: [
|
||||||
|
{ x: 'x0', y: 'y0' },
|
||||||
|
{ x: 'x1', y: 'y1' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
arrayField.swap(0, 1);
|
||||||
|
expect(formModel.values).toEqual({
|
||||||
|
arr: [
|
||||||
|
{ x: 'x1', y: 'y1' },
|
||||||
|
{ x: 'x0', y: 'y0' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(formModel.getField('arr.0.x').state.errors).toEqual({
|
||||||
|
'arr.0.x': [{ name: 'arr.0.x', message: 'err1x', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
|
expect(formModel.getField('arr.1.x').state.errors).toEqual({
|
||||||
|
'arr.1.x': [{ name: 'arr.1.x', message: 'err0x', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// assert form.state.errors
|
||||||
|
expect(formModel.state.errors['arr.0.x']).toEqual([
|
||||||
|
{ name: 'arr.0.x', message: 'err1x', level: FeedbackLevel.Error },
|
||||||
|
]);
|
||||||
|
expect(formModel.state.errors['arr.1.x']).toEqual([
|
||||||
|
{ name: 'arr.1.x', message: 'err0x', level: FeedbackLevel.Error },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct form.state.errors after swapping invalid field with valid field', () => {
|
||||||
|
const arrayField = formModel.createFieldArray('arr');
|
||||||
|
const a = arrayField!.append('a');
|
||||||
|
const b = arrayField!.append('b');
|
||||||
|
arrayField!.append('c');
|
||||||
|
|
||||||
|
formModel.init({});
|
||||||
|
|
||||||
|
b.state.errors = {
|
||||||
|
'arr.1': [{ name: 'arr.1', message: 'err1', level: FeedbackLevel.Error }],
|
||||||
|
};
|
||||||
|
|
||||||
|
arrayField.swap(0, 1);
|
||||||
|
expect(formModel.getField('arr.0').state.errors).toEqual({
|
||||||
|
'arr.0': [{ name: 'arr.0', message: 'err1', level: FeedbackLevel.Error }],
|
||||||
|
});
|
||||||
|
expect(formModel.getField('arr.1').state.errors).toEqual(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should trigger array effect and child effect', () => {
|
it('should trigger array effect and child effect', () => {
|
||||||
|
|||||||
@ -204,6 +204,7 @@ export class FieldArrayModel<TValue = FieldValue> extends FieldModel<Array<TValu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldFormValues = this.form.values;
|
||||||
const tempValue = [...this.value];
|
const tempValue = [...this.value];
|
||||||
|
|
||||||
const fromValue = tempValue[from];
|
const fromValue = tempValue[from];
|
||||||
@ -212,7 +213,46 @@ export class FieldArrayModel<TValue = FieldValue> extends FieldModel<Array<TValu
|
|||||||
tempValue[to] = fromValue;
|
tempValue[to] = fromValue;
|
||||||
tempValue[from] = toValue;
|
tempValue[from] = toValue;
|
||||||
|
|
||||||
this.form.setValueIn(this.name, tempValue);
|
this.form.store.setIn(this.path, tempValue);
|
||||||
|
this.form.fireOnFormValuesChange({
|
||||||
|
values: this.form.values,
|
||||||
|
prevValues: oldFormValues,
|
||||||
|
name: this.name,
|
||||||
|
options: {
|
||||||
|
action: 'array-swap',
|
||||||
|
indexes: [from, to],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// swap related FieldModels
|
||||||
|
const newFieldMap = new Map<string, FieldModel>(this.form.fieldMap);
|
||||||
|
|
||||||
|
const fromFields = this.findAllFieldsAt(from);
|
||||||
|
const toFields = this.findAllFieldsAt(to);
|
||||||
|
const fromRootPath = this.getPathAt(from);
|
||||||
|
const toRootPath = this.getPathAt(to);
|
||||||
|
const leafFieldsModified: FieldModel[] = [];
|
||||||
|
fromFields.forEach((f) => {
|
||||||
|
const newName = f.path.replaceParent(fromRootPath, toRootPath).toString();
|
||||||
|
f.name = newName;
|
||||||
|
if (!f.children.length) {
|
||||||
|
f.updateNameForLeafState(newName);
|
||||||
|
leafFieldsModified.push(f);
|
||||||
|
}
|
||||||
|
newFieldMap.set(newName, f);
|
||||||
|
});
|
||||||
|
toFields.forEach((f) => {
|
||||||
|
const newName = f.path.replaceParent(toRootPath, fromRootPath).toString();
|
||||||
|
f.name = newName;
|
||||||
|
if (!f.children.length) {
|
||||||
|
f.updateNameForLeafState(newName);
|
||||||
|
}
|
||||||
|
newFieldMap.set(newName, f);
|
||||||
|
leafFieldsModified.push(f);
|
||||||
|
});
|
||||||
|
this.form.fieldMap = newFieldMap;
|
||||||
|
leafFieldsModified.forEach((f) => f.bubbleState());
|
||||||
|
this.form.alignStateWithFieldMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
move(from: number, to: number) {
|
move(from: number, to: number) {
|
||||||
@ -236,5 +276,53 @@ export class FieldArrayModel<TValue = FieldValue> extends FieldModel<Array<TValu
|
|||||||
tempValue.splice(to, 0, fromValue);
|
tempValue.splice(to, 0, fromValue);
|
||||||
|
|
||||||
this.form.setValueIn(this.name, tempValue);
|
this.form.setValueIn(this.name, tempValue);
|
||||||
|
|
||||||
|
// todo(fix): should move fields in order to make sure fields' state is also moved
|
||||||
|
}
|
||||||
|
|
||||||
|
protected insertAt(index: number, value: TValue) {
|
||||||
|
if (!this.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index > this.value.length) {
|
||||||
|
throw new Error(`[Form]: FieldArrayModel.insertAt Error: index exceeds array boundary`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempValue = [...this.value];
|
||||||
|
tempValue.splice(index, 0, value);
|
||||||
|
this.form.setValueIn(this.name, tempValue);
|
||||||
|
|
||||||
|
// todo: should move field in order to make sure field state is also moved
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get element path at given index
|
||||||
|
* @param index
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected getPathAt(index: number) {
|
||||||
|
return this.path.concat(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find all fields including child and grandchild fields at given index.
|
||||||
|
* @param index
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected findAllFieldsAt(index: number) {
|
||||||
|
const rootPath = this.getPathAt(index);
|
||||||
|
const rootPathString = rootPath.toString();
|
||||||
|
|
||||||
|
const res: FieldModel[] = this.form.fieldMap.get(rootPathString)
|
||||||
|
? [this.form.fieldMap.get(rootPathString)!]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
this.form.fieldMap.forEach((field, fieldName) => {
|
||||||
|
if (rootPath.isChildOrGrandChild(fieldName)) {
|
||||||
|
res.push(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { Errors, Feedback, OnFormValuesChangePayload, ValidateTrigger, Warnings
|
|||||||
import { Path } from './path';
|
import { Path } from './path';
|
||||||
|
|
||||||
export function updateFeedbacksName(feedbacks: Feedback<any>[], name: string) {
|
export function updateFeedbacksName(feedbacks: Feedback<any>[], name: string) {
|
||||||
return feedbacks.map((f) => ({
|
return (feedbacks || []).map((f) => ({
|
||||||
...f,
|
...f,
|
||||||
name,
|
name,
|
||||||
}));
|
}));
|
||||||
@ -91,7 +91,7 @@ export namespace FieldEventUtils {
|
|||||||
) {
|
) {
|
||||||
const { name: changedName, options } = payload;
|
const { name: changedName, options } = payload;
|
||||||
|
|
||||||
if (options?.action === 'array-splice') {
|
if (options?.action === 'array-splice' || options?.action === 'array-swap') {
|
||||||
// const splicedIndexes = options?.indexes || [];
|
// const splicedIndexes = options?.indexes || [];
|
||||||
//
|
//
|
||||||
// const splicedPaths = splicedIndexes.map(index => new Path(changedName).concat(index));
|
// const splicedPaths = splicedIndexes.map(index => new Path(changedName).concat(index));
|
||||||
@ -109,7 +109,7 @@ export namespace FieldEventUtils {
|
|||||||
// return false;
|
// return false;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// splice 情况下仅触发数组field的校验
|
// splice 和 swap 都属于数组跟级别的变更,仅需触发数组field的校验, 无需校验子项
|
||||||
return fieldName === changedName;
|
return fieldName === changedName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -108,7 +108,7 @@ export interface CreateFormReturn<TValues> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface OnFormValuesChangeOptions {
|
export interface OnFormValuesChangeOptions {
|
||||||
action?: 'array-append' | 'array-splice';
|
action?: 'array-append' | 'array-splice' | 'array-swap';
|
||||||
indexes?: number[];
|
indexes?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -135,6 +135,10 @@ export class FormModelV2 extends FormModel implements Disposable {
|
|||||||
return this.node.getNodeRegistry().formMeta;
|
return this.node.getNodeRegistry().formMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get values() {
|
||||||
|
return this.nativeFormModel?.values;
|
||||||
|
}
|
||||||
|
|
||||||
protected _feedbacks: FormFeedback[] = [];
|
protected _feedbacks: FormFeedback[] = [];
|
||||||
|
|
||||||
get feedbacks(): FormFeedback[] {
|
get feedbacks(): FormFeedback[] {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user