Skip to content

Commit

Permalink
adds deprecation warnings to enum, updates docs to indicate they're d…
Browse files Browse the repository at this point in the history
…eprecated
  • Loading branch information
iway1 committed Jan 4, 2023
1 parent 056cbcb commit 4df28e4
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 64 deletions.
113 changes: 61 additions & 52 deletions API.md
Expand Up @@ -6,22 +6,24 @@
- [Hooks](#hooks)

## createTsForm

Create schema form creates a typesafe reusable form component based on your zod-to-component mapping.

```ts
const Form = createTsForm(mapping, options)
const Form = createTsForm(mapping, options);
```

Typically you'll do this once per project, as the mapping can map any number of schemas to any number of components.

### createTsForm params
### createTsForm params

**mapping** - A zod-to-component mapping. An array of two-tuples where the first element is a zod schema and the second element is a React functional component:

```ts
const mapping = [
[z.string(), TextField],
[z.boolean(), CheckboxField],
] as const
] as const;
```

The zod schema on the left determines which properties in your form schemas get mapped to which components:
Expand All @@ -30,113 +32,112 @@ The zod schema on the left determines which properties in your form schemas get
const FormSchema = z.object({
name: z.string(), // Maps to TextField
isOver18: z.boolean(), // Maps to CheckBoxField
})
});
```

You can use any zod schema. Objects get matched based on their properties.

**options** - (**optional**) Allows further customization of the form:

```tsx
const Form = createTsForm(mapping, {
FormComponent: CustomFormComponent,
propsMap: [
['name', 'someOtherPropName'],
] as const
})
```
```tsx
const Form = createTsForm(mapping, {
FormComponent: CustomFormComponent,
propsMap: [["name", "someOtherPropName"]] as const,
});
```

- **options.FormComponent** - (**optional**)A custom form component to use as the container for your field components. Defaults to an html "form". It will be passed a "onSubmit" and "children" prop and should render the children:
- **options.FormComponent** - (**optional**)A custom form component to use as the container for your field components. Defaults to an html "form". It will be passed a "onSubmit" and "children" prop and should render the children:

```tsx
function FormContainer({
children,
onSubmit,
} : {
children: ReactNode,
onSubmit: ()=>void,
}: {
children: ReactNode;
onSubmit: () => void;
}) {
return (
<form onSubmit={onSubmit}>
{children}
<button>Submit</button>
</form>
)
);
}

const MyForm = createTsForm(mapping, {
FormComponent: FormContainer,
})
});
```

- **options.propsMap** - (**optional**) Controls which props get passed to your component as well as allows customizing their name. An array of tuples where the first element is a the name of a **mappable prop**, and the second element is the name of the prop you want it forwarded too. Any elements not included in the array will not be passed to input components, and you will not be able to pass any props included on the right hand side to your components via the `props` prop from the `Form` component.

```ts

function MyComponent({
myCustomPropName,
myCustomControlName,
} : {
myCustomPropName: string, // receives name
myCustomControlName: string, // receives control
}: {
myCustomPropName: string; // receives name
myCustomControlName: string; // receives control
}) {
//...
}
const propsMap = [
['name', 'myCustomPropName'],
['control', 'myCustomControlName']
]
const componentMap = [
[z.string(), MyComponent],
] as const;
["name", "myCustomPropName"],
["control", "myCustomControlName"],
];
const componentMap = [[z.string(), MyComponent]] as const;

const Form = createTsForm(componentMap, {
propsMap,
})
});
```

Defaults to:

```ts
[
['name', 'name'],
['control', 'control'],
['enumValues', 'enumValues'],
]
["name", "name"],
["control", "control"],
["enumValues", "enumValues"],
];
```

Mappable props are:
Mappable props are:

- `name` - the name of the input (the property name in your zod schema).
- `control` - the react hook form control
- `enumValues` - `enumValues` extracted from your zod enum schema.
- `enumValues` - (**deprecated**) `enumValues` extracted from your zod enum schema.
- `label` - The label extracted from `.describe()`
- `placeholder` - The placeholder extracted from `.describe()`

This can be useful in cases where you would like to integrate with existing components, or just don't want `@ts-react/form` to forward any props for you.

## createUniqueFieldSchema

This is useful when dealing with multiple schemas of the same type that you would like to have mapped to different components:

```tsx
const BigTextFieldSchema = createUniqueFieldSchema(z.string(), 'id'); // need to pass a unique string literal
const BigTextFieldSchema = createUniqueFieldSchema(z.string(), "id"); // need to pass a unique string literal

const mapping = [
[z.string(), TextField],
[BigTextFieldSchema, BigTextField]
] as const
[BigTextFieldSchema, BigTextField],
] as const;

const FormSchema = z.object({
name: z.string(), // renders as TextField
bigName: BigTextFieldSchema // renders as BigTextField
})
bigName: BigTextFieldSchema, // renders as BigTextField
});
```

## FormComponent

This is the component returned via `createSchemaForm`

### Props

| **Prop** | **Req** | **Type** | **Description** |
|---------------|---------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ------------- | ------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| schema | Yes | AnyZodObject | A zod object that will be used to validate your form input. |
| onSubmit | Yes | (schema: DataType)=>void | A function that will be called when the form is submitted and validated successfully. |
| props | Maybe | Record<string, ComponentProps> | props to pass to your components. Will be required if any of your components have required props, optional otherwise. |
Expand All @@ -147,52 +148,60 @@ This is the component returned via `createSchemaForm`
| form | No | UseFormReturn | Optionally pass a `react-hook-form` useForm() result so that you can have control of your form state in the parent component. |

## Hooks

### `useTsController`

A typesafe hook that automatically connects to your form state:

```tsx
function TextField() {
const {field: {onChange, value}, error} = useTsController<string>();
const {
field: { onChange, value },
error,
} = useTsController<string>();
// ...
}
```

Returns everything that `react-hook-form`'s [useController](https://react-hook-form.com/api/usecontroller) returns plus an extra `error` object and slightly modified typings to make the form state more intuitive to work with.

### `useDescription`

Returns the `label` and `placeholder` extracted from a call to `.describe()`:

```tsx
function TextField() {
const {label, placeholder} = useDescription() // {label?: string, placeholder?: string};
const { label, placeholder } = useDescription(); // {label?: string, placeholder?: string};
// ...
}

const FormSchema = z.object({
name: z.string().describe("Name // Please enter your name")
})
name: z.string().describe("Name // Please enter your name"),
});
```

Note you can also pass labels and placeholders and normal react props if you prefer.

### `useReqDescription`

Exactly the same as `useDescription`, except it will throw an error if either `label` or `placeholder` is not passed via the `.describe()` syntax. Useful if you want to make sure they're always passed.

### `useEnumValues`
### `useEnumValues` (**deprecated**, don't use)

Returns enum values passed to `z.enum()` for the field that's being rendered:

```tsx
function MyDropdown() {
const values = useEnumValues(); // ['red', 'green', 'blue']
return (
<select>
{values.map(e=>{
{values.map((e) => {
//...
})}
</select>
)
);
}
const FormSchema = z.object({
favoriteColor: z.enum(['red', 'green', 'blue'])
})
favoriteColor: z.enum(["red", "green", "blue"]),
});
```

23 changes: 11 additions & 12 deletions README.md
Expand Up @@ -54,7 +54,7 @@ Create a zod-to-component mapping to map zod schemas to your components then cre
const mapping = [
[z.string(), TextField],
[z.boolean(), CheckBoxField],
[z.enum(["placeholder"]), DropDownSelect],
[z.number(), NumberField],
] as const; // 👈 `as const` is necessary

// A typesafe React component
Expand Down Expand Up @@ -107,7 +107,7 @@ function TextField() {
return (
<>
<input
value={field.value?field.value:''} // conditional to prevent "uncontrolled to controlled" react warning
value={field.value ? field.value : ""} // conditional to prevent "uncontrolled to controlled" react warning
onChange={(e) => {
field.onChange(e.target.value);
}}
Expand Down Expand Up @@ -232,24 +232,22 @@ const MyFormSchema = z.object({
```

## Handling Optionals
`@ts-react/form` will match optionals to their non optional zod schemas:

`@ts-react/form` will match optionals to their non optional zod schemas:

```tsx
const mapping = [
[z.string(), TextField],
] as const
const mapping = [[z.string(), TextField]] as const;

const FormSchema = z.object({
optionalEmail: z.string().email().optional(), // renders to TextField
nullishZipCode: z.string().min(5, "5 chars please").nullish() // renders to TextField
})
nullishZipCode: z.string().min(5, "5 chars please").nullish(), // renders to TextField
});
```

Your zod-component-mapping should not include any optionals. If you want a reusable optional schema, you can do something like this:

```tsx
const mapping = [
[z.string(), TextField],
] as const
const mapping = [[z.string(), TextField]] as const;

export const OptionalTextField = z.string().optional();
```
Expand Down Expand Up @@ -528,14 +526,15 @@ const FormSchemaTwo = z.object({
If you prefer, you can just pass label and placeholder as normal props via `props`.

## TypeScript versions

Older versions of typescript have worse intellisense and may not show an error in your editor. Make sure your editors typescript version is set to v4.9 plus. The easiest approach is to upgrade your typescript globally if you haven't recently:

```sh
sudo npm -g upgrade typescript
```

Or, in VSCode you can do (Command + Shift + P) and search for "Select Typescript Version" to change your editors Typescript Version:

![Screenshot 2023-01-01 at 10 55 11 AM](https://user-images.githubusercontent.com/12774588/210178740-edafa8d1-5a69-4e36-8852-c0a01f36c35d.png)

Note that you can still compile with older versions of typescript and the type checking will work.
Expand Down
48 changes: 48 additions & 0 deletions field-examples.md
@@ -0,0 +1,48 @@
# Example Fields

These can be a good starting points for how to implement certain types of fields.

1. [Select](#select)

## Select

```tsx
function Select({
options
} : {
options: {
label: string,
id: string,
}[]
}) {
const {field, error} = useTsController<string>();
return (
<>
<select
value={field.value?field.value:'none'}
onChange={(e)=>{
field.onChange(e.target.value);
}}
>
{!field.value && <option value="none">Please select...</option>}
{options.map((e) => (
<option value={e.id}>{e.label}</option>
))}
</select>
<span>
{error?.errorMessage && error.errorMessage}
</span>
<>
);
}

const mapping = [
// z.number() is also viable. You may have to use "createUniqueFieldSchema" (since you probably already have a Text Field)
[z.string(), DropDownSelect],
] as const;

const MyForm = z.object({
eyeColor: z.enum(["blue", "brown", "green", "hazel"]),
favoritePants: z.enum(["jeans", "khakis", "none"]),
});
```
2 changes: 2 additions & 0 deletions src/FieldContext.tsx
Expand Up @@ -6,6 +6,7 @@ import {
useController,
UseControllerReturn,
} from "react-hook-form";
import { printUseEnumWarning } from "./logging";
import { errorFromRhfErrorObject } from "./zodObjectErrors";

export const FieldContext = createContext<null | {
Expand Down Expand Up @@ -192,6 +193,7 @@ export function enumValuesNotPassedError() {
*/
export function useEnumValues() {
const { enumValues } = useContextProt("useEnumValues");
printUseEnumWarning();
if (!enumValues) throw new Error(enumValuesNotPassedError());
return enumValues;
}
3 changes: 3 additions & 0 deletions src/createSchemaForm.tsx
Expand Up @@ -16,6 +16,7 @@ import { unwrapEffects, UnwrapZodType } from "./unwrap";
import { RTFBaseZodType, RTFSupportedZodTypes } from "./supportedZodTypes";
import { FieldContextProvider } from "./FieldContext";
import { isZodTypeEqual } from "./isZodTypeEqual";
import { printWarningsForSchema } from "./logging";

/**
* @internal
Expand Down Expand Up @@ -92,6 +93,8 @@ function checkForDuplicateTypes(array: RTFSupportedZodTypes[]) {
array.slice(i + 1).map((w) => [v, w] as const)
);
for (const [a, b] of combinations) {
printWarningsForSchema(a);
printWarningsForSchema(b);
if (isZodTypeEqual(a!, b)) {
throw new Error(duplicateTypeError());
}
Expand Down

0 comments on commit 4df28e4

Please sign in to comment.