Skip to content

Commit

Permalink
fix(Form): optimize field creation (#527)
Browse files Browse the repository at this point in the history
  • Loading branch information
tenphi authored Nov 21, 2024
1 parent 6050a48 commit 3aad044
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-brooms-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cube-dev/ui-kit': patch
---

Add input trimming and keyboard interaction for TextInputMapper.
5 changes: 5 additions & 0 deletions .changeset/lovely-carrots-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cube-dev/ui-kit': patch
---

Add support for all html attributes in basic components.
5 changes: 5 additions & 0 deletions .changeset/quick-badgers-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cube-dev/ui-kit': minor
---

Do not create field instance for non-exist fields in Form. Use default values from Form when creating new fields.
19 changes: 19 additions & 0 deletions src/components/fields/TextInputMapper/TextInputMapper.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ const Template: StoryFn<CubeTextInputMapperProps> = ({ ...props }) => (
);

const FormTemplate: StoryFn<CubeTextInputMapperProps> = ({ ...props }) => (
<Form
defaultValues={{ field: { name: 'value' } }}
labelPosition="top"
onSubmit={(data) => console.log('! onSubmit', data)}
>
<TextInputMapper
name="field"
label="Field Mapper"
{...props}
onChange={(value) => console.log('! onChange', value)}
/>
<Submit>Submit</Submit>
</Form>
);

const FormTemplateSync: StoryFn<CubeTextInputMapperProps> = ({ ...props }) => (
<Form
defaultValues={{ field: { name: 'value' } }}
labelPosition="top"
Expand Down Expand Up @@ -66,3 +82,6 @@ WithValueAndNewMapping.play = async ({ canvasElement }) => {

export const WithinForm = FormTemplate.bind({});
WithinForm.args = {};

export const WithinFormInputSync = FormTemplateSync.bind({});
WithinFormInputSync.args = {};
44 changes: 39 additions & 5 deletions src/components/fields/TextInputMapper/TextInputMapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
ComponentType,
ForwardedRef,
forwardRef,
KeyboardEvent,
useEffect,
useMemo,
useRef,
Expand All @@ -9,7 +11,7 @@ import {

import { useEvent } from '../../../_internal/hooks';
import { FieldBaseProps } from '../../../shared';
import { mergeProps } from '../../../utils/react/index';
import { mergeProps, useCombinedRefs } from '../../../utils/react/index';
import { useFieldProps, useFormProps, wrapWithField } from '../../form';
import { CloseIcon, PlusIcon } from '../../../icons';
import { Button } from '../../actions';
Expand Down Expand Up @@ -63,7 +65,12 @@ function removeDuplicates(mappings: Mapping[]) {
});
}

function TextInputMapper(props: CubeTextInputMapperProps, ref: any) {
function TextInputMapper(
props: CubeTextInputMapperProps,
ref: ForwardedRef<HTMLDivElement>,
) {
ref = useCombinedRefs(ref);

props = useFormProps(props);
props = useFieldProps(props, {
defaultValidationTrigger: 'onChange',
Expand Down Expand Up @@ -122,7 +129,7 @@ function TextInputMapper(props: CubeTextInputMapperProps, ref: any) {
const onMappingsChange = useEvent((newMappings: Mapping[]) => {
const newValue = newMappings.reduce(
(acc, { key, value }) => {
acc[key] = value;
acc[key.trim()] = value.trim();

return acc;
},
Expand All @@ -136,8 +143,25 @@ function TextInputMapper(props: CubeTextInputMapperProps, ref: any) {
} else {
onChange?.(newValue);
}

const updatedMappings = extractLocalValues(newValue ?? {}, newMappings);

if (JSON.stringify(updatedMappings) !== JSON.stringify(mappings)) {
setMappings(updatedMappings);
}
});

// useEffect(() => {
// // focus on the last non-disabled input
// setTimeout(() => {
// (
// ref?.current?.querySelector(
// '[data-qa="Mapping"]:last-child input:not([disabled])',
// ) as HTMLInputElement
// )?.focus();
// }, 100);
// }, [mappings.length]);

const addNewMapping = useEvent(() => {
setMappings((prev) => {
return [...prev, { key: '', value: '', id: counterRef.current++ }];
Expand Down Expand Up @@ -176,17 +200,26 @@ function TextInputMapper(props: CubeTextInputMapperProps, ref: any) {
onMappingsChange(mappings);
});

const onKeyDown = useEvent((e: KeyboardEvent<HTMLDivElement>) => {
// if Ctrl+Enter or Cmd+Enter is pressed then add new mapping if that's enabled
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && showNewButton) {
addNewMapping();
}
});

const renderedMappings = useMemo(() => {
return mappings.map((mapping) => {
return mappings.map((mapping, index) => {
const { key, value, id } = mapping;

return (
<Grid
key={id}
qa="Mapping"
columns="minmax(0, 1fr) minmax(0, 1fr) min-content"
gap="1x"
>
<TextInputMapperInput
autoFocus={index === mappings.length - 1}
id={id}
isDisabled={isDisabled}
type="name"
Expand Down Expand Up @@ -222,7 +255,7 @@ function TextInputMapper(props: CubeTextInputMapperProps, ref: any) {
}, [JSON.stringify(mappings)]);

const element = (
<Flow gap="1x">
<Flow ref={ref} gap="1x" onKeyDown={onKeyDown}>
{[...renderedMappings]}
{showNewButton ? (
<Space gap={0}>
Expand Down Expand Up @@ -251,6 +284,7 @@ export interface CubeTextInputMapperInputProps {
onChange?: (id: number, newValue: string) => void;
onSubmit?: (id: number) => void;
isDisabled?: boolean;
autoFocus?: boolean;
}

function TextInputMapperInput(props: CubeTextInputMapperInputProps) {
Expand Down
25 changes: 13 additions & 12 deletions src/components/form/Form/use-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class CubeFormInstance<
TFormData extends CubeFormData<T> = CubeFormData<T>,
> {
public forceReRender: () => void = () => {};
private initialFields: PartialString<T> = {};
private defaultValues: PartialString<T> = {};
private fields: TFormData = {} as TFormData;
public ref = {};
public isSubmitting = false;
Expand Down Expand Up @@ -70,17 +70,17 @@ export class CubeFormInstance<
newData: PartialString<T>,
touched?: boolean,
skipRender?: boolean,
createFields = false,
inputOnly = false,
) => {
let flag = false;

newData = { ...newData, ...dotize.convert(newData) };

Object.keys(newData).forEach((name: keyof T & string) => {
let field = this.fields[name];

if (!field && createFields) {
this.createField(name, skipRender);
field = this.fields[name];
if (!field) {
return;
}

if (!field || isEqual(field.value, newData[name])) {
Expand Down Expand Up @@ -205,35 +205,35 @@ export class CubeFormInstance<
}

setInitialFieldsValue(values: PartialString<T>): void {
this.initialFields = { ...values, ...dotize.convert(values) };
this.defaultValues = { ...values, ...dotize.convert(values) };
}

updateInitialFieldsValue(values: FieldTypes): void {
this.initialFields = {
...this.initialFields,
this.defaultValues = {
...this.defaultValues,
...values,
...dotize.convert(values),
};
}

resetFields(names?: (keyof T & string)[], skipRender?: boolean): void {
const fieldsValue = this.getFieldsValue();
const fieldNames = Object.keys({ ...fieldsValue, ...this.initialFields });
const fieldNames = Object.keys({ ...fieldsValue, ...this.defaultValues });
const filteredFieldNames = names
? fieldNames.filter((name) => names.includes(name))
: fieldNames;

const values = filteredFieldNames.reduce((map, name) => {
if (name in this.initialFields) {
map[name] = this.initialFields[name];
if (name in this.defaultValues) {
map[name] = this.defaultValues[name];
} else {
map[name] = undefined;
}

return map;
}, {});

this.setFieldsValue(values, false, skipRender, true);
this.setFieldsValue(values, false, skipRender);
}

async validateField<Name extends keyof T & string>(name: Name): Promise<any> {
Expand Down Expand Up @@ -404,6 +404,7 @@ export class CubeFormInstance<
touched: false,
errors: [],
validationId: 0,
value: this.defaultValues[name],
...data,
// it should be impossible to define or override status value
status: data?.errors?.length ? 'invalid' : undefined,
Expand Down
4 changes: 2 additions & 2 deletions src/components/layout/Flex.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { forwardRef } from 'react';

import {
BaseProps,
AllBaseProps,
CONTAINER_STYLES,
ContainerStyleProps,
extractStyles,
Expand All @@ -16,7 +16,7 @@ const FlexElement = tasty({
},
});

export interface CubeFlexProps extends BaseProps, ContainerStyleProps {}
export interface CubeFlexProps extends AllBaseProps, ContainerStyleProps {}

export const Flex = forwardRef(function Flex(props: CubeFlexProps, ref) {
const styles = extractStyles(props, CONTAINER_STYLES);
Expand Down
4 changes: 2 additions & 2 deletions src/components/layout/Flow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { forwardRef } from 'react';

import {
BaseProps,
AllBaseProps,
CONTAINER_STYLES,
ContainerStyleProps,
extractStyles,
Expand All @@ -18,7 +18,7 @@ const FlowElement = tasty({

const STYLE_PROPS = CONTAINER_STYLES;

export interface CubeFlowProps extends BaseProps, ContainerStyleProps {}
export interface CubeFlowProps extends AllBaseProps, ContainerStyleProps {}

export const Flow = forwardRef(function Flow(props: CubeFlowProps, ref) {
const styles = extractStyles(props, STYLE_PROPS);
Expand Down
4 changes: 2 additions & 2 deletions src/components/layout/Grid.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { forwardRef } from 'react';

import {
BaseProps,
AllBaseProps,
CONTAINER_STYLES,
ContainerStyleProps,
extractStyles,
Expand All @@ -18,7 +18,7 @@ const GridElement = tasty({
});

export interface CubeGridProps
extends BaseProps,
extends Omit<AllBaseProps, 'rows'>,
ContainerStyleProps,
ShortGridStyles {}

Expand Down
4 changes: 2 additions & 2 deletions src/components/layout/Space.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { forwardRef } from 'react';

import {
BaseProps,
AllBaseProps,
CONTAINER_STYLES,
ContainerStyleProps,
extractStyles,
Expand All @@ -24,7 +24,7 @@ const SpaceElement = tasty({
},
});

export interface CubeSpaceProps extends BaseProps, ContainerStyleProps {
export interface CubeSpaceProps extends AllBaseProps, ContainerStyleProps {
direction?: 'vertical' | 'horizontal';
}

Expand Down
8 changes: 5 additions & 3 deletions src/tasty/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ export interface BasePropsWithoutChildren
theme?: 'default' | 'danger' | 'special' | (string & {});
}

export interface BaseProps
export interface BaseProps<K extends keyof HTMLElementTagNameMap = 'div'>
extends AriaLabelingProps,
BasePropsWithoutChildren,
Pick<AllHTMLAttributes<HTMLElementTagNameMap['div']>, 'children'> {}
Pick<AllHTMLAttributes<HTMLElementTagNameMap[K]>, 'children'> {}

export interface AllBaseProps<K extends keyof HTMLElementTagNameMap = 'div'>
extends BaseProps,
Expand All @@ -97,8 +97,10 @@ export interface AllBaseProps<K extends keyof HTMLElementTagNameMap = 'div'>
| 'color'
| 'height'
| 'width'
| 'content'
| 'translate'
> {
as?: string;
as?: K;
}

export type BaseStyleProps = Pick<Styles, (typeof BASE_STYLES)[number]>;
Expand Down

0 comments on commit 3aad044

Please sign in to comment.