Skip to content

Commit e46b93a

Browse files
committed
Rewrite FormState typings
1 parent edc70d7 commit e46b93a

File tree

12 files changed

+120
-78
lines changed

12 files changed

+120
-78
lines changed

example/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ function TodoItem(props: {
406406
/**
407407
* Shows a JSON representation of a form
408408
*/
409-
function FormValues<T>(props: { form: FormState<T> }) {
409+
function FormValues<T extends object>(props: { form: FormState<T> }) {
410410
const form = useAnyListener(props.form);
411411
const [show, setShow] = useState({ values: true, defaultValues: false, errorMap: true, dirtyMap: true, state: false });
412412
return (

example/src/CustomInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { VisualRender } from "./VisualRender";
55
/**
66
* A custom input that can be reused everywhere when using useForm
77
*/
8-
export function CustomInput<T>({
8+
export function CustomInput<T extends object>({
99
form,
1010
name,
1111
...rest

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"@types/react-dom": "^16.9.7",
3737
"cross-env": "^7.0.2",
3838
"gh-pages": "^2.2.0",
39-
"microbundle-crl": "^0.13.10",
39+
"microbundle-crl": "^0.13.11",
4040
"prettier": "^2.0.4",
4141
"react": "17.0.0",
4242
"react-dom": "17.0.0",
4343
"react-scripts": "^3.4.1",
44-
"typescript": "^3.7.5"
44+
"typescript": "^4.2.3"
4545
},
4646
"files": [
4747
"dist"

src/Components.tsx

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { ChildFormState, DefaultError, DefaultState, DirtyMap, ErrorMap, FormState } from "./form";
2+
import { ChildFormState, DefaultError, DefaultState, DirtyMap, ErrorMap, FieldsOfType, FormState, KeysOfType } from "./form";
33
import { useArrayForm, useListener, useAnyListener, useChildForm, useTruthyListener } from "./hooks";
44

55
/**
@@ -9,18 +9,23 @@ import { useArrayForm, useListener, useAnyListener, useChildForm, useTruthyListe
99
* @param parent The parent form.
1010
* @param name The parent's field to create a child form for.
1111
*/
12-
export function ArrayForm<Parent, Key extends keyof Parent, ParentState = DefaultState, ParentError extends string = DefaultError>(props: {
13-
form: FormState<Parent, ParentState, ParentError>;
14-
name: Key;
12+
export function ArrayForm<
13+
T extends FieldsOfType<any, any[]>,
14+
K extends KeysOfType<T, any[] | object>,
15+
State = DefaultState,
16+
Error extends string = DefaultError
17+
>(props: {
18+
form: FormState<T, State, Error>;
19+
name: K;
1520
render?: (props: {
16-
form: ChildFormState<Parent, Key, ParentState, ParentError>;
21+
form: ChildFormState<T, K, State, Error>;
1722
remove: (index: number) => void;
1823
clear: () => void;
1924
move: (index: number, newIndex: number) => void;
2025
swap: (index: number, newIndex: number) => void;
21-
append: (value: NonNullable<Parent[Key]>[any]) => void;
22-
values: NonNullable<Parent[Key]>;
23-
setValues: (values: NonNullable<Parent[Key]>) => void;
26+
append: (value: NonNullable<T[K]>[any]) => void;
27+
values: NonNullable<T[K]>;
28+
setValues: (values: NonNullable<T[K]>) => void;
2429
}) => React.ReactNode;
2530
}) {
2631
const childForm = useArrayForm(props.form, props.name);
@@ -40,15 +45,15 @@ export function ArrayForm<Parent, Key extends keyof Parent, ParentState = Defaul
4045
* @param form The form to listen on.
4146
* @param name The form's field to listen to.
4247
*/
43-
export function Listener<T, Key extends keyof T, State = DefaultState, Error extends string = DefaultError>(props: {
48+
export function Listener<T extends object, K extends keyof T, State = DefaultState, Error extends string = DefaultError>(props: {
4449
form: FormState<T, State, Error>;
45-
name: Key;
50+
name: K;
4651
render?: (props: {
47-
value: T[Key];
48-
defaultValue: T[Key];
49-
setValue: (value: T[Key]) => void;
50-
dirty: DirtyMap<T>[Key];
51-
error: ErrorMap<T, Error>[Key];
52+
value: T[K];
53+
defaultValue: T[K];
54+
setValue: (value: T[K]) => void;
55+
dirty: DirtyMap<T>[K];
56+
error: ErrorMap<T, Error>[K];
5257
state: State;
5358
form: FormState<T, State, Error>;
5459
}) => React.ReactNode;
@@ -63,7 +68,7 @@ export function Listener<T, Key extends keyof T, State = DefaultState, Error ext
6368
* You shouldn't use this hook in large components, as it rerenders each time something changes. Use the wrapper <AnyListener /> instead.
6469
* @param form The form that was passed in.
6570
*/
66-
export function AnyListener<T, State = DefaultState, Error extends string = DefaultError>(props: {
71+
export function AnyListener<T extends object, State = DefaultState, Error extends string = DefaultError>(props: {
6772
form: FormState<T, State, Error>;
6873
render?: (props: FormState<T, State, Error>) => React.ReactNode;
6974
}) {
@@ -78,10 +83,15 @@ export function AnyListener<T, State = DefaultState, Error extends string = Defa
7883
* @param parentForm The parent form.
7984
* @param name The parent's field to create a child form for.
8085
*/
81-
export function ChildForm<Parent, Key extends keyof Parent, ParentState = DefaultState, ParentError extends string = DefaultError>(props: {
82-
form: FormState<Parent, ParentState, ParentError>; // Use the parent prop instead of the form prop when using ChildForm.
83-
name: Key;
84-
render?: (props: ChildFormState<Parent, Key, ParentState, ParentError>) => React.ReactNode;
86+
export function ChildForm<
87+
T extends FieldsOfType<any, object>,
88+
K extends KeysOfType<T, object>,
89+
ParentState = DefaultState,
90+
ParentError extends string = DefaultError
91+
>(props: {
92+
form: FormState<T, ParentState, ParentError>; // Use the parent prop instead of the form prop when using ChildForm.
93+
name: K;
94+
render?: (props: ChildFormState<T, K, ParentState, ParentError>) => React.ReactNode;
8595
}) {
8696
const childForm = useChildForm(props.form, props.name);
8797
// Causes a rerender when the object value becomes null/undefined

src/elements/FormError.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import React, { HTMLAttributes } from "react";
22
import { DefaultError, FormState } from "../form";
33
import { useListener } from "../hooks";
44

5-
export type FormErrorProps<T, Key extends keyof T, Error extends string = DefaultError> = Omit<
5+
export type FormErrorProps<T extends object, K extends keyof T, Error extends string = DefaultError> = Omit<
66
HTMLAttributes<HTMLParagraphElement>,
77
"name" | "form"
88
> & {
99
form: FormState<T, any, Error>;
10-
name: Key;
10+
name: K;
1111
};
1212

1313
/**
@@ -17,7 +17,11 @@ export type FormErrorProps<T, Key extends keyof T, Error extends string = Defaul
1717
*
1818
* When this error component is too basic for your needs, you can always [create your own](https://github.com/CodeStix/typed-react-form/wiki/FormError#custom-error-component).
1919
*/
20-
export function FormError<T, Key extends keyof T, Error extends string = DefaultError>({ form, name, ...rest }: FormErrorProps<T, Key, Error>) {
20+
export function FormError<T extends object, K extends keyof T, Error extends string = DefaultError>({
21+
form,
22+
name,
23+
...rest
24+
}: FormErrorProps<T, K, Error>) {
2125
const { error } = useListener(form, name);
2226
if (!error || typeof error === "object") return null;
2327
return <p {...rest}>{error + ""}</p>;

src/elements/FormInput.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,15 @@ export type FormInputType =
3232
| "tel"
3333
| "range";
3434

35-
export type FormInputProps<T, State, Error extends string, Key extends keyof T, Value extends T[Key] | T[Key][keyof T[Key]]> = BaldInputProps & {
35+
export type FormInputProps<
36+
T extends object,
37+
State,
38+
Error extends string,
39+
K extends keyof T,
40+
Value extends T[K] | T[K][keyof T[K]]
41+
> = BaldInputProps & {
3642
form: FormState<T, State, Error>;
37-
name: Key;
43+
name: K;
3844
type?: FormInputType;
3945
value?: Value;
4046
errorClassName?: string;
@@ -56,9 +62,9 @@ export type FormInputProps<T, State, Error extends string, Key extends keyof T,
5662
* When this component does not satisfy your needs, you can always [create your own](https://github.com/CodeStix/typed-react-form/wiki/Custom-inputs#example-custom-input).
5763
*/
5864
export function FormInput<
59-
T,
60-
Key extends keyof T,
61-
Value extends T[Key] | T[Key][keyof T[Key]],
65+
T extends object,
66+
K extends keyof T,
67+
Value extends T[K] | T[K][keyof T[K]],
6268
State extends DefaultState = DefaultState,
6369
Error extends string = DefaultError
6470
>({
@@ -78,7 +84,7 @@ export function FormInput<
7884
value: inputValue,
7985
checked: inputChecked,
8086
...rest
81-
}: FormInputProps<T, State, Error, Key, Value>) {
87+
}: FormInputProps<T, State, Error, K, Value>) {
8288
const { value: currentValue, error, dirty, state, setValue, defaultValue } = useListener(form, name);
8389

8490
let [inValue, inChecked] = useMemo(() => {

src/elements/FormSelect.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DefaultError, DefaultState, FormState } from "../form";
33
import { DEFAULT_DIRTY_CLASS, DEFAULT_ERROR_CLASS, getClassName } from "./FormInput";
44
import { useListener } from "../hooks";
55

6-
export type FormSelectProps<T, State, Error extends string> = Omit<SelectHTMLAttributes<HTMLSelectElement>, "form" | "name"> & {
6+
export type FormSelectProps<T extends object, State, Error extends string> = Omit<SelectHTMLAttributes<HTMLSelectElement>, "form" | "name"> & {
77
form: FormState<T, State, Error>;
88
name: keyof T;
99
errorClassName?: string;
@@ -21,7 +21,7 @@ export type FormSelectProps<T, State, Error extends string> = Omit<SelectHTMLAtt
2121
*
2222
* When this component does not satisfy your needs, you can always [create your own](https://github.com/CodeStix/typed-react-form/wiki/Custom-inputs#example-custom-input).
2323
*/
24-
export function FormSelect<T, State extends DefaultState = DefaultState, Error extends string = DefaultError>({
24+
export function FormSelect<T extends object, State extends DefaultState = DefaultState, Error extends string = DefaultError>({
2525
form,
2626
name,
2727
errorClassName,

src/elements/FormTextArea.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DefaultError, DefaultState, FormState } from "../form";
33
import { DEFAULT_DIRTY_CLASS, DEFAULT_ERROR_CLASS, getClassName } from "./FormInput";
44
import { useListener } from "../hooks";
55

6-
export type FormTextAreaProps<T, State, Error extends string> = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "form" | "name"> & {
6+
export type FormTextAreaProps<T extends object, State, Error extends string> = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "form" | "name"> & {
77
form: FormState<T, State, Error>;
88
name: keyof T;
99
errorClassName?: string;
@@ -21,7 +21,7 @@ export type FormTextAreaProps<T, State, Error extends string> = Omit<TextareaHTM
2121
*
2222
* When this component does not satisfy your needs, you can always [create your own](https://github.com/CodeStix/typed-react-form/wiki/Custom-inputs#example-custom-input).
2323
*/
24-
export function FormTextArea<T, State extends DefaultState = DefaultState, Error extends string = DefaultError>({
24+
export function FormTextArea<T extends object, State extends DefaultState = DefaultState, Error extends string = DefaultError>({
2525
form,
2626
name,
2727
errorClassName,

src/form.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ export type ListenerCallback = () => void;
22
export type ListenerMap = { [T in string]?: ListenerCallback };
33
export type Validator<T, Error> = (values: T) => ErrorType<T, Error> | Promise<ErrorType<T, Error>>;
44

5-
export type ChildFormMap<T, State, Error extends string> = {
6-
[Key in keyof T]?: ChildFormState<T, Key, State, Error>;
5+
export type ChildFormMap<T extends object, State, Error extends string> = {
6+
[K in KeysOfType<T, object>]?: ChildFormState<T, K, State, Error>;
77
};
88

99
export type DirtyMap<T> = {
@@ -19,6 +19,11 @@ export type ErrorMap<T, Error> = {
1919
export type DefaultError = string;
2020
export type DefaultState = { isSubmitting: boolean };
2121

22+
export type FieldsOfType<T, Field> = {
23+
[Key in keyof T as NonNullable<T[Key]> extends Field ? Key : never]: T[Key];
24+
};
25+
export type KeysOfType<T extends FieldsOfType<any, Field>, Field> = keyof FieldsOfType<T, Field>;
26+
2227
function memberCopy<T>(value: T): T {
2328
if (Array.isArray(value)) {
2429
return [...value] as any;
@@ -55,7 +60,7 @@ function comparePrimitiveObject<T>(a: T, b: T): boolean | undefined {
5560
return false;
5661
}
5762

58-
export class FormState<T, State = DefaultState, Error extends string = DefaultError> {
63+
export class FormState<T extends object, State = DefaultState, Error extends string = DefaultError> {
5964
/**
6065
* The id of this form, for debugging purposes.
6166
*/
@@ -174,7 +179,7 @@ export class FormState<T, State = DefaultState, Error extends string = DefaultEr
174179
this.dirtyMap[key] = dirty;
175180

176181
if (notifyChild) {
177-
let child = this.childMap[key];
182+
let child = this.childMap[key as any];
178183
if (child) {
179184
child.setValues(value, validate, isDefault, true, false);
180185
this.dirtyMap[key] = child.dirty;
@@ -362,8 +367,8 @@ export class FormState<T, State = DefaultState, Error extends string = DefaultEr
362367
if (!error) delete this.errorMap[key];
363368
else this.errorMap[key] = error;
364369

365-
if (notifyChild && this.childMap[key]) {
366-
let changed = this.childMap[key]!.setErrors(error ?? ({} as any), true, false);
370+
if (notifyChild && this.childMap[key as any]) {
371+
let changed = this.childMap[(key as unknown) as KeysOfType<T, object>]!.setErrors(error ?? ({} as any), true, false);
367372
if (!changed && error !== undefined) return false;
368373
}
369374

@@ -446,7 +451,7 @@ export class FormState<T, State = DefaultState, Error extends string = DefaultEr
446451
this._state = newState;
447452

448453
let c = Object.keys(this.values) as (keyof T)[];
449-
if (notifyChild) c.forEach((e) => this.childMap[e]?.setState(newState, true, false));
454+
if (notifyChild) c.forEach((e) => (this.childMap[e as any] as ChildFormState<T, any, State, Error>).setState(newState, true, false));
450455

451456
c.forEach((e) => this.fireListeners(e));
452457
this.fireAnyListeners();
@@ -550,15 +555,15 @@ export class FormState<T, State = DefaultState, Error extends string = DefaultEr
550555
}
551556
}
552557

553-
export class ChildFormState<Parent, Key extends keyof Parent, ParentState, ParentError extends string> extends FormState<
554-
NonNullable<Parent[Key]>,
555-
ParentState,
556-
ParentError
558+
export class ChildFormState<T extends FieldsOfType<any, object>, K extends KeysOfType<T, object>, State, Error extends string> extends FormState<
559+
NonNullable<T[K]>,
560+
State,
561+
Error
557562
> {
558-
public name: Key;
559-
public readonly parent: FormState<Parent, ParentState, ParentError>;
563+
public name: K;
564+
public readonly parent: FormState<T, State, Error>;
560565

561-
public constructor(parent: FormState<Parent, ParentState, ParentError>, name: Key) {
566+
public constructor(parent: FormState<T, State, Error>, name: K) {
562567
super(
563568
parent.values[name] ?? ({} as any),
564569
parent.defaultValues[name] ?? ({} as any),
@@ -570,4 +575,17 @@ export class ChildFormState<Parent, Key extends keyof Parent, ParentState, Paren
570575
this.parent = parent;
571576
this.name = name;
572577
}
578+
579+
// public setValueInternal<F extends keyof NonNullable<T[K]>>(
580+
// key: F,
581+
// value: T[K][F] | undefined,
582+
// dirty: boolean,
583+
// validate?: boolean,
584+
// isDefault: boolean = false,
585+
// notifyChild: boolean = true,
586+
// notifyParent: boolean = true,
587+
// fireAny: boolean = true
588+
// ) {
589+
// super.setValueInternal(key, value, dirty, validate, isDefault, notifyChild, notifyParent, fireAny);
590+
// }
573591
}

0 commit comments

Comments
 (0)