├── .browserslistrc ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── lint-build.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── demo-app ├── .eslintignore ├── app │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── root.tsx │ ├── routes │ │ └── index.tsx │ └── typings-test.tsx ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── remix.config.js ├── remix.env.d.ts └── tsconfig.json ├── package-lock.json ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src └── index.tsx └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | defaults and supports es6-module 4 | maintained node versions 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | build/ 3 | dist/ 4 | node_modules/ 5 | public/build/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').Linter.BaseConfig} 3 | */ 4 | module.exports = { 5 | extends: [ 6 | "@remix-run/eslint-config", 7 | "@remix-run/eslint-config/node", 8 | "@remix-run/eslint-config/jest", 9 | "prettier", 10 | ], 11 | rules: { 12 | "@typescript-eslint/consistent-type-imports": "warn", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/lint-build.yml: -------------------------------------------------------------------------------- 1 | name: ⚙️ Validate 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | pull_request: 7 | 8 | jobs: 9 | lint: 10 | name: Lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: ⬇️ Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - name: ⎔ Setup node 17 | uses: actions/setup-node@v3 18 | with: 19 | cache: "npm" 20 | node-version-file: "package.json" 21 | 22 | - name: 📥 Install deps 23 | run: npm ci 24 | 25 | - name: ✅ Lint 26 | run: npm run lint 27 | 28 | build: 29 | name: Build 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: ⬇️ Checkout repo 33 | uses: actions/checkout@v3 34 | 35 | - name: ⎔ Setup node 36 | uses: actions/setup-node@v3 37 | with: 38 | cache: "npm" 39 | node-version-file: "package.json" 40 | 41 | - name: 📥 Install deps 42 | run: npm ci 43 | 44 | - name: 🏗 Build 45 | run: npm run build 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.code-workspace 2 | .env 3 | .env* 4 | .DS_Store 5 | 6 | .cache/ 7 | /demo-app/build 8 | /dist 9 | /node_modules 10 | /demo-app/node_modules 11 | /demo-app/public/build 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 Matt Brophy 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remix Validity State 2 | 3 | `remix-validity-state` is a small [React](https://reactjs.org/) form validation library that aims to embrace HTML input validation and play nicely with [Remix](https://remix.run) and [React Router](https://reactrouter.com/en/main) primitives (specifically submitting forms to `action` handlers). However, it's worth noting that this library doesn't use anything specific from Remix or React Router and could be leveraged in any React application. 4 | 5 | > [!CAUTION] 6 | > 7 | > This library is no longer in active development as I didn't really have enough time to keep working on it in my spare time. Feel free to fork it for personal use, but if you're looking for a production-ready solution I would recommend checking out [Conform](https://conform.guide/) which is much more actively maintained and has a lot of the same underlying "use the platform" design goals as this library. 8 | 9 | - [Remix Validity State](#remix-validity-state) 10 | - [Design Goals](#design-goals) 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Demo App](#demo-app) 14 | - [Getting Started](#getting-started) 15 | - [Define your form validations](#define-your-form-validations) 16 | - [Provide your validations via `FormProvider`](#provide-your-validations-via-formprovider) 17 | - [Render `` Components inside your `FormProvider`](#render-input-components-inside-your-formprovider) 18 | - [Wire up server-side validations](#wire-up-server-side-validations) 19 | - [Add your server action response to the `FormProvider`](#add-your-server-action-response-to-the-formprovider) 20 | - [That's it!](#thats-it) 21 | - [Advanced Usages and Concepts](#advanced-usages-and-concepts) 22 | - [`ExtendedValidityState`](#extendedvaliditystate) 23 | - [Multiple Inputs with the Same Name](#multiple-inputs-with-the-same-name) 24 | - [Dynamic (Form-Dependent) Validation Attributes](#dynamic-form-dependent-validation-attributes) 25 | - [Custom Validations](#custom-validations) 26 | - [Server-only Validations](#server-only-validations) 27 | - [Error Messages](#error-messages) 28 | - [useValidatedInput()](#usevalidatedinput) 29 | - [Custom `ref` usage](#custom-ref-usage) 30 | - [Textarea and Select Elements](#textarea-and-select-elements) 31 | - [Radio and Checkbox Inputs](#radio-and-checkbox-inputs) 32 | - [Styling](#styling) 33 | - [Typescript](#typescript) 34 | - [Feedback + Contributing](#feedback--contributing) 35 | 36 | ## Design Goals 37 | 38 | This library is built with the following design goals in mind: 39 | 40 | **1. Leverage built-in HTML input validation attributes _verbatim_** 41 | 42 | What ever happened to good old ``? Far too often we reach for some custom validation library just to check that a value is not empty (and potentially ship a boatload of JS to the client in order to do so). Let's use what we have readily available when we can! That way we don't have to relearn something new. If you already know some of the HTML validation attributes...then you're ready to use this library. 43 | 44 | **2. Share validations between client and server** 45 | 46 | Thanks to `Remix`, this is finally _much_ more straightforward than it has been in the past. But wait 🤔, aren't we using DOM validations? We don't have a DOM on the server?!? Don't worry - in true Remix spirit, we emulate the DOM validations on the server. 47 | 48 | **3. Expose validation results via a [`ValidityState`](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState)-like API** 49 | 50 | We will need an API to explain the validation state of an input...good news - the web already has one! Let's [`#useThePlatform`](https://twitter.com/search?q=%23usetheplatform) and build on top of `ValidityState`. 51 | 52 | **4. Permit custom sync/async validations beyond those built into HTML** 53 | 54 | Congrats for making it to bullet 4 and not leaving as soon as we mentioned the super-simple HTML validations. Don't worry - it's not lost on me that folks need to check their email addresses for uniqueness in the DB. We've got you covered with custom sync/async validations. 55 | 56 | **5. Provide limited abstractions to simplify form markup generation** 57 | 58 | Semantically correct and accessible `
` markup is verbose. Any convenient form library oughta provide _some_ wrapper components to make simple forms _easy_. However, any form library worth it's weight has to offer low level access to allow for true custom forms, and the ability to built custom abstractions for your application use-case. Therefore, any wrapper components will be little more than syntactic sugar on top of the lower level APIs. 59 | 60 | ## Installation 61 | 62 | ```sh 63 | > npm install remix-validity-state 64 | 65 | # or 66 | 67 | > yarn add remix-validity-state 68 | ``` 69 | 70 | > **Info** 71 | > 72 | > This library is bundled for modern browsers (see `.browserslistrc`). If you need to support older browsers you may need to configure your build process accordingly. 73 | 74 | ## Usage 75 | 76 | ### Demo App 77 | 78 | There's a sample Remix app in this repository in the `demo-app/` folder, so you can open it in StackBlitz or run it locally: 79 | 80 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/brophdawg11/remix-validity-state/tree/main/demo-app?file=app%2Froutes%2Findex.tsx&title=remix-validity-state%20Demo%20App) 81 | 82 | To run the app locally: 83 | 84 | ```bash 85 | git clone git@github.com:brophdawg11/remix-validity-state.git 86 | cd remix-validity-state/demo-app 87 | npm ci 88 | npm run dev 89 | ``` 90 | 91 | ### Getting Started 92 | 93 | #### Define your form validations 94 | 95 | In order to share validations between server and client, we define a single object containing all of our form field validations, keyed by the input names. Validations are specified using the built-in HTML validation attributes, exactly as you'd render them onto a JSX ``. 96 | 97 | If you're using TypeScript (and you should!) you should define a schema that corresponds to the `FormDefinition` interface so you can benefit from proper type inference on library APIs. 98 | 99 | ```ts 100 | interface FormSchema { 101 | inputs: { 102 | firstName: InputDefinition; 103 | middleInitial: InputDefinition; 104 | lastName: InputDefinition; 105 | emailAddress: InputDefinition; 106 | }; 107 | } 108 | 109 | let formDefinition: FormSchema = { 110 | inputs: { 111 | firstName: { 112 | validationAttrs: { 113 | required: true, 114 | maxLength: 50, 115 | }, 116 | }, 117 | middleInitial: { 118 | validationAttrs: { 119 | pattern: "^[a-zA-Z]{1}$", 120 | }, 121 | }, 122 | lastName: { 123 | validationAttrs: { 124 | required: true, 125 | maxLength: 50, 126 | }, 127 | }, 128 | emailAddress: { 129 | validationAttrs: { 130 | type: "email", 131 | required: true, 132 | maxLength: 50, 133 | }, 134 | }, 135 | }, 136 | }; 137 | ``` 138 | 139 | This allows us to directly render these attributes onto our HTML inputs internally via something like `` 140 | 141 | #### Provide your validations via `FormProvider` 142 | 143 | In order to make these validations easily accessible, we provide them via a `` that should wrap your underlying `` element. We do this with a wrapper component around the actual context for better TypeScript inference. 144 | 145 | ```jsx 146 | import { FormProvider } from "remix-validity-state"; 147 | 148 | function MyFormPage() { 149 | return ( 150 | 151 | {/* Your goes in here */} 152 | 153 | ); 154 | } 155 | ``` 156 | 157 | #### Render `` Components inside your `FormProvider` 158 | 159 | ```jsx 160 | import { FormProvider } from "remix-validity-state"; 161 | 162 | function MyFormPage() { 163 | return ( 164 | 165 | 166 | 167 | 168 | 169 | 170 | ); 171 | } 172 | ``` 173 | 174 | The `` component is our wrapper that handles the `