Skip to content

Commit

Permalink
refactor: form metadata type
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung committed Nov 13, 2023
1 parent ee779f0 commit 13622e2
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 262 deletions.
6 changes: 3 additions & 3 deletions examples/remix/app/routes/todos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ export default function TodoForm() {
const lastResult = useActionData<typeof action>();
const { form, fields, context } = useForm({
lastResult,
// onValidate({ formData }) {
// return parse(formData, { schema: todosSchema });
// },
onValidate({ formData }) {
return parse(formData, { schema: todosSchema });
},
shouldValidate: 'onBlur',
});
const taskList = useFieldList({
Expand Down
77 changes: 59 additions & 18 deletions packages/conform-dom/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type UnionKeyType<T, K extends UnionKeyof<T>> = T extends {
? T[K]
: undefined;

export type DefaultValue<Schema> = Schema extends
export type FormValue<Schema> = Schema extends
| string
| number
| boolean
Expand All @@ -43,12 +43,12 @@ export type DefaultValue<Schema> = Schema extends
: Schema extends File
? undefined
: Schema extends Array<infer InnerType>
? Array<DefaultValue<InnerType>>
? Array<FormValue<InnerType>>
: Schema extends Record<string, any>
? { [Key in UnionKeyof<Schema>]?: DefaultValue<UnionKeyType<Schema, Key>> }
? { [Key in UnionKeyof<Schema>]?: FormValue<UnionKeyType<Schema, Key>> }
: any;

export type FieldName<Type> = string & { __type?: Type };
export type FieldName<Schema> = string & { __type?: Schema };

export type Constraint = {
required?: boolean;
Expand Down Expand Up @@ -77,17 +77,46 @@ export type FormContext = {
state: FormState;
};

export type FormOptions<Type> = {
defaultValue?: DefaultValue<Type>;
export type FormOptions<Schema> = {
/**
* An object representing the initial value of the form.
*/
defaultValue?: FormValue<Schema>;

/**
* An object describing the constraint of each field
*/
constraint?: Record<string, Constraint>;

/**
* An object describing the result of the last submission
*/
lastResult?: SubmissionResult;

/**
* Define when conform should start validation.
* Support "onSubmit", "onInput", "onBlur".
*
* @default "onSubmit"
*/
shouldValidate?: 'onSubmit' | 'onBlur' | 'onInput';

/**
* Define when conform should revalidate again.
* Support "onSubmit", "onInput", "onBlur".
*
* @default Same as shouldValidate, or "onSubmit" if shouldValidate is not provided.
*/
shouldRevalidate?: 'onSubmit' | 'onBlur' | 'onInput';

/**
* A function to be called when the form should be (re)validated.
*/
onValidate?: (context: {
form: HTMLFormElement;
submitter: HTMLInputElement | HTMLButtonElement | null;
formData: FormData;
}) => Submission<Type>;
}) => Submission<Schema>;
};

export type SubscriptionSubject = {
Expand All @@ -106,20 +135,21 @@ export type SubscriptionScope = {
name?: string[];
};

export type Form<Type extends Record<string, unknown> = any> = {
export type Form<Schema extends Record<string, any> = any> = {
id: string;
submit(event: SubmitEvent): {
formData: FormData;
action: ReturnType<typeof getFormAction>;
encType: ReturnType<typeof getFormEncType>;
method: ReturnType<typeof getFormMethod>;
submission?: Submission<Type>;
submission?: Submission<Schema>;
};
reset(event: Event): void;
input(event: Event): void;
blur(event: Event): void;
initialize(): void;
report(result: SubmissionResult): void;
update(options: Omit<FormOptions<Type>, 'lastResult'>): void;
update(options: Omit<FormOptions<Schema>, 'lastResult'>): void;
subscribe(
callback: () => void,
getSubject?: () => SubscriptionSubject | undefined,
Expand All @@ -132,10 +162,10 @@ export const VALIDATION_UNDEFINED = '__undefined__';

export const VALIDATION_SKIPPED = '__skipped__';

export function createForm<Type extends Record<string, unknown> = any>(
export function createForm<Schema extends Record<string, any> = any>(
formId: string,
options: FormOptions<Type>,
): Form<Type> {
options: FormOptions<Schema>,
): Form<Schema> {
let subscribers: Array<{
callback: () => void;
getSubject?: () => SubscriptionSubject | undefined;
Expand Down Expand Up @@ -254,10 +284,10 @@ export function createForm<Type extends Record<string, unknown> = any>(
);
}

function shouldNotify<Type>(config: {
prev: Record<string, Type>;
next: Record<string, Type>;
compareFn: (prev: Type | undefined, next: Type | undefined) => boolean;
function shouldNotify<Schema>(config: {
prev: Record<string, Schema>;
next: Record<string, Schema>;
compareFn: (prev: Schema | undefined, next: Schema | undefined) => boolean;
cache: Record<string, boolean>;
scope?: SubscriptionScope;
}): boolean {
Expand Down Expand Up @@ -529,6 +559,16 @@ export function createForm<Type extends Record<string, unknown> = any>(
requestIntent(element.form, validate.serialize(element.name));
}

function initialize() {
document.addEventListener('input', input);
document.addEventListener('focusout', blur);

return () => {
document.removeEventListener('input', input);
document.removeEventListener('focusout', blur);
};
}

function reset(event: Event) {
const element = getFormElement();

Expand Down Expand Up @@ -642,7 +682,7 @@ export function createForm<Type extends Record<string, unknown> = any>(
}
}

function update(options: Omit<FormOptions<Type>, 'lastResult'>) {
function update(options: Omit<FormOptions<Schema>, 'lastResult'>) {
latestOptions = options;
}

Expand Down Expand Up @@ -672,6 +712,7 @@ export function createForm<Type extends Record<string, unknown> = any>(
reset,
input,
blur,
initialize,
report,
update,
subscribe,
Expand Down
3 changes: 2 additions & 1 deletion packages/conform-dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ export {
type Constraint,
type FormState,
type FieldName,
type DefaultValue,
type FormValue,
type FormContext,
type FormOptions,
type Form,
type SubscriptionSubject,
type SubscriptionScope,
Expand Down
10 changes: 5 additions & 5 deletions packages/conform-dom/submission.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type DefaultValue } from './form';
import { type FormValue } from './form';
import { requestSubmit } from './dom';
import { simplify, flatten, isPlainObject, setValue } from './formdata';
import { invariant } from './util';
Expand Down Expand Up @@ -411,12 +411,12 @@ export const list = createIntent<ListIntentPayload, void>({

export type ListIntentPayload<Schema = unknown> =
| { name: string; operation: 'insert'; defaultValue?: Schema; index?: number }
| { name: string; operation: 'prepend'; defaultValue?: DefaultValue<Schema> }
| { name: string; operation: 'append'; defaultValue?: DefaultValue<Schema> }
| { name: string; operation: 'prepend'; defaultValue?: FormValue<Schema> }
| { name: string; operation: 'append'; defaultValue?: FormValue<Schema> }
| {
name: string;
operation: 'replace';
defaultValue: DefaultValue<Schema>;
defaultValue: FormValue<Schema>;
index: number;
}
| { name: string; operation: 'remove'; index: number }
Expand All @@ -437,7 +437,7 @@ export function requestIntent(
}

export function updateList<Schema>(
list: Array<DefaultValue<Schema>>,
list: Array<FormValue<Schema>>,
payload: ListIntentPayload<Schema>,
): void {
switch (payload.operation) {
Expand Down
57 changes: 31 additions & 26 deletions packages/conform-react/context.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
type Constraint,
type DefaultValue,
type FieldName,
type Form,
type FormValue,
type FormContext,
type SubscriptionScope,
type SubscriptionSubject,
Expand All @@ -25,37 +25,44 @@ import {

export type Pretty<T> = { [K in keyof T]: T[K] } & {};

export type BaseMetadata<Type> = {
export type BaseMetadata<Schema> = {
key?: string;
id: string;
errorId: string;
descriptionId: string;
initialValue: DefaultValue<Type>;
value: DefaultValue<Type>;
initialValue: FormValue<Schema>;
value: FormValue<Schema>;
errors: string[] | undefined;
allErrors: Record<string, string[]>;
allValid: boolean;
valid: boolean;
dirty: boolean;
};

export type Field<Type> = {
name: FieldName<Type>;
export type Field<Schema> = {
name: FieldName<Schema>;
formId: string;
};

export type FormMetadata<Type extends Record<string, any>> = BaseMetadata<Type>;
export type FormMetadata<Schema extends Record<string, any>> =
BaseMetadata<Schema> & {
onSubmit: (
event: React.FormEvent<HTMLFormElement>,
) => ReturnType<Form<Schema>['submit']>;
onReset: (event: React.FormEvent<HTMLFormElement>) => void;
noValidate: boolean;
};

export type FieldMetadata<Type> = BaseMetadata<Type> & {
export type FieldMetadata<Schema> = BaseMetadata<Schema> & {
formId: string;
name: FieldName<Type>;
name: FieldName<Schema>;
constraint?: Constraint;
};

export const Context = createContext<Record<string, Form>>({});
export const Registry = createContext<Record<string, Form>>({});

export function useContextForm(formId: string, context?: Form) {
const registry = useContext(Context);
export function useFormStore(formId: string, context?: Form) {
const registry = useContext(Registry);
const form = context ?? registry[formId];

if (!form) {
Expand All @@ -66,11 +73,9 @@ export function useContextForm(formId: string, context?: Form) {
}

export function useFormContext(
formId: string,
context?: Form,
form: Form,
subjectRef?: MutableRefObject<SubscriptionSubject>,
): FormContext {
const form = useContextForm(formId, context);
const subscribe = useCallback(
(callback: () => void) =>
form.subscribe(callback, () => subjectRef?.current),
Expand All @@ -89,13 +94,13 @@ export function FormProvider(props: {
context: Form;
children: ReactNode;
}): ReactElement {
const context = useContext(Context);
const registry = useContext(Registry);
const value = useMemo(
() => ({ ...context, [props.context.id]: props.context }),
[context, props.context],
() => ({ ...registry, [props.context.id]: props.context }),
[registry, props.context],
);

return <Context.Provider value={value}>{props.children}</Context.Provider>;
return <Registry.Provider value={value}>{props.children}</Registry.Provider>;
}

export function FormStateInput(
Expand All @@ -109,7 +114,7 @@ export function FormStateInput(
context: Form;
},
): React.ReactElement {
const form = useContextForm(props.formId ?? props.context.id, props.context);
const form = useFormStore(props.formId ?? props.context.id, props.context);

return (
<input
Expand All @@ -133,14 +138,14 @@ export function useSubjectRef(
return subjectRef;
}

export function getBaseMetadata<Type>(
export function getBaseMetadata<Schema>(
formId: string,
context: FormContext,
options: {
name?: string;
subjectRef: MutableRefObject<SubscriptionSubject>;
},
): BaseMetadata<Type> {
): BaseMetadata<Schema> {
const name = options.name ?? '';
const id = name ? `${formId}-${name}` : formId;
const updateSubject = (
Expand All @@ -160,8 +165,8 @@ export function getBaseMetadata<Type>(
id,
errorId: `${id}-error`,
descriptionId: `${id}-description`,
initialValue: context.initialValue[name] as DefaultValue<Type>,
value: context.value[name] as DefaultValue<Type>,
initialValue: context.initialValue[name] as FormValue<Schema>,
value: context.value[name] as FormValue<Schema>,
errors: context.error[name],
get key() {
return context.state.key[name];
Expand Down Expand Up @@ -228,15 +233,15 @@ export function getBaseMetadata<Type>(
);
}

export function getFieldMetadata<Type>(
export function getFieldMetadata<Schema>(
formId: string,
context: FormContext,
options: {
name?: string;
key?: string | number;
subjectRef: MutableRefObject<SubscriptionSubject>;
},
): FieldMetadata<Type> {
): FieldMetadata<Schema> {
const name =
typeof options.key !== 'undefined'
? formatPaths([...getPaths(options.name ?? ''), options.key])
Expand Down
Loading

0 comments on commit 13622e2

Please sign in to comment.