-A fast, completely type-checked React form builder, focussed on typescript integration and minimal rerenders. Featuring:
+
+
+
+
-- [Object fields](https://codestix.github.io/typed-react-form/advanced/Object-fields)
-- [Array fields](https://codestix.github.io/typed-react-form/advanced/Array-fields)
-- [Validation](https://codestix.github.io/typed-react-form/validation)
-- [Easily toggle fields](https://codestix.github.io/typed-react-form/advanced/Toggling-a-field)
-- [Listeners (subscription based state updates)](https://codestix.github.io/typed-react-form/reference/useListener)
+
+ A completely type-checked form builder for React with Typescript
+
-**All of this while keeping type-checking!**
+- ✔️ **Type-checked**: Make less errors, even field names are strongly typed.
+- 🤔 **Simple**: A well [documented](https://codestix.github.io/typed-react-form/), intuitive and easy to understand api.
+- :fire: **Fast**: Only rerenders the fields that change if used correctly. This allows you to create huge forms.
+- 📦 **Pretty Small**: [](https://bundlephobia.com/result?p=typed-react-form)
## Install
@@ -20,6 +28,17 @@ npm install typed-react-form
## [Documentation here](https://codestix.github.io/typed-react-form/)
+## Typescript demos
+
+### Type-checked field names
+
+
+### Type-checked custom inputs
+
+
+### Type-checked object/array fields
+
+
## Javascript/typescript React
This library is built from the ground up for React with typescript, but it also works with with vanilla React, without enforced type checking.
@@ -28,10 +47,11 @@ This library is built from the ground up for React with typescript, but it also
Contributions are welcome.
-1. Clone this repo
+1. Clone this repo.
2. Install deps using `yarn`. Yarn is required because of the resolutions field in package.json, npm does not support this.
-3. Open a new terminal and navigate to `example/`, run `yarn` and `yarn start` to start the testing application.
-4. Done! When you edit source code, it will be rebuilt and update the testing application.
+3. Run `yarn start`, this will watch source files in `src/` and rebuild on change.
+4. Open a new terminal and navigate to `testing/`, run `yarn` and `yarn start` to start the testing application.
+5. Done! When you edit source code, it will be rebuilt and update the testing application.
## License
diff --git a/Todo.md b/Todo.md
index 7407dff..a0e88f1 100644
--- a/Todo.md
+++ b/Todo.md
@@ -2,7 +2,6 @@
- Nested validators in child forms to improve validation performance
- Combine array helpers into one object? This is usefull to pass to other components
- Require index for array fields
-- Add React.forwardRef to input elements
-- Rename `setDefaultValues` to `setAllValues`
-- Let `comparePrimitiveObject` compare deep objects too
- Field on blur
+- Use React.forwardRef instead of innerRef on Field/FieldError
+- Rename `FormField` -> `FieldInfo` & better documentation on passed Field props
diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md
index c7fb167..9619802 100644
--- a/docs/Troubleshooting.md
+++ b/docs/Troubleshooting.md
@@ -36,13 +36,19 @@ When you use [styled-components](https://github.com/styled-components/styled-com
```tsx
// Example styled CustomInput
const StyledCustomInput: typeof CustomInput = styled(CustomInput)`
- &.typed-form-dirty {
+ &.field-dirty {
background-color: #0001;
}
- &.typed-form-error {
+ &.field-error {
color: red;
font-weight: bold;
}
`;
```
+
+---
+
+## Element type is invalid: expected a string (for built-in components) or a class/function but got: undefined
+
+This sometimes happens after installing this package. Restart your project to fix it. (restart `react-scripts`)
diff --git a/docs/_config.yml b/docs/_config.yml
index 6f4d6d7..4d32bd7 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -1,10 +1,12 @@
-theme: "just-the-docs"
-baseurl: "/typed-react-form"
+# theme: "just-the-docs"
+# baseurl: "/typed-react-form"
remote_theme: pmarsceill/just-the-docs
aux_links:
"GitHub":
- "https://github.com/CodeStix/typed-react-form"
+ "NPM":
+ - "https://www.npmjs.com/package/typed-react-form"
aux_links_new_tab: true
diff --git a/docs/examples/Auto-disable-submit-button.md b/docs/examples/Auto-disable-submit-button.md
index a8f374a..9bac8be 100644
--- a/docs/examples/Auto-disable-submit-button.md
+++ b/docs/examples/Auto-disable-submit-button.md
@@ -24,7 +24,7 @@ function FormExample() {
onSubmit={(ev) => {
ev.preventDefault();
console.log("save", form.values);
- form.setDefaultValues(form.values);
+ form.setValues(form.values);
}}
>
diff --git a/docs/images/demo-custom.gif b/docs/images/demo-custom.gif
new file mode 100644
index 0000000..d85c67a
Binary files /dev/null and b/docs/images/demo-custom.gif differ
diff --git a/docs/images/demo-example.gif b/docs/images/demo-example.gif
new file mode 100644
index 0000000..e64fcb2
Binary files /dev/null and b/docs/images/demo-example.gif differ
diff --git a/docs/images/demo-objectfield.gif b/docs/images/demo-objectfield.gif
new file mode 100644
index 0000000..4060608
Binary files /dev/null and b/docs/images/demo-objectfield.gif differ
diff --git a/docs/images/thumb.pdn b/docs/images/thumb.pdn
new file mode 100755
index 0000000..2130789
Binary files /dev/null and b/docs/images/thumb.pdn differ
diff --git a/docs/images/thumb.png b/docs/images/thumb.png
new file mode 100755
index 0000000..336b4eb
Binary files /dev/null and b/docs/images/thumb.png differ
diff --git a/docs/images/thumbextrasmall.png b/docs/images/thumbextrasmall.png
new file mode 100755
index 0000000..503eec6
Binary files /dev/null and b/docs/images/thumbextrasmall.png differ
diff --git a/docs/images/thumbsmall.png b/docs/images/thumbsmall.png
new file mode 100755
index 0000000..49ab650
Binary files /dev/null and b/docs/images/thumbsmall.png differ
diff --git a/docs/index.md b/docs/index.md
index 8f41a6c..3801cf4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -19,6 +19,8 @@ yarn add typed-react-form
This library works with both **Javascript** and **Typescript**. Typescript is certainly preferred because of the enforced type-checking!
+**Make sure to restart your project after installing. (restart `react-scripts`)**
+
## Step 2: Creating a form
### Using the `useForm` hook
@@ -36,6 +38,13 @@ function MyForm() {
}
```
+
+
### Creating the submit handler
Use `form.handleSubmit` to validate before calling your function. It does not get called if there is a validation error, and prevents the page from reloading.
@@ -49,14 +58,15 @@ function MyForm() {
const form = useForm({ email: "" });
function submit() {
- // The form.handleSubmit validates the form before calling this function
+ // You don't have to use form.handleSubmit, it just validates
+ // your form before calling this function
console.log("submit", form.values);
}
- // Use the standard html form element, which exposes the onSubmit event.
+ // Use the standard html form element, which has the onSubmit event.
+ // Make sure to add type="submit" to your submit button
return (
);
@@ -78,10 +88,8 @@ function MyForm() {
async function submit() {
// Implement your submit logic here...
console.log("submitting", form.values);
- // Fake fetch, by waiting for 500ms
- await new Promise((res) => setTimeout(res, 500));
// Optional: set new default values
- form.setDefaultValues(form.values);
+ form.setValues(form.values);
}
return (
diff --git a/docs/reference/Field.md b/docs/reference/Field.md
index b5a949e..96d5ef6 100644
--- a/docs/reference/Field.md
+++ b/docs/reference/Field.md
@@ -21,7 +21,7 @@ The input transforms its value based on the `type` prop, which **currently suppo
It is allowed to use multiple inputs on the same field, all of them will be synchronized.
-These inputs are given a `className` when errored (`typed-form-error`) or modified (`typed-form-dirty`) by default.
+These inputs are given a `className` when errored (`field-error`) or modified (`field-dirty`) by default.
## Examples
@@ -127,7 +127,46 @@ const form = useForm({
### Styling/custom component
-TODO
+#### ❌ Inline styling
+
+You can pass the `style` and `className` props to the `Field` component, but this will get repetitive and annoying fast.
+
+```tsx
+
+
+
+
+
+
+
+
+
+```
+
+You should only use the `errorStyle`, `dirtyStyle`, `errorClassName` and `dirtyClassName` props in rare situations where you need specific style overrides for a specific field.
+
+#### ✔️ Using custom component
+
+You should pass a custom component to the `as` prop of `Field`. The props required by your component will be placed on the `Field` component (will be type-checked).
+
+
+
+Some props of your custom component will be filled in automatically by the `Field` component:
+
+|`onChange`|The change handler, this can be a `React.ChangeEventHandler` or a `(value: T) => void`, ready to be passed to your underlying input.
+|`value`|The current value in string format, ready to be passed to the underlying input element.
+|`checked`|The current checked state as boolean, ready to be passed to the underlying input element.
+|`disabled`|A boolean that will be true when submitting.
+|`field`|A [`FormField`](/typed-react-form/reference/useListener.html#return-value) instance, which contains information about the field like the value, its error, the form it is part of and whether is has been modified.
+|`style`|Will merge your passed `style` with `errorStyle` when there is an error on this field and `dirtyStyle` when the field has been modified.
+|`className`|Will merge your passed className with `field-error` when there is an error on this field and `field-dirty` when the field has been modified. (These classNames can be changed using the `errorClassName` and `dirtyClassName` props)
+
+**If you don't like this way of passing props**, you can also [create custom inputs](/typed-react-form/examples/Custom-input) using the useListener hook (advanced).
### Submit
@@ -154,13 +193,17 @@ The form or child form that contains the field to bind to this input.
The name of the field in the form that will be bound to this input.
+#### `as` (`"input"` by default)
+
+The element/componment to render, this can be a string specifying "input", "select", "textarea" or a custom component. Is "input" by default. The props of the passed custom component are available on this Field component.
+
#### `errorClassName` and `errorStyle`
-The className and/or style to set when there is an error on this field. Default className is `typed-form-error`.
+The className and/or style to set when there is an error on this field. Default className is `field-error`.
#### `dirtyClassName` and `dirtyStyle`
-The className and/or style to set when this field has been modified. Default className is `typed-form-dirty`.
+The className and/or style to set when this field has been modified. Default className is `field-dirty`.
#### `dateAsNumber` (only type="date")
@@ -182,6 +225,9 @@ Make sure you pass along a `value` too, this is the value that will be set when
- The value of the checkbox when using primitive arrays.
- The on-checked value of the checkbox when using the `setNullOnUncheck/setUndefinedOnUncheck` prop.
+#### `innerRef`
+The ref prop to pass to the underlaying component/input.
+
---
## Custom fields/inputs
diff --git a/docs/reference/FormState.md b/docs/reference/FormState.md
index 4849483..dd8b101 100644
--- a/docs/reference/FormState.md
+++ b/docs/reference/FormState.md
@@ -19,7 +19,7 @@ The values of the form. Can be set with `setValues()`.
#### `defaultValues` **(readonly)**
-The default values of the form. Input elements do not change this. This gets used to reset the form and to calculate dirty flags. Can be set with `setDefaultValues()`.
+The default values of the form. Input elements do not change this. This gets used to reset the form and to calculate dirty flags. Can be set with `setValues(?,?,true)`.
#### `childMap` **(readonly)**
@@ -78,14 +78,7 @@ Sets values _OR_ default values for a form.
- `values` **(required)**: The new values/default values to set on this form.
- `validate` **(optional)**: Validate? When defined, overrides `validateOnChange`.
-- `isDefault` **(optional, false)**: When true, updates the default values instead of the normal values. Only updates one or the other, to set both at the same time, use `setDefaultValues()` or 2 function calls.
-
-#### `setDefaultValues(values, validate = true, notifyChild = true, notifyParent = true)`
-
-Set both values _AND_ default values for a form. If you only want to set default values, use `setValues(...,...,true)`.
-
-- `values` **(required)**: The new values to set on this form.
-- `validate` **(optional)**: Validate? When defined, overrides `validateOnChange`.
+- `isDefault` **(optional)**: Leave undefined to set both `values` and `defaultValues`. Set to true to only set `defaultValues` and false to only set `values`.
#### `validate()`
diff --git a/docs/reference/useForm.md b/docs/reference/useForm.md
index 59e6aea..55216f1 100644
--- a/docs/reference/useForm.md
+++ b/docs/reference/useForm.md
@@ -11,7 +11,7 @@ Creates a new form state manager. This hook does not cause a rerender.
This hook must be called, unconditionally, at the start of your component, just like the normal React hooks.
-**Note for using `setState` along with `useForm`**: This library is built upon the fact that only the things that change should rerender. When using `setState` with the form, a state change causes the whole form to rerender. You can reduce this problem by creating components from the things that use the state, or you can use [custom form state](/typed-react-form/reference/useForm#defaultstate-optional-issubmitting-false).
+**Note for using `setState` along with `useForm`**: This library is built upon the fact that only the things that change should rerender. When using `setState` with the form, a state change causes the whole form to rerender. You can reduce this problem by creating components from the things that use the state, or you can use [custom form state](/typed-react-form/reference/useForm.html#defaultstate-optional).
`useForm(defaultValues, validator?, validateOnChange = false, validateOnMount = false, defaultState = {isSubmitting: false})`
diff --git a/docs/reference/useListener.md b/docs/reference/useListener.md
index ccb4bac..6e9af0d 100644
--- a/docs/reference/useListener.md
+++ b/docs/reference/useListener.md
@@ -88,7 +88,7 @@ function BreadForm() {
## Return value
-Returns a object containing the following fields, which you can destruct:
+Returns a `FormField` instance containing the following fields, which you can destruct:
```tsx
return {
@@ -98,8 +98,8 @@ return {
dirty, // True if the field is modified (if not default value anymore)
error, // The error on this field
state, // The state of the form (contains isSubmitting)
- form; // The form this field belongs to
-}
+ form // The form this field belongs to (FormState instance)
+};
// Example usage
const { value, setValue, error } = useListener(form, "fieldname");
diff --git a/package.json b/package.json
index 83e91ac..3061f3d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "typed-react-form",
- "version": "1.3.2",
- "description": "A React form form builder, focussed on typescript integration and minimal rerenders.",
+ "version": "2.2.3",
+ "description": "A completely type-checked React form builder.",
"author": "codestix",
"license": "MIT",
"repository": "CodeStix/typed-react-form",
diff --git a/src/Field.tsx b/src/Field.tsx
index a7f19ca..ef7b9ee 100644
--- a/src/Field.tsx
+++ b/src/Field.tsx
@@ -14,7 +14,7 @@ export type FieldProps = {
*/
form: FormState;
/**
- * The name of the field
+ * The name of the field in form
*/
name: K;
/**
@@ -43,14 +43,54 @@ export type FieldProps = {
* You can change the behaviour of the default deserializer using the `type` prop.
*/
deserializer?: Deserializer;
+ /**
+ * The class to set when there is an error on this field.
+ */
+ errorClassName?: string;
+ /**
+ * The class to set when this field has been modified.
+ */
+ dirtyClassName?: string;
+ /**
+ * The style to set when where is an error on this field.
+ */
+ errorStyle?: React.CSSProperties;
+ /**
+ * The style to set when this field has been modified.
+ */
+ dirtyStyle?: React.CSSProperties;
+ /**
+ * The ref to pass to your input component.
+ */
+ innerRef?: React.Ref;
};
+// Note on innerRef: React.forwardRef breaks type-checking
+
export function Field | keyof JSX.IntrinsicElements = "input">(
props: FieldProps &
- Omit, "value" | "checked" | "onChange" | "field" | keyof FieldProps | keyof SerializeProps> &
+ Omit, "value" | "checked" | "onChange" | "field" | "ref" | keyof FieldProps | keyof SerializeProps> &
SerializeProps
) {
- const { form, as = "input", serializer, dateAsNumber, setNullOnUncheck, setUndefinedOnUncheck, deserializer, hideWhenNull, ...rest } = props;
+ const {
+ form,
+ as = "input",
+ serializer,
+ dateAsNumber,
+ setNullOnUncheck,
+ setUndefinedOnUncheck,
+ deserializer,
+ hideWhenNull,
+ disabled,
+ className,
+ style,
+ errorClassName,
+ errorStyle,
+ dirtyClassName,
+ dirtyStyle,
+ innerRef,
+ ...rest
+ } = props;
const serializeProps = {
dateAsNumber,
setNullOnUncheck,
@@ -63,12 +103,20 @@ export function Field {
- let v = "target" in ev ? (["checkbox", "radio"].includes(props.type!) ? ev.target.checked : ev.target.value) : ev;
+ let v =
+ typeof ev === "object" && "target" in ev
+ ? props.type === "checkbox" || props.type === "radio"
+ ? ev.target.checked
+ : ev.target.value
+ : ev;
if (typeof v === "string" || typeof v === "boolean")
field.setValue((deserializer ?? defaultDeserializer)(v, field.value, serializeProps));
else field.setValue(v);
@@ -110,6 +158,9 @@ export function defaultSerializer(currentValue: T, props: SerializeProps):
case "datetime-local":
case "date": {
let dateValue = currentValue as any;
+ if (dateValue === null || dateValue === undefined || dateValue === "") {
+ return "";
+ }
if (typeof dateValue === "string") {
let ni = parseInt(dateValue);
if (!isNaN(ni)) dateValue = ni;
diff --git a/src/FieldError.tsx b/src/FieldError.tsx
index 9487f0b..85c2ebb 100644
--- a/src/FieldError.tsx
+++ b/src/FieldError.tsx
@@ -30,10 +30,14 @@ export function FieldError<
*/
as?: C;
transformer?: (error: ErrorType) => React.ReactNode;
- } & Omit, "transformer" | "as" | "name" | "form" | "children" | "field">
+ /**
+ * The ref to pass to your error component.
+ */
+ innerRef?: React.Ref;
+ } & Omit, "transformer" | "as" | "name" | "form" | "children" | "field" | "ref">
) {
- const { form, as = React.Fragment, transformer, ...rest } = props;
+ const { form, as = React.Fragment, transformer, innerRef, ...rest } = props;
const field = useListener(form, props.name);
if (!field.error || typeof field.error === "object") return null;
- return React.createElement(as, { ...rest, field, children: transformer ? transformer(field.error) : String(field.error) });
+ return React.createElement(as, { ...rest, ref: innerRef, field, children: transformer ? transformer(field.error) : String(field.error) });
}
diff --git a/src/form.ts b/src/form.ts
index fa12880..b3f2859 100644
--- a/src/form.ts
+++ b/src/form.ts
@@ -30,7 +30,7 @@ function memberCopy(value: T): T {
} else if (typeof value === "object") {
return { ...value };
} else {
- throw new Error("Can only memberCopy() arrays and objects.");
+ throw new Error(`Can only memberCopy() arrays and objects, got '${String(value)}'. Probably due to invalid useForm() value.`);
}
}
@@ -62,12 +62,12 @@ export class FormState | undefined,
validate?: boolean,
- isDefault: boolean = false,
+ isDefault?: boolean,
notifyChild: boolean = true,
notifyParent: boolean = true
) {
+ if (isDefault === undefined) {
+ this.setValues(values, false, true, notifyChild, notifyParent);
+ isDefault = false;
+ }
+
let keys = Object.keys(isDefault ? this.defaultValues : this.values);
let v: typeof values = values ?? {};
addDistinct(keys, Object.keys(v));
@@ -287,18 +295,6 @@ export class FormState, validate: boolean = true, notifyChild: boolean = true, notifyParent: boolean = true) {
- this.setValues(values, false, true, notifyChild, notifyParent);
- this.setValues(values, validate, false, notifyChild, notifyParent);
- }
-
/**
* Force validation on this form. Required when `validateOnChange` is disabled. **This function works with both asynchronous and synchronous validators.**
* @returns true if the form is valid.
diff --git a/src/hooks.ts b/src/hooks.ts
index f8d67f6..43fd27b 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -157,7 +157,7 @@ export function useArrayField<
useEffect(() => {
let id = parentForm.listen(name, () => {
let val = parentForm.values[name] as any;
- if (val.length !== oldLength.current) {
+ if (Array.isArray(val) && val.length !== oldLength.current) {
setRender((i) => i + 1);
oldLength.current = val.length;
}
@@ -166,17 +166,17 @@ export function useArrayField<
}, []);
const append = useCallback((value: NonNullable[any]) => {
- form.setValues([...(form.values as any), value] as any);
+ form.setValues([...(form.values as any), value] as any, true, false);
}, []);
const remove = useCallback((index: number) => {
let newValues = [...(form.values as any)];
newValues.splice(index, 1);
- form.setValues(newValues as any);
+ form.setValues(newValues as any, true, false);
}, []);
const clear = useCallback(() => {
- form.setValues([] as any);
+ form.setValues([] as any, true, false);
}, []);
const move = useCallback((from: number, to: number) => {
@@ -188,7 +188,7 @@ export function useArrayField<
newArr[k] = newArr[k + increment];
}
newArr[to] = target;
- form.setValues(newArr as any);
+ form.setValues(newArr as any, true, false);
}, []);
const swap = useCallback((index: number, newIndex: number) => {
@@ -197,7 +197,7 @@ export function useArrayField<
}
let values = [...(form.values as any)];
[values[index], values[newIndex]] = [values[newIndex], values[index]];
- form.setValues(values as any);
+ form.setValues(values as any, true, false);
}, []);
return {
diff --git a/testing/src/Fieldform.tsx b/testing/src/Fieldform.tsx
index f47bddd..6df4ca2 100644
--- a/testing/src/Fieldform.tsx
+++ b/testing/src/Fieldform.tsx
@@ -1,51 +1,46 @@
-import React from "react";
-import { useForm, Field, AnyListener, FieldError, FormField, Listener } from "typed-react-form";
+import React, { useRef } from "react";
+import { useForm, Field, AnyListener, ObjectField } from "typed-react-form";
-function Input(props: { value?: string; onChange?: (value: string) => void; style: React.CSSProperties }) {
- return props.onChange?.(ev.target.value)} />;
-}
-
-function validate(_: any) {
- return {
- email: "yikes"
- };
-}
+const inputStyle: React.CSSProperties = {
+ color: "gray",
+ padding: "0.3em"
+};
-function Error(props: { children: React.ReactNode; field: FormField }) {
- return (
-