Skip to content

Commit 8ed26e4

Browse files
committed
Custom input examples
1 parent e7038d7 commit 8ed26e4

File tree

3 files changed

+110
-43
lines changed

3 files changed

+110
-43
lines changed

example/src/CustomInput.tsx

Lines changed: 107 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,113 @@
11
import React, { InputHTMLAttributes } from "react";
2-
import { FormState, useListener } from "typed-react-form";
3-
import { VisualRender } from "./VisualRender";
2+
import { defaultDeserializer, defaultSerializer, FormState, SerializeProps, useForm, useListener } from "typed-react-form";
43

5-
/**
6-
* A custom input that can be reused everywhere when using useForm
7-
*/
8-
export function CustomInput<T extends object>({
9-
form,
10-
name,
11-
...rest
12-
}: {
13-
form: FormState<T, { isSubmitting: boolean }>;
14-
name: keyof T;
15-
} & Omit<InputHTMLAttributes<HTMLInputElement>, "name" | "form">) {
16-
const { value, dirty, defaultValue, error, state } = useListener(form, name);
4+
// Most basic example of a type-checked input
5+
function CustomInput1<T extends object>(props: { form: FormState<T>; name: keyof T }) {
6+
// Listen for changes on form field
7+
const { value, setValue } = useListener(props.form, props.name);
8+
// Convert value to string, and set string on change. This only works with text fields
9+
return <input value={value + ""} onChange={(ev) => setValue(ev.target.value as any)} />;
10+
}
11+
12+
// Basic example of a type-checked input, which you can pass <input> props to.
13+
// Make sure to add Omit, otherwise our form and name props will interfere
14+
function CustomInput2<T extends object>(props: { form: FormState<T>; name: keyof T } & Omit<InputHTMLAttributes<HTMLInputElement>, "form" | "name">) {
15+
// Rest contains <input> props
16+
const { form, name, ...rest } = props;
17+
const { value, setValue } = useListener(form, name);
18+
// Apply <input> props using {...rest}
19+
return <input value={value + ""} onChange={(ev) => setValue(ev.target.value as any)} {...rest} />;
20+
}
21+
22+
// Example of a type-checked number input
23+
function CustomInput3<T extends object>(props: { form: FormState<T>; name: keyof T }) {
24+
const { value, setValue } = useListener(props.form, props.name);
25+
// Convert value to string, and back to number on change
26+
return <input value={value + ""} onChange={(ev) => setValue(parseFloat(ev.target.value) as any)} />;
27+
}
28+
29+
// Example of a type-checked input using the builtin serializer (that is also used for FormInput), this
30+
// serializer supports date fields, number fields, primitive array fields. SerializeProps will add the type prop.
31+
// Make sure to add SerializeProps
32+
function CustomInput4<T extends object>(props: { form: FormState<T>; name: keyof T } & SerializeProps) {
33+
const { value, setValue } = useListener(props.form, props.name);
34+
// defaultSerializer: converts value to string
35+
// defaultDeserializer: converts string back to value
36+
let v = defaultSerializer(value, props);
37+
return (
38+
<input
39+
type={props.type}
40+
value={typeof v === "string" ? v : undefined}
41+
checked={typeof v === "boolean" ? v : undefined}
42+
onChange={(ev) => setValue(defaultDeserializer(ev.target.value, ev.target.checked, value, props))}
43+
/>
44+
);
45+
}
1746

47+
// Example of a type-checked input using the type-checked default serializer,
48+
// where value field of your custom input is type-checked
49+
function CustomInput5<T extends object, Key extends keyof T>(props: { form: FormState<T>; name: Key } & SerializeProps<T[Key]>) {
50+
const { value, setValue } = useListener(props.form, props.name);
51+
let v = defaultSerializer(value, props);
52+
return (
53+
<input
54+
type={props.type}
55+
value={typeof v === "string" ? v : undefined}
56+
checked={typeof v === "boolean" ? v : undefined}
57+
onChange={(ev) => setValue(defaultDeserializer(ev.target.value, ev.target.checked, value, props))}
58+
/>
59+
);
60+
}
61+
62+
// Fully fledged type-checked input
63+
function CustomInput6<T extends object, Key extends keyof T>(
64+
props: { form: FormState<T>; name: Key } & SerializeProps<T[Key]> &
65+
Omit<InputHTMLAttributes<HTMLInputElement>, "form" | "name" | "value" | "type">
66+
) {
67+
// Remove SerializeProps, form and name from props so rest contains the <input> props.
68+
const { form, name, dateAsNumber, setNullOnUncheck, setUndefinedOnUncheck, value: inputValue, type, ...rest } = props;
69+
const { value, setValue } = useListener(form, name);
70+
let v = defaultSerializer(value, { dateAsNumber, setNullOnUncheck, setUndefinedOnUncheck, value: inputValue, type });
71+
return (
72+
<input
73+
type={type}
74+
value={typeof v === "string" ? v : undefined}
75+
checked={typeof v === "boolean" ? v : undefined}
76+
onChange={(ev) => setValue(defaultDeserializer(ev.target.value, ev.target.checked, value, props))}
77+
{...rest}
78+
/>
79+
);
80+
}
81+
82+
export function CustomInputsForm() {
83+
const form = useForm(
84+
{
85+
firstName: "John",
86+
lastName: "Pineapple",
87+
gender: "male" as "male" | "female"
88+
},
89+
(values) => ({ firstName: values.firstName.length < 3 ? "Firstname must be longer!" : undefined }) // Example validator
90+
);
1891
return (
19-
<VisualRender>
20-
<input
21-
disabled={state.isSubmitting}
22-
placeholder={defaultValue as any}
23-
style={{
24-
background: dirty ? "#eee" : "#fff",
25-
padding: "0.3em",
26-
border: "1px solid #0005",
27-
borderRadius: "0.5em",
28-
fontSize: "inherit",
29-
color: error ? "#e11" : "initial"
30-
}}
31-
value={value as any}
32-
onChange={(ev) => form.setValue(name, ev.target.value as any)}
33-
{...rest}
34-
/>
35-
{error && (
36-
<span
37-
style={{
38-
padding: "0.3em",
39-
fontWeight: "bold",
40-
color: "red"
41-
}}
42-
>
43-
{error}
44-
</span>
45-
)}
46-
</VisualRender>
92+
<form
93+
style={{ margin: "1em" }}
94+
onReset={() => form.resetAll()}
95+
onSubmit={async (ev) => {
96+
ev.preventDefault();
97+
if (form.error) return;
98+
form.setState({ isSubmitting: true });
99+
console.log("submit", form.values);
100+
}}
101+
>
102+
<CustomInput1 form={form} name="lastName" />
103+
<CustomInput2 form={form} name="lastName" style={{ background: "gray" }} />
104+
<CustomInput3 form={form} name="lastName" />
105+
<CustomInput4 form={form} name="lastName" type="checkbox" />
106+
<CustomInput5 form={form} name="gender" type="radio" value="female" />
107+
<CustomInput5 form={form} name="gender" type="radio" value="male" />
108+
<CustomInput6 form={form} name="gender" type="radio" />
109+
<button>Submit</button>
110+
<button type="reset">Reset</button>
111+
</form>
47112
);
48113
}

example/src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { BrowserRouter, Route, Switch } from "react-router-dom";
88
import { StyledForm } from "./StyledForm";
99
import { ExampleForm } from "./ExampleForm";
1010
import App from "./App";
11+
import { CustomInputsForm } from "./CustomInput";
1112

1213
function Router() {
1314
return (
@@ -17,6 +18,7 @@ function Router() {
1718
<Route path="/object-types-array" component={OneOfObjectArrayForm} />
1819
<Route path="/styled-form" component={StyledForm} />
1920
<Route path="/test" component={App} />
21+
<Route path="/custom-inputs" component={CustomInputsForm} />
2022
<Route path="/" component={ExampleForm} />
2123
</Switch>
2224
</BrowserRouter>

src/elements/FormInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export type Serializer<T> = (currentValue: T, props: SerializeProps<T>) => boole
3232

3333
export type Deserializer<T> = (inputValue: string, inputChecked: boolean, currentValue: T, props: SerializeProps<T>) => T;
3434

35-
export type SerializeProps<V> = {
35+
export type SerializeProps<V = any> = {
3636
dateAsNumber?: boolean;
3737
setUndefinedOnUncheck?: boolean;
3838
setNullOnUncheck?: boolean;

0 commit comments

Comments
 (0)