Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Screen/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react';

export interface ScreenProps {
visible?: boolean;
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}

const Screen: React.FC<ScreenProps> = ({ visible = false, children, className, style }) => {
// Simple fade in/out animation using inline styles
const screenStyle: React.CSSProperties = {
transition: 'opacity 0.3s, transform 0.3s',
opacity: visible ? 1 : 0,
transform: visible ? 'scale(1)' : 'scale(0.8)',
display: visible ? 'block' : 'none',
...style,
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fade-in animation may not work as expected due to the use of display: 'none'. When visible becomes true, the display property changes from none to block and opacity changes to 1 within the same render cycle. The browser doesn't have a 'from' state to transition from, so the element will appear instantly rather than fading in. The fade-out animation will work correctly.

To achieve a proper fade-in animation while still using display: 'none' (presumably to unmount fields), you would typically need a more complex implementation involving useEffect and timeouts to manage rendering and style changes in separate phases, or use a library like react-transition-group. If an instant appearance on 'show' is acceptable, then this implementation is fine.


// Combine className if provided
const combinedClassName = className ? `rc-field-form-screen ${className}` : 'rc-field-form-screen';

return (
<div
className={combinedClassName}
style={screenStyle}
>
{children}
</div>
);
};

export default Screen;
33 changes: 30 additions & 3 deletions src/hooks/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ export class FormStore {
return this.fieldEntities.filter(field => field.getNamePath().length);
};

/**
* Get a map of registered field entities with their name path as the key.
* @param pure Only include fields which have a `name`. Default: false
* @returns A NameMap containing field entities indexed by their name paths
*/
private getFieldsMap = (pure: boolean = false) => {
const cache: NameMap<FieldEntity> = new NameMap();
this.getFieldEntities(pure).forEach(field => {
Expand All @@ -248,14 +253,35 @@ export class FormStore {
return cache;
};

private getFieldEntitiesForNamePathList = (nameList?: NamePath[]): FlexibleFieldEntity[] => {
/**
* Get field entities based on a list of name paths.
* @param nameList - Array of name paths to search for. If not provided, returns all field entities with names.
* @param includesSubNamePath - Whether to include fields that have the given name path as a prefix.
*/
private getFieldEntitiesForNamePathList = (
nameList?: NamePath[],
includesSubNamePath = false,
): FlexibleFieldEntity[] => {
if (!nameList) {
return this.getFieldEntities(true);
}
const cache = this.getFieldsMap(true);
return nameList.map(name => {

if (!includesSubNamePath) {
return nameList.map(name => {
const namePath = getNamePath(name);
return cache.get(namePath) || { INVALIDATE_NAME_PATH: getNamePath(name) };
});
}

return nameList.flatMap(name => {
const namePath = getNamePath(name);
return cache.get(namePath) || { INVALIDATE_NAME_PATH: getNamePath(name) };
const fields: FlexibleFieldEntity[] = cache.getAsPrefix(namePath);

if (fields.length) {
return fields;
}
return [{ INVALIDATE_NAME_PATH: namePath }];
});
};

Expand All @@ -282,6 +308,7 @@ export class FormStore {

const fieldEntities = this.getFieldEntitiesForNamePathList(
Array.isArray(mergedNameList) ? mergedNameList : null,
true,
);

const filteredNameList: NamePath[] = [];
Expand Down
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import type { FormRef, FormInstance } from './interface';
import Field from './Field';
import List from './List';
import Screen from './Screen';
import useForm from './hooks/useForm';
import type { FormProps } from './Form';
import FieldForm from './Form';
Expand All @@ -19,6 +20,7 @@ interface RefFormType extends InternalFormType {
FormProvider: typeof FormProvider;
Field: typeof Field;
List: typeof List;
Screen: typeof Screen;
useForm: typeof useForm;
useWatch: typeof useWatch;
}
Expand All @@ -28,10 +30,11 @@ const RefForm: RefFormType = InternalForm as RefFormType;
RefForm.FormProvider = FormProvider;
RefForm.Field = Field;
RefForm.List = List;
RefForm.Screen = Screen;
RefForm.useForm = useForm;
RefForm.useWatch = useWatch;

export { Field, List, useForm, FormProvider, FieldContext, ListContext, useWatch };
export { Field, List, Screen, useForm, FormProvider, FieldContext, ListContext, useWatch };

export type { FormProps, FormInstance, FormRef };

Expand Down
19 changes: 19 additions & 0 deletions src/utils/NameMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ class NameMap<T> {
return this.kvs.get(normalize(key));
}

public getAsPrefix(key: InternalNamePath): T[] {
const normalizedKey = normalize(key);
const normalizedPrefix = normalizedKey + SPLIT;
const results: T[] = [];

const current = this.kvs.get(normalizedKey);
if (current !== undefined) {
results.push(current);
}

this.kvs.forEach((value, itemNormalizedKey) => {
if (itemNormalizedKey.startsWith(normalizedPrefix)) {
results.push(value);
}
});

return results;
}

public update(key: InternalNamePath, updater: (origin: T) => T | null) {
const origin = this.get(key);
const next = updater(origin);
Expand Down
19 changes: 19 additions & 0 deletions tests/list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1171,4 +1171,23 @@ describe('Form.List', () => {
list: [{ name: 'A' }, { name: 'BB' }, { name: 'C' }, { name: 'D' }],
});
});

it('getFieldsValue(["list"]) should same as getFieldsValue().list', async () => {
generateForm(
fields =>
fields.map(field => (
<Field {...fields} name={[field.name, 'name']} key={field.key}>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There appears to be a typo in this line. You are spreading fields (the array of ListField objects) as props to the Field component, instead of field (the individual ListField object from the map iteration). While the test might still pass because name and key are explicitly provided, spreading the fields array is incorrect and could pass unexpected props to the Field component.

Suggested change
<Field {...fields} name={[field.name, 'name']} key={field.key}>
<Field {...field} name={[field.name, 'name']} key={field.key}>

<Input />
</Field>
)),
{
initialValues: {
list: [{ name: 'bamboo', notExist: 'little' }],
},
},
);

expect(form.current!.getFieldsValue()).toEqual({ list: [{ name: 'bamboo' }] });
expect(form.current!.getFieldsValue(['list'])).toEqual({ list: [{ name: 'bamboo' }] });
});
});