Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
type FieldPropsComponent,
Form,
SubmitButton,
typedField,
useFieldProps,
} from "@mittwald/flow-react-components/react-hook-form";
import {
ActionGroup,
Label,
Section,
} from "@mittwald/flow-react-components";
import { type FC, useMemo } from "react";
import { useForm } from "react-hook-form";

export default () => {
const CustomFieldComponent: FC<FieldPropsComponent> = (
props,
) => {
const {
ref,
children,
value,
defaultValue,
onChange,
onBlur,
form,
name,
disabled,
isReadOnly,
fieldComponents: {
FieldErrorView,
FieldComponentContainer,
FieldChildrenContainer,
},
} = useFieldProps(props);

return (
<FieldComponentContainer>
<FieldChildrenContainer>
{children}
</FieldChildrenContainer>
<input
readOnly={isReadOnly}
disabled={disabled}
form={form}
name={name}
ref={ref}
style={{ order: 2 }}
value={value}
defaultValue={defaultValue}
onChange={onChange}
onBlur={onBlur}
/>
<FieldErrorView />
</FieldComponentContainer>
);
};

// only necessary because CustomFieldComponent is an inline component in this demo page
const MemoCustomFieldComponent = useMemo(
() => CustomFieldComponent,
[],
);

interface Values {
myCustomField: string;
}
const form = useForm<Values>();
const Field = typedField(form);

return (
<Form form={form} onSubmit={console.log}>
<Section>
<Field
name="myCustomField"
rules={{
required: "Bitte gib eine Nachricht ein",
}}
>
<MemoCustomFieldComponent>
<Label>Name</Label>
</MemoCustomFieldComponent>
</Field>
<ActionGroup>
<SubmitButton>Speichern</SubmitButton>
</ActionGroup>
</Section>
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Die Field-Component nutzt dieselben Properties wie die [Controller-Component](https://www.react-hook-form.com/api/usecontroller/controller/) von React Hook Form – mit Ausnahme von `render`.
Die Field-Component nutzt dieselben Properties wie die
[Controller-Component](https://www.react-hook-form.com/api/usecontroller/controller/)
von React Hook Form – mit Ausnahme von `render`.

# Playground

Expand All @@ -8,6 +10,16 @@ Die Field-Component nutzt dieselben Properties wie die [Controller-Component](ht

# typedField

Die `typedField(form)`-Factory erzeugt eine Field-Component, die an den Typ des jeweiligen Forms angepasst ist. Dadurch führen beispielsweise unbekannte Field-Namen zu einem TypeScript-Fehler.
Die `typedField(form)`-Factory erzeugt eine Field-Component, die an den Typ des
jeweiligen Forms angepasst ist. Dadurch führen beispielsweise unbekannte
Field-Namen zu einem TypeScript-Fehler.

<LiveCodeEditor example="typed-field" />

---

# custom field components

asd

<LiveCodeEditor example="custom-field" />
62 changes: 47 additions & 15 deletions apps/remote-dom-demo/src/app/remote/react-hook-form/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,37 @@

import {
ActionGroup,
Autocomplete,
Button,
CopyButton,
Checkbox,
CheckboxGroup,
ComboBox,
CopyButton,
FileField,
Label,
Option,
MarkdownEditor,
Option,
PasswordCreationField,
Section,
Select,
TextArea,
TextField,
PasswordCreationField,
Autocomplete,
CheckboxGroup,
Checkbox,
} from "@mittwald/flow-remote-react-components";
import {
Form,
Field,
SubmitButton,
type FieldPropsComponent,
Form,
ResetButton,
SubmitButton,
useFieldProps,
} from "@mittwald/flow-remote-react-components/react-hook-form";
import {
Policy,
generatePasswordCreationFieldValidation,
Policy,
RuleType,
} from "@mittwald/flow-react-components/mittwald-password-tools-js";
import { useForm } from "react-hook-form";
import type { FC } from "react";

const customPolicy = Policy.fromDeclaration({
minComplexity: 1,
Expand All @@ -42,7 +45,37 @@ const customPolicy = Policy.fromDeclaration({
],
});

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const CustomFieldComponent: FC<FieldPropsComponent> = (props) => {
const {
children,
value,
onBlur,
fieldComponents: {
FieldErrorView,
FieldComponentContainer,
FieldChildrenContainer,
},
} = useFieldProps(props);

return (
<FieldComponentContainer>
<FieldChildrenContainer>
<CheckboxGroup
value={value}
onBlur={onBlur}
onChange={(v) => console.log("change", v)}
>
<Checkbox value="read">Lesen</Checkbox>
<Checkbox value="write">Schreiben</Checkbox>
</CheckboxGroup>
{children}
</FieldChildrenContainer>
<FieldErrorView />
</FieldComponentContainer>
);
};

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export default function Page() {
const form = useForm({
Expand All @@ -59,6 +92,7 @@ export default function Page() {
password: "",
permissions: [],
agreeTerms: false,
email: "asd@asd.de",
},
});

Expand Down Expand Up @@ -149,12 +183,10 @@ export default function Page() {
</Button>
</FileField>
</Field>
<Field name="permissions">
<CheckboxGroup>
<Field name="permissions" rules={{ required: "yes" }}>
<CustomFieldComponent>
<Label>Berechtigungen</Label>
<Checkbox value="read">Lesen</Checkbox>
<Checkbox value="write">Schreiben</Checkbox>
</CheckboxGroup>
</CustomFieldComponent>
</Field>
<Field name="agreeTerms">
<Label>Terms</Label>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { useFormContext } from "@/integrations/react-hook-form/components/FormContextProvider/FormContextProvider";
import { dynamic, type PropsContext } from "@/lib/propsContext";
import { PropsContextProvider } from "@/lib/propsContext";
import {
dynamic,
type PropsContext,
PropsContextProvider,
} from "@/lib/propsContext";
import { type PropsWithChildren } from "react";
import {
type ControllerProps,
type ControllerRenderProps,
type FieldPath,
type FieldValues,
useController,
type UseFormReturn,
Expand All @@ -13,11 +18,29 @@ import { useLocalizedStringFormatter } from "react-aria";
import locales from "./locales/*.locale.json";
import FieldErrorView from "@/views/FieldErrorView";
import { useUpdateFormDefaultValue } from "@/integrations/react-hook-form/components/Field/hooks/useUpdateFormDefaultValue";
import { FieldPropsContext } from "@/integrations/react-hook-form/components/Field/hooks/useFieldProps";

export interface FieldProps<T extends FieldValues, TName extends FieldPath<T>>
extends Omit<ControllerProps<T, TName>, "render">, PropsWithChildren {}

export interface FieldProps<T extends FieldValues>
extends Omit<ControllerProps<T>, "render">, PropsWithChildren {}
export interface ForwardedFieldProps<
T extends FieldValues = FieldValues,
TName extends FieldPath<T> = FieldPath<T>,
> extends Omit<ControllerRenderProps<T, TName>, "name"> {
form?: string;
name: string;
isRequired?: boolean;
defaultValue?: ControllerRenderProps<T, TName>["value"];
isReadOnly?: boolean;
isInvalid?: boolean;
validationBehavior: "aria";
children?: ReturnType<typeof dynamic> | undefined;
}

export function Field<T extends FieldValues>(props: FieldProps<T>) {
export function Field<
T extends FieldValues,
TName extends FieldPath<T> = FieldPath<T>,
>(props: FieldProps<T, TName>) {
const { children, name, defaultValue, ...rest } = props;

const stringFormatter = useLocalizedStringFormatter(locales);
Expand Down Expand Up @@ -89,7 +112,7 @@ export function Field<T extends FieldValues>(props: FieldProps<T>) {
</>
);
}),
};
} satisfies ForwardedFieldProps<T, TName>;

const propsContext: PropsContext = {
Autocomplete: {
Expand Down Expand Up @@ -146,7 +169,7 @@ export function Field<T extends FieldValues>(props: FieldProps<T>) {
formContext.isReadOnly,
]}
>
{children}
<FieldPropsContext value={fieldProps}>{children}</FieldPropsContext>
</PropsContextProvider>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
createContext,
type FC,
type PropsWithChildren,
useContext,
useMemo,
} from "react";
import type { ForwardedFieldProps } from "@/integrations/react-hook-form/components/Field/Field";
import { useFieldComponent } from "@/lib/hooks/useFieldComponent";
import { useControlledHostValueProps } from "@/lib/remote/useControlledHostValueProps";
import { mergeProps } from "@react-aria/utils";
import resolveDynamicProps from "@/lib/propsContext/dynamicProps/resolveDynamicProps";
import type { FieldPath, FieldValues } from "react-hook-form";
import { PropsContextProvider } from "@/lib/propsContext";
import DivView from "@/views/DivView";

export const FieldPropsContext = createContext<
ForwardedFieldProps<never, never>
>({} as never);

export interface FieldPropsComponent<
T extends FieldValues = FieldValues,
TName extends FieldPath<T> = FieldPath<T>,
>
extends
Omit<Partial<ForwardedFieldProps<T, TName>>, "children">,
PropsWithChildren {}

export const useFieldProps = <T = unknown,>(
props: PropsWithChildren<T>,
isTextValueComponent = false,
) => {
const contextProps = useContext(FieldPropsContext);
const controlledProps = useControlledHostValueProps(contextProps);
const finalPropsFromContext = isTextValueComponent
? controlledProps
: contextProps;

const mergedProps = mergeProps(
props,
finalPropsFromContext,
resolveDynamicProps(finalPropsFromContext, props),
) as T & ForwardedFieldProps<never, never>;
const {
FieldErrorView,
FieldErrorCaptureContext,
fieldPropsContext,
fieldProps,
} = useFieldComponent(mergedProps);

const FieldComponentContainer: FC<PropsWithChildren> = useMemo(
() =>
({ children, ...rest }) => (
<DivView {...rest} {...fieldProps}>
{children}
</DivView>
),
[fieldProps],
);

const FieldChildrenContainer: FC<PropsWithChildren> = useMemo(
() =>
({ children }) => (
<PropsContextProvider props={fieldPropsContext}>
<FieldErrorCaptureContext>{children}</FieldErrorCaptureContext>
</PropsContextProvider>
),
[fieldPropsContext],
);

return {
...mergedProps,
fieldComponents: {
FieldComponentContainer,
FieldChildrenContainer,
FieldErrorView,
},
} as const;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { FieldProps } from "@/integrations/react-hook-form/components/Field/Field";
import { useLayoutEffect } from "react";
import type { FieldValues, UseFormReturn } from "react-hook-form";
import type { FieldPath, FieldValues, UseFormReturn } from "react-hook-form";

export const useUpdateFormDefaultValue = <T extends FieldValues>(
fieldProps: FieldProps<T>,
export const useUpdateFormDefaultValue = <
T extends FieldValues,
TName extends FieldPath<T> = FieldPath<T>,
>(
fieldProps: FieldProps<T, TName>,
form: UseFormReturn<T>,
) => {
const { defaultValue, name } = fieldProps;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default } from "@/integrations/react-hook-form/components/Field/Field";

export { Field, type FieldProps, typedField } from "./Field";
export { useFieldProps, type FieldPropsComponent } from "./hooks/useFieldProps";
Loading
Loading