Skip to content

Commit

Permalink
refactor: modified DynamicZone, FormDynamicZone, types/utilities, exa…
Browse files Browse the repository at this point in the history
…mple ReactHookFormDynamicZone
  • Loading branch information
Quentin Le Caignec committed Sep 24, 2024
1 parent 92f6952 commit 0442bf4
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 105 deletions.
27 changes: 27 additions & 0 deletions packages/haring-react-shared/src/helpers/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,30 @@ export function isNotNullNorEmpty<S>(
export function isCallback<T, U>(maybeFunc: T | U): maybeFunc is T {
return typeof maybeFunc === 'function';
}

export function isObject(value: unknown): value is object {
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}

export function findNestedObject(
object: object,
keyToMatch: string,
valueToMatch: string,
): object | null {
if (isObject(object)) {
const entries = Object.entries(object);
for (const element of entries) {
const [objectKey, objectValue] = element;
if (objectKey === keyToMatch && objectValue && valueToMatch) {
return object;
}
if (isObject(objectValue)) {
const child = findNestedObject(objectValue, keyToMatch, valueToMatch);
if (child !== null) {
return child;
}
}
}
}
return null;
}
2 changes: 2 additions & 0 deletions packages/haring-react-shared/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export {
createThemes,
isCallback,
isNotNullNorEmpty,
isObject,
findNestedObject,
typeGuard,
typeGuardInterface,
} from './helpers';
Expand Down
12 changes: 10 additions & 2 deletions packages/haring-react/src/Form/DynamicZone/DynamicZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,16 @@ export function DynamicZone(props: IDynamicZoneProps): ReactElement {
{...block.blockCardProps}
key={block.id}
actions={block.blockActions}
footerChildren={block.blockFooter}
headerChildren={block.blockHeader}
footerChildren={
typeof block.blockFooter === 'function'
? block.blockFooter(block, index)
: block.blockFooter
}
headerChildren={
typeof block.blockHeader === 'function'
? block.blockHeader(block, index)
: block.blockHeader
}
internalComponentProps={internalBlockComponentProps}
onToggle={(opened) => onToggleBlock(block, index, opened)}
opened={block.opened}
Expand Down
8 changes: 6 additions & 2 deletions packages/haring-react/src/types/dynamic-zone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ export interface IBaseBlock extends Record<string, unknown> {
export interface IBaseBlockCardOptions {
blockActions?: IAction<IDynamicZoneBlockReference>[];
blockCardProps?: CardProps;
blockFooter?: ReactNode;
blockHeader?: ReactNode;
blockFooter?:
| ReactNode
| ((block: IBaseBlockFull, index: number) => ReactNode);
blockHeader?:
| ReactNode
| ((block: IBaseBlockFull, index: number) => ReactNode);
}

export type IBaseBlockFull<Block extends IBaseBlock = IBaseBlock> = Block &
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactElement } from 'react';

export function CommonFormErrorText(props: { message: string }): ReactElement {
const { message } = props;
return (
<em role="alert" style={{ color: 'red' }}>
Error: {message}
</em>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import type {
SubmitErrorHandler,
SubmitHandler,
} from 'react-hook-form';
import type { UseFormRegister } from 'react-hook-form/dist/types/form';

import { ErrorMessage } from '@hookform/error-message';
import { Box, Group, Stack } from '@mantine/core';
import { Cube, Leaf } from '@phosphor-icons/react';
import { FormDynamicZone } from '@smile/haring-react';
import { useFieldArray, useForm } from 'react-hook-form';

import { withExceptionCapturing } from '../utilities/react-hook-form-utilities';

import { CommonFormErrorText } from './ReactHookFormDynamicZone.mock';

interface IContentA extends IBaseBlock {
value: string;
}
Expand All @@ -39,6 +43,117 @@ const initialBlocks: IDynamicContents[] = [
},
];

const availableBlock: (
register: UseFormRegister<IFields>,
errors: object,
) => IFormDynamicZoneBlock<IDynamicContents>[] = (register, errors) => [
{
block: {
blockType: 'exampleA',
opened: true,
value: '',
},
blockButtonOptions: {
blockType: 'exampleA',
label: 'Example A',
leftSection: <Cube />,
},
blockCardOptions: {
blockFooter: (_b, i) => (
<Stack>
<ErrorMessage
errors={errors}
name={`content.${i}.value.input1`}
render={({ message }: { message: string }) => (
<CommonFormErrorText message={message} />
)}
/>
<ErrorMessage
errors={errors}
name={`content.${i}.value.input2`}
render={({ message }: { message: string }) => (
<CommonFormErrorText message={message} />
)}
/>
</Stack>
),
blockHeader: (
<>
<Cube key="1" />
<span key="2">Example A</span>
</>
),
},
renderFunc: (b: IExampleBlock, i: number): ReactElement => {
return (
<Group>
<input
key={b.id + 1}
{...register(`content.${i}.value.input1`, {
minLength: { message: '3 characters minimum', value: 3 },
required: 'The first field is required',
})}
placeholder="nested field 1"
/>
<input
key={b.id + 2}
{...register(`content.${i}.value.input2`, {
minLength: { message: '3 characters minimum', value: 3 },
required: 'The second field is required',
})}
placeholder="nested field 2"
/>
</Group>
);
},
},
{
block: {
blockType: 'exampleB',
opened: true,
selected: '',
},
blockButtonOptions: {
blockType: 'exampleB',
label: 'Example B',
leftSection: <Leaf />,
},
blockCardOptions: {
blockFooter: (_b, i) => (
<ErrorMessage
errors={errors}
name={`content.${i}.selected`}
render={({ message }: { message: string }) => (
<CommonFormErrorText message={message} />
)}
/>
),
blockHeader: (
<>
<Leaf key="1" />
<span key="2">Example B</span>
</>
),
},
renderFunc: (b: IExampleBlock, i: number): ReactElement => {
return (
<select
key={b.id}
{...register(`content.${i}.selected`, {
required: 'This field is required',
})}
>
<option disabled value="">
-- Please choose an option --
</option>
<option value="selectA">Value A</option>
<option value="selectB">Value B</option>
</select>
);
},
},
];

interface IFields {
content: IDynamicContents[];
email: string;
Expand All @@ -59,8 +174,7 @@ export function ReactHookFormDynamicZone(
handleSubmit,
register,
getValues,
watch,
// formState: { errors },
formState: { errors },
} = useForm<IFields>({
defaultValues: {
content: initialBlocks,
Expand All @@ -81,94 +195,6 @@ export function ReactHookFormDynamicZone(
update(index, { ...updatedBlock, opened });
}

const availableBlocks: IFormDynamicZoneBlock<IDynamicContents>[] = [
{
block: {
blockType: 'exampleA',
opened: true,
value: '',
},
blockButtonOptions: {
blockType: 'exampleA',
label: 'Example A',
leftSection: <Cube />,
},
blockCardOptions: {
blockHeader: (
<>
<Cube key="1" />
<span key="2">Example A</span>
</>
),
},
renderFunc: (b: IExampleBlock, i: number): ReactElement => {
return (
<Group>
<input
key={b.id + 1}
{...register(`content.${i}.value.input1`, {
minLength: 3,
required: 'This field is required',
})}
placeholder="nested field 1"
/>
<input
key={b.id + 2}
{...register(`content.${i}.value.input2`, {
minLength: 3,
required: 'This field is required',
})}
placeholder="nested field 2"
/>
</Group>
);
},
},
{
block: {
blockType: 'exampleB',
opened: true,
selected: '',
},
blockButtonOptions: {
blockType: 'exampleB',
label: 'Example B',
leftSection: <Leaf />,
},
blockCardOptions: {
blockHeader: (
<>
<Leaf key="1" />
<span key="2">Example B</span>
</>
),
},
renderFunc: (b: IExampleBlock, i: number): ReactElement => {
return (
<select
key={b.id}
{...register(`content.${i}.selected`, {
required: 'This field is required',
})}
>
<option disabled value="">
-- Please choose an option --
</option>
<option value="selectA">Value A</option>
<option value="selectB">Value B</option>
</select>
);
},
},
];

console.log('watch', watch('content'));
// TODO: everything important seems to work, now test default values (first in the sense of giving an existing array of blocks with existing values,
// feeding it into the form defaultValues and sending it down into the dynamic zone,
// then maybe some way to give default values on register?,
// then test error display and various complex use cases,
// then maybe test animations

return (
<Box mx="auto">
<form
Expand All @@ -178,24 +204,14 @@ export function ReactHookFormDynamicZone(
>
<Stack>
<FormDynamicZone<IDynamicContents>
availableBlocks={availableBlocks}
availableBlocks={availableBlock(register, errors)}
blocksArray={fields}
onAppendUpdate={(newBlock: IDynamicContents) => append(newBlock)}
onRemoveUpdate={(i: number) => remove(i)}
onSwapUpdate={(i: number, ii: number) => swap(i, ii)}
onToggleUpdate={onToggle}
/>
<input type="submit" />
{/* <ErrorMessage*/}
{/* errors={errors}*/}
{/* name="content.0.input1"*/}
{/* render={({ messages }) =>*/}
{/* messages &&*/}
{/* Object.entries(messages).map(([type, message]) => (*/}
{/* <p key={type}>{message}</p>*/}
{/* ))*/}
{/* }*/}
{/*/ >*/}
</Stack>
</form>
</Box>
Expand Down

0 comments on commit 0442bf4

Please sign in to comment.