diff --git a/www/docs/docs/codesandbox-starter.md b/www/docs/docs/codesandbox-starter.md new file mode 100644 index 0000000..5779d33 --- /dev/null +++ b/www/docs/docs/codesandbox-starter.md @@ -0,0 +1,5 @@ +# CodeSandbox Starter + +Here's a tiny codesandbox starter to get you going: + +https://codesandbox.io/s/react-ts-form-example-6gkuyx?file=/src/App.tsx diff --git a/www/docs/docs/example-inputs/_category_.json b/www/docs/docs/example-inputs/_category_.json new file mode 100644 index 0000000..aa93302 --- /dev/null +++ b/www/docs/docs/example-inputs/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Example Components", + "link": { + "type": "generated-index", + "description": "Usage of the module" + } +} diff --git a/www/docs/docs/example-inputs/checkbox.md b/www/docs/docs/example-inputs/checkbox.md new file mode 100644 index 0000000..556f76e --- /dev/null +++ b/www/docs/docs/example-inputs/checkbox.md @@ -0,0 +1,20 @@ +# CheckBox + +```tsx +function Checkbox({ name }: { name: string }) { + const { + field: { onChange, value }, + } = useTsController(); + + return ( + + ); +} +``` diff --git a/www/docs/docs/example-inputs/multi-checkbox.md b/www/docs/docs/example-inputs/multi-checkbox.md new file mode 100644 index 0000000..54366c9 --- /dev/null +++ b/www/docs/docs/example-inputs/multi-checkbox.md @@ -0,0 +1,40 @@ +# MultiCheckBox + +```tsx +function MultiCheckbox({ options }: { options: string[] }) { + const { + field: { onChange, value }, + } = useTsController(); + + function toggleField(option: string) { + if (!value) onChange([option]); + else { + onChange( + value.includes(option) + ? value.filter((e) => e !== option) + : [...value, option] + ); + } + } + + return ( + <> + {options.map((optionValue) => ( + + ))} + + ); +} +``` diff --git a/www/docs/docs/example-inputs/number-field.md b/www/docs/docs/example-inputs/number-field.md new file mode 100644 index 0000000..150dad4 --- /dev/null +++ b/www/docs/docs/example-inputs/number-field.md @@ -0,0 +1,26 @@ +# NumberField + +```tsx +function NumberField({ req }: { req: number }) { + const { + field: { onChange, value }, + error, + } = useTsController(); + return ( + <> + + {`req is ${req}`} + { + const value = parseInt(e.target.value); + if (isNaN(value)) onChange(undefined); + else onChange(value); + }} + /> + {error && error.errorMessage} + + + ); +} +``` diff --git a/www/docs/docs/example-inputs/select.md b/www/docs/docs/example-inputs/select.md new file mode 100644 index 0000000..cc6ccb4 --- /dev/null +++ b/www/docs/docs/example-inputs/select.md @@ -0,0 +1,25 @@ +# SelectField + +```tsx +function Select({ options }: { options: string[] }) { + const { field, error } = useTsController(); + return ( + <> + + {error?.errorMessage && error.errorMessage} + + ); +} +``` diff --git a/www/docs/docs/example-inputs/text-field.md b/www/docs/docs/example-inputs/text-field.md new file mode 100644 index 0000000..9807b05 --- /dev/null +++ b/www/docs/docs/example-inputs/text-field.md @@ -0,0 +1,20 @@ +# TextField + +```tsx +function TextField() { + const { + field: { onChange, value }, + error, + } = useTsController(); + + return ( + <> + onChange(e.target.value)} + value={value ? value : ""} + /> + {error && error.errorMessage} + + ); +} +``` diff --git a/www/docs/docs/installation.md b/www/docs/docs/installation.md index e60fcff..96f1191 100644 --- a/www/docs/docs/installation.md +++ b/www/docs/docs/installation.md @@ -4,7 +4,7 @@ sidebar_position: 1 # Installation -Make sure you have "strict": true in your `tsconfig.json` `compilerOptions` and make sure you set your editors [typescript version to v4.9](https://github.com/iway1/react-ts-form#typescript-versions) (or intellisense won't be as reliable). +Make sure you have "strict": true in your `tsconfig.json` `compilerOptions` and make sure you set your editors [typescript version to v4.9 or above](https://github.com/iway1/react-ts-form#typescript-versions) (or intellisense won't be as reliable). Install package and dependencies with your preferred package manager: diff --git a/www/docs/docs/manual-form-submission.md b/www/docs/docs/manual-form-submission.md deleted file mode 100644 index 293acf3..0000000 --- a/www/docs/docs/manual-form-submission.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -sidebar_position: 8 ---- - -# Manual Form Submission - -## Submitting the form manually - -The default form component as well as a custom form component (if used) will automatically be passed the `onSubmit` function. -Normally, you'll want to pass a button to the `renderAfter` or `renderBefore` prop of the form: - -```tsx - } /> -``` - -For React Native, or for other reasons, you will need to call `submit` explicitly: - -```tsx - ( - - Submit - - )} -/> -``` diff --git a/www/docs/docs/react-native.md b/www/docs/docs/react-native.md index 3856b4c..626bd73 100644 --- a/www/docs/docs/react-native.md +++ b/www/docs/docs/react-native.md @@ -2,7 +2,9 @@ sidebar_position: 9 --- -# React Native Usage +# React Native + +This library works the same in React Native as it does in React. ## React Native Usage @@ -30,22 +32,3 @@ const mapping = [ const MyForm = createTsForm(mapping, { FormComponent: FormContainer }); ``` - -## Default values - -You can provide typesafe default values like this: - -```tsx -const Schema = z.object({ - string: z.string(), - num: z.number() -}) -//... - -``` diff --git a/www/docs/docs/recommended-setup-tips.md b/www/docs/docs/recommended-setup-tips.md new file mode 100644 index 0000000..212b1f8 --- /dev/null +++ b/www/docs/docs/recommended-setup-tips.md @@ -0,0 +1,125 @@ +# Recommended Setup Tips + +## Tips + +For most projects, the following approach will work well + +## Tip 1. Create a custom form component + +Creating a custom form container is recommended for DRYness. For example: + +```tsx +function FormContainer({ + onSubmit, + children, + loading, +}: { + onSubmit: () => void; + children: ReactNode; + loading?: boolean; +}) { + return ( +
+ {children} + +
+ ); +} + +// Make sure to pass it to `createTsForm` +const Form = createTsForm(mapping, { FormComponent: FormContainer }); +``` + +The above form component has a "loading" prop to show a loading spinner while the form is submitting to make it easy for us to code a great UX. Also notice the form component has a `space-y-4`, ensuring consistent spacing between form components. + +## 2. Consider creating custom components per text field type + +There are many different types of text fields we may want to implement. For example, a phone input might behave differently than a password input. If you're buildling custom behavior per input types, consider using a different Schema per input: + +```tsx +const PhoneSchema = createUniqueFieldSchema( + z.string().regex(/[0-9]{10}/), + "phone" +); +const PasswordSchema = createUniqueFieldSchema( + z.string().min(8, "Must be 8 characters in length"), + "password" +); + +const mapping = [ + [z.string(), TextField], + [PhoneSchema, PhoneField], + [PasswordSchema, PasswordField], +] as const; +``` + +### Component Composition / Wrapper Components + +Since we probably want to share some styling and such across these similar types of fields, we can use a base controlled component as a child of the components passed to the mapping, and the components passed to the mapping would be "wrappers" for this inner base text field component: + +```tsx +function TextFieldBase({ + label, + value, + onChange, + error, + inputProps, +}: { + value: string; + onChange: (value: string) => void; + label?: string; + error?: string; + inputProps?: Omit, "onChange" | "value">; +}) { +
+ + { + onChange(e.target.value); + }} + {...inputProps} + /> + {error} +
; +} + +function TextField() { + const { + field: { onChange, value }, + error, + } = useTsController(); + const { label } = useDescription(); + + return ( + + ); +} + +function PhoneField() { + const { + field: { onChange, value }, + error, + } = useTsController(); + const { label } = useDescription(); + + return ( + onChange(unmaskPhone)} + label={label} + error={error?.errorMessage} + /> + ); +} +``` + +In simple cases this may not be necessary, and may be easier to just pass props to the same TextField component. But if the input components are very different from eachother, we can save passing a lot of props by creating these types of wrappers. If you're using a component library like MUI or Mantine, you likely won't need to define `TextFieldBase` yourself, and should instead use the TextField component from the component library diff --git a/www/docs/docs/troubleshooting/limitations.md b/www/docs/docs/troubleshooting/limitations.md index d0518d6..d442b69 100644 --- a/www/docs/docs/troubleshooting/limitations.md +++ b/www/docs/docs/troubleshooting/limitations.md @@ -7,4 +7,4 @@ sidebar_position: 2 ## Limitations - Doesn't support class components -- `@ts-react/form` allows you to pass props to your components and render elements in between your components, which is good for almost all form designs out there. Some designs may not be easily achievable. For example, if you need a container around multiple sections of your form, this library doesn't allow splitting child components into containers at the moment. (Though if it's a common-enough use case and you'd like to see it added, open an issue!) +- `@ts-react/form` doesn't currently support dependent field props (but will in the future), so you won't be able to modify the behavior of input components based on form values (for example if you needed to hide a form field based on the value of another form field) diff --git a/www/docs/docs/add-non-input-components.md b/www/docs/docs/usage/add-non-input-components.md similarity index 86% rename from www/docs/docs/add-non-input-components.md rename to www/docs/docs/usage/add-non-input-components.md index f5e1542..fde86f8 100644 --- a/www/docs/docs/add-non-input-components.md +++ b/www/docs/docs/usage/add-non-input-components.md @@ -2,7 +2,7 @@ sidebar_position: 6 --- -# Include Non-Input Components +# Non-Input Components ## Adding non input components into your form @@ -21,3 +21,5 @@ Some times you need to render components in between your fields (maybe a form se }} /> ``` + +Or you can use the [custom layouts feature](./custom-layouts.md) diff --git a/www/docs/docs/complex-fields.md b/www/docs/docs/usage/complex-fields.md similarity index 100% rename from www/docs/docs/complex-fields.md rename to www/docs/docs/usage/complex-fields.md diff --git a/www/docs/docs/custom-layouts.md b/www/docs/docs/usage/custom-layouts.md similarity index 100% rename from www/docs/docs/custom-layouts.md rename to www/docs/docs/usage/custom-layouts.md diff --git a/www/docs/docs/usage/default-values.md b/www/docs/docs/usage/default-values.md new file mode 100644 index 0000000..ad3d09a --- /dev/null +++ b/www/docs/docs/usage/default-values.md @@ -0,0 +1,18 @@ +# Default values + +You can provide typesafe default values to your form like this: + +```tsx +const Schema = z.object({ + string: z.string(), + num: z.number() +}) +//... + +``` diff --git a/www/docs/docs/error-handling.md b/www/docs/docs/usage/error-handling.md similarity index 86% rename from www/docs/docs/error-handling.md rename to www/docs/docs/usage/error-handling.md index 9b2135e..67c5e17 100644 --- a/www/docs/docs/error-handling.md +++ b/www/docs/docs/usage/error-handling.md @@ -4,6 +4,8 @@ sidebar_position: 3 # Error Handling +Handling error messages is extremely important for UX. `react-ts-form` makes it very easy to create the best error handling UX by allowing passing validation step specific error messages in your zod schemas, and passing the error messages to your components. + ## Accessing Error Messages in your component `@ts-react/form` also returns an error object that's more accurately typed than `react-hook-forms`'s that you can use to show errors: @@ -56,4 +58,4 @@ return ( ); ``` -For more information about dealing with errors (IE imperatively resetiting errors), check out the [hook form docs](https://react-hook-form.com) +For more information about dealing with errors (IE imperatively resetting errors), check out the [hook form docs](https://react-hook-form.com) diff --git a/www/docs/docs/customize-form-components.md b/www/docs/docs/usage/form-components.md similarity index 97% rename from www/docs/docs/customize-form-components.md rename to www/docs/docs/usage/form-components.md index ac8bcdb..eacf3ef 100644 --- a/www/docs/docs/customize-form-components.md +++ b/www/docs/docs/usage/form-components.md @@ -2,7 +2,7 @@ sidebar_position: 7 --- -# Customize Form Components +# Form Components ## Customizing form components diff --git a/www/docs/docs/useform-state.md b/www/docs/docs/usage/form-state.md similarity index 91% rename from www/docs/docs/useform-state.md rename to www/docs/docs/usage/form-state.md index 2f57f92..46ad9ac 100644 --- a/www/docs/docs/useform-state.md +++ b/www/docs/docs/usage/form-state.md @@ -2,9 +2,9 @@ sidebar_position: 4 --- -# useForm State +# Accessing Form State -## Accessing useForm state +## Form State Sometimes you need to work with the form directly (such as to reset the form from the parent). In these cases, just pass the `react-hook-form` `useForm()` result to your form: diff --git a/www/docs/docs/usage/input-components.md b/www/docs/docs/usage/input-components.md index 9bbecc5..a583908 100644 --- a/www/docs/docs/usage/input-components.md +++ b/www/docs/docs/usage/input-components.md @@ -2,7 +2,7 @@ sidebar_position: 2 --- -# Input Components +# Creating Components ## Creating a custom input component @@ -26,18 +26,3 @@ function TextField() { ``` @ts-react/form will magically connect your component to the appropriate field with this hook. - -## Passing control and name to props - -You can also receive the control and name as props, if you prefer: - -```tsx -function TextField({ control, name }: { control: Control; name: string }) { - const { field, fieldState } = useController({ name, control }); - //... -} -``` - -This approach is less typesafe than useTsController. - -If you want the `control`, `name`, or other `@ts-react/form` data to be passed to props with a different name check out [Prop Forwarding](https://react-ts-form.com/docs/docs/prop-forwarding). diff --git a/www/docs/docs/quality-of-life.md b/www/docs/docs/usage/labels-placeholders.md similarity index 76% rename from www/docs/docs/quality-of-life.md rename to www/docs/docs/usage/labels-placeholders.md index b959d7b..5f6d29a 100644 --- a/www/docs/docs/quality-of-life.md +++ b/www/docs/docs/usage/labels-placeholders.md @@ -2,13 +2,9 @@ sidebar_position: 12 --- -# Quality of Life +# Labels And Placeholders -## ❤️ Quality of Life / Productivity ❤️ - -These allow you to build forms even faster by connecting zod schemas directly to react state. These features are opt-in, it's possible to do the things in this section via props but these approaches may be faster / easier. - -### Quick Labels / Placeholders +## Quick Labels / Placeholders `@ts-react/form` provides a way to quickly add labels / placeholders via `zod`'s `.describe()` method: diff --git a/www/docs/docs/usage/quick-start.md b/www/docs/docs/usage/quick-start.md index 8c2cffa..3c4039e 100644 --- a/www/docs/docs/usage/quick-start.md +++ b/www/docs/docs/usage/quick-start.md @@ -1,9 +1,25 @@ ---- -sidebar_position: 1 ---- - # Quick Start +## Installation + +Make sure you have "strict": true in your `tsconfig.json` `compilerOptions` and make sure you set your editors [typescript version to v4.9 or above](https://github.com/iway1/react-ts-form#typescript-versions) (or intellisense won't be as reliable). + +Install package and dependencies with your preferred package manager: + +### Install package + +```bash +npm install @ts-react/form +``` + +### Required peer dependencies + +```bash +npm install zod react-hook-form @hookform/resolvers +``` + +Or use yarn / pnpm + ## Usage Create a zod-to-component mapping to map zod schemas to your components then create your form with `createTsForm()` (typically once per project): @@ -57,4 +73,4 @@ function MyPage() { That's it! Adding a new field to your form just means adding an additional property to the schema. -It's recommended but not required that you create a custom form component to handle repetitive stuff (like rendering the submit button). +It's highly recommended that you [create a custom form component](./form-components.md) to handle repetitive stuff (like rendering the submit button). diff --git a/www/docs/docs/schema-collisions.md b/www/docs/docs/usage/schema-collisions.md similarity index 100% rename from www/docs/docs/schema-collisions.md rename to www/docs/docs/usage/schema-collisions.md diff --git a/www/docs/docs/zod-refine.md b/www/docs/docs/zod-tips.md similarity index 84% rename from www/docs/docs/zod-refine.md rename to www/docs/docs/zod-tips.md index 9c89ff4..4a27fce 100644 --- a/www/docs/docs/zod-refine.md +++ b/www/docs/docs/zod-tips.md @@ -2,7 +2,7 @@ sidebar_position: 11 --- -# Zod Refine +# Zod Tips ## Using Zod Refine @@ -24,3 +24,7 @@ z.object({ ``` This would map the message "Passwords must match!" to the `confirmPassword` field in case the passwords don't match. Note that `.refine` only validates after the base schema object passes validation. + +## Zod Transform + +Zod transform is not currently supported, this library assumes that the input and output type of your zod schema parsing should match. diff --git a/www/docusaurus.config.js b/www/docusaurus.config.js index 9aec5bc..b88041a 100644 --- a/www/docusaurus.config.js +++ b/www/docusaurus.config.js @@ -41,8 +41,7 @@ const config = { sidebarPath: require.resolve("./sidebars.js"), // Please change this to your repo. // Remove this to remove the "edit this page" links. - editUrl: - "https://github.com/iway1/react-ts-form/tree/main/packages/create-docusaurus/templates/shared/", + editUrl: "https://github.com/iway1/react-ts-form/", }, theme: { @@ -55,6 +54,9 @@ const config = { themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ + colorMode: { + defaultMode: "dark", + }, metadata: [ { name: "twitter:card", @@ -116,9 +118,9 @@ const config = { items: [ { type: "doc", - docId: "docs/installation", + docId: "docs/usage/quick-start", position: "left", - label: "Get Started", + label: "Docs", }, { type: "doc", @@ -145,7 +147,7 @@ const config = { }, { label: "Usage", - to: "docs/category/usage", + to: "docs/docs/usage/quick-start", }, { label: "API", @@ -165,6 +167,7 @@ const config = { ], copyright: `Built with ❤️ - react-ts-form ${new Date().getFullYear()}`, }, + prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, diff --git a/www/sidebars.js b/www/sidebars.js index 52cd926..43c3e0f 100644 --- a/www/sidebars.js +++ b/www/sidebars.js @@ -13,8 +13,53 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - docs: [{ type: "autogenerated", dirName: "docs" }], + docs: [ + { + type: "category", + label: "Usage", + + items: [ + { type: "doc", id: "docs/usage/quick-start" }, + { type: "doc", id: "docs/usage/input-components" }, + { type: "doc", id: "docs/usage/typesafe-props" }, + { type: "doc", id: "docs/usage/error-handling" }, + { type: "doc", id: "docs/usage/complex-fields" }, + { type: "doc", id: "docs/usage/schema-collisions" }, + { type: "doc", id: "docs/usage/form-components" }, + { type: "doc", id: "docs/usage/custom-layouts" }, + { type: "doc", id: "docs/usage/default-values" }, + { type: "doc", id: "docs/usage/form-state" }, + { type: "doc", id: "docs/usage/labels-placeholders" }, + ], + }, + { + type: "category", + label: "Example Components", + items: [ + { type: "doc", id: "docs/example-inputs/text-field" }, + { type: "doc", id: "docs/example-inputs/number-field" }, + { type: "doc", id: "docs/example-inputs/select" }, + { type: "doc", id: "docs/example-inputs/checkbox" }, + { type: "doc", id: "docs/example-inputs/multi-checkbox" }, + ], + }, + { + type: "doc", + id: "docs/recommended-setup-tips", + }, + { + type: "doc", + id: "docs/zod-tips", + }, + { + type: "doc", + id: "docs/react-native", + }, + { + type: "doc", + id: "docs/codesandbox-starter", + }, + ], //api: [{ type: "autogenerated", dirName: "./api" }], api: [ { diff --git a/www/src/components/HomepageFeatures/index.tsx b/www/src/components/HomepageFeatures/index.tsx index 3a28f59..74d8d21 100644 --- a/www/src/components/HomepageFeatures/index.tsx +++ b/www/src/components/HomepageFeatures/index.tsx @@ -51,7 +51,7 @@ function Feature({ title, description }: FeatureItem) { export default function HomepageFeatures(): JSX.Element { const { isDarkTheme } = useColorMode(); - const background = isDarkTheme ? "bg-background" : "bg-white"; + const background = isDarkTheme ? "" : "bg-white"; const text = isDarkTheme ? "text-white" : "text-black"; return (
{ // fetch github stars from api @@ -56,7 +56,7 @@ function HomepageHeader() { ? "bg-white text-black hover:text-black" : "bg-background text-white hover:text-white" }`} - to="/docs/docs/installation" + to="/docs/docs/usage/quick-start" target="_blank" >
@@ -66,10 +66,18 @@ function HomepageHeader() {
- demo +
+ demo +
);