Skip to content

Commit

Permalink
feat: wip FormDynamicZone
Browse files Browse the repository at this point in the history
  • Loading branch information
MorganeLeCaignec committed Sep 4, 2024
1 parent 5188d00 commit 0ef57a7
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type {
IFormDynBlock,
IFormField,
IFormRegisterFunc,
} from '../../types/form-dynamic-zone';
import type { IFormDynBlock, IFormField, IFormRegisterFunc } from '../../types';
import type { ReactElement } from 'react';

import { Alien, Cube, Leaf, TreasureChest } from '@phosphor-icons/react';
Expand All @@ -23,6 +19,7 @@ export const dynamicBlocksMock: IFormDynBlock[] = [
</>
),
blockType: 'default',
opened: true,
value: 'initial',
},
button: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ function render() {
const methods = useForm();
return (
<FormProvider {...methods}>
<Cmp {...props} />
<div style={{ margin: '0 200px' }}>
<Cmp {...props} />
</div>
</FormProvider>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,52 @@
import type { IFormDynBlock, IFormField } from '../../types/form-dynamic-zone';
import type {
IFormDynBlock,
IFormDynSubmit,
IFormField,
IFormFieldWithoutId,
} from '../../types';
import type { IBaseBlockButton, IBaseBlockType } from '@smile/haring-react';
import type { IDynamicZoneBlockReference } from '@smile/haring-react/src/Form/DynamicZone/DynamicZoneBlock/DynamicZoneBlock';
import type { IAction } from '@smile/haring-react-shared';
import type { ReactElement } from 'react';

import { Flex } from '@mantine/core';
import { Stack } from '@mantine/core';
import { ArrowDown, ArrowUp, Trash } from '@phosphor-icons/react';
import { DynamicZone } from '@smile/haring-react';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import { useFieldArray, useFormContext } from 'react-hook-form';

interface IFormDynamicZoneActionLabels {
deleteLabel: string;
moveDownLabel: string;
moveUpLabel: string;
}

const defaultActionLabels: IFormDynamicZoneActionLabels = {
deleteLabel: 'Delete',
moveDownLabel: 'Move Down',
moveUpLabel: 'Move Up',
};

export interface IFormDynamicZoneProps {
actionLabels?: IFormDynamicZoneActionLabels;
dynamicBlocks: IFormDynBlock[];
dynamicZoneName: string;
}

export function FormDynamicZone(props: IFormDynamicZoneProps): ReactElement {
const { dynamicBlocks, dynamicZoneName } = props;
const {
dynamicBlocks,
dynamicZoneName,
actionLabels = defaultActionLabels,
} = props;

const { control, register, handleSubmit } =
useFormContext<Record<typeof dynamicZoneName, Omit<IFormField, 'id'>[]>>();
useFormContext<Record<typeof dynamicZoneName, IFormFieldWithoutId[]>>();
const { fields, append, remove, swap, update } = useFieldArray({
control,
name: dynamicZoneName,
});
const watched = useWatch({ control, name: dynamicZoneName });

const blockOptions: IBaseBlockButton[] = dynamicBlocks.map((b) => b.button);

function onAppend(type: IBaseBlockType): void {
const correspondingType = dynamicBlocks.find(
(b) => b.block.blockType === type,
);
if (correspondingType === undefined) {
throw Error(
`Could not find an IFormDynBlock of blocktype '${type} in given dynamicBlocks'`,
);
}
append({
...correspondingType.block,
});
}

function renderBlock(block: IFormField, index: number): ReactElement {
const correspondingType = dynamicBlocks.find(
(b) => b.block.blockType === block.blockType,
Expand All @@ -55,39 +64,91 @@ export function FormDynamicZone(props: IFormDynamicZoneProps): ReactElement {
);
}

function onRemove(ref: IDynamicZoneBlockReference): void {
remove(ref.index);
}

function onSwap(
ref: IDynamicZoneBlockReference,
direction: 'down' | 'up',
): void {
const secondIndex = ref.index + (direction === 'up' ? -1 : 1);
if (secondIndex >= 0 && secondIndex < ref.arrayLength) {
swap(ref.index, ref.index + (direction === 'up' ? -1 : 1));
}
}

function onToggle(block: IFormField, index: number, opened: boolean): void {
update(index, { ...block, opened });
const { id, ...blockWithoutId } = block;
update(index, { ...blockWithoutId, opened });
}

function isMoveDisabled(
ref: IDynamicZoneBlockReference,
direction: 'down' | 'up',
): boolean {
return direction === 'up'
? ref.index === 0
: ref.index === ref.arrayLength - 1;
}

const formDynamicZoneDefaultActions: IAction<IDynamicZoneBlockReference>[] = [
{
componentProps: (ref) => ({ disabled: isMoveDisabled(ref, 'up') }),
icon: <ArrowUp size={16} />,
id: 'move-up',
label: actionLabels.moveUpLabel,
onAction: (ref) => onSwap(ref, 'up'),
},
{
componentProps: (ref) => ({ disabled: isMoveDisabled(ref, 'down') }),
icon: <ArrowDown size={16} />,
id: 'move-down',
label: actionLabels.moveDownLabel,
onAction: (ref) => onSwap(ref, 'down'),
},
{
icon: <Trash size={16} />,
id: 'delete',
label: actionLabels.deleteLabel,
onAction: onRemove,
},
];

function onAppend(type: IBaseBlockType): void {
const correspondingType = dynamicBlocks.find(
(b) => b.block.blockType === type,
);
if (correspondingType === undefined) {
throw Error(
`Could not find an IFormDynBlock of blocktype '${type} in given dynamicBlocks'`,
);
}
append({
...correspondingType.block,
blockActions: formDynamicZoneDefaultActions,
});
}

function onSubmit(data: unknown): void {
function onSubmit(data: IFormDynSubmit): void {
console.log(data);

Check warning on line 134 in packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.tsx

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement

Check warning on line 134 in packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.tsx

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
}

return (
<div>
<Flex>
<button onClick={() => remove(0)} type="button">
remove first
</button>
<button
onClick={() => swap(fields.length - 1, fields.length - 2)}
type="button"
>
swap last
</button>
</Flex>
{/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
<form onSubmit={handleSubmit(onSubmit)}>
form container, {dynamicZoneName}: {JSON.stringify(watched, null, 2)}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
<form onSubmit={handleSubmit(onSubmit)}>
<Stack>
<DynamicZone<IFormField>
blockOptions={blockOptions}
blocks={fields as IFormField[]}
blocks={fields}
fluid
m={0}
onAppendBlock={onAppend}
onRenderBlockContent={renderBlock}
onToggleBlock={onToggle}
/>
<input type="submit" />
</form>
</div>
<input style={{ margin: 'auto' }} type="submit" />
</Stack>
</form>
);
}
19 changes: 14 additions & 5 deletions packages/haring-react-hook-form/src/types/form-dynamic-zone.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import type { IBaseBlock, IBaseBlockButton } from '@smile/haring-react';
import type { IOmitRespectIndexSignature } from '@smile/haring-react-shared';
import type { ReactElement } from 'react';
import type { UseFormRegister } from 'react-hook-form/dist/types/form';

export interface IFormField extends IBaseBlock {
// fieldName: string;
// fieldId: string;
value?: string;
}

export type IFormRegisterFunc<T extends IFormField = IFormField> =
UseFormRegister<Record<string, Omit<T, 'id'>[]>>;
export interface IFormFieldWithoutId
extends IOmitRespectIndexSignature<IFormField, 'id'> {
id?: never; // forbid property "id" from being declared, to never override the internal IBaseBlock.id which is only used by useFieldArray()
}

export type IFormRegisterFunc<
T extends IFormFieldWithoutId = IFormFieldWithoutId,
> = UseFormRegister<Record<string, T[]>>;

export interface IFormDynBlock {
block: Omit<IFormField, 'id'>;
block: IFormFieldWithoutId;
button: IBaseBlockButton;
renderFunc: (
block: IBaseBlock,
block: IFormField,
index: number,
registerFunc: IFormRegisterFunc,
registerName: string,
) => ReactElement;
}

export type IFormDynSubmit = Record<string, IFormFieldWithoutId[]>;
1 change: 1 addition & 0 deletions packages/haring-react-hook-form/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './form-dynamic-zone';
1 change: 1 addition & 0 deletions packages/haring-react-shared/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type {
IThemeOverride,
IThemes,
IFilter,
IOmitRespectIndexSignature,
} from './types';
// type exports
export { mainTheme, primaryTheme, secondaryTheme, themes } from './theme';
1 change: 1 addition & 0 deletions packages/haring-react-shared/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './options';
export * from './theme';
export * from './filters';
export * from './items';
export * from './utility';
3 changes: 3 additions & 0 deletions packages/haring-react-shared/src/types/utility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type IOmitRespectIndexSignature<T, K extends PropertyKey> = {
[P in keyof T as Exclude<P, K>]: T[P];
};

0 comments on commit 0ef57a7

Please sign in to comment.