Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Form): optimize field creation #527

Merged
merged 6 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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 { 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 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 @@
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 @@
} 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 All @@ -153,7 +177,7 @@
ValueComponent = ValueComponent ?? TextInputMapperInput;

const onKeyChange = useEvent((id: number, value: string) => {
mappings.find((mapping) => {

Check warning on line 180 in src/components/fields/TextInputMapper/TextInputMapper.tsx

View workflow job for this annotation

GitHub Actions / Tests & lint

Array.prototype.find() expects a return value from arrow function
if (mapping.id === id) {
mapping.key = value;
}
Expand All @@ -163,7 +187,7 @@
});

const onValueChange = useEvent((id: number, value: string) => {
mappings.find((mapping) => {

Check warning on line 190 in src/components/fields/TextInputMapper/TextInputMapper.tsx

View workflow job for this annotation

GitHub Actions / Tests & lint

Array.prototype.find() expects a return value from arrow function
if (mapping.id === id) {
mapping.value = value;
}
Expand All @@ -176,17 +200,26 @@
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 @@
}, [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 @@
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
Loading