├── .changeset ├── README.md └── config.json ├── .github └── workflows │ ├── changeset-release.yml │ └── main.yml ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── packages ├── react-next │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ ├── array.ts │ │ ├── index.test.tsx │ │ ├── index.ts │ │ ├── map-obj.ts │ │ ├── object.ts │ │ ├── scalar.ts │ │ ├── types.ts │ │ └── utils.ts └── react │ ├── CHANGELOG.md │ ├── package.json │ └── src │ ├── array.ts │ ├── composite-types.ts │ ├── index.tsx │ ├── map-obj.ts │ ├── object.ts │ ├── types.ts │ ├── useForm.ts │ ├── validation.ts │ └── value-types.ts ├── test-app ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ └── index.tsx └── tsconfig.json ├── tsconfig.json └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with mono-repos or single package repos to help you verion and release your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) 4 | 5 | We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 6 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@0.3.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "Thinkmill/magical-forms" } 6 | ], 7 | "commit": false, 8 | "linked": [], 9 | "access": "public", 10 | "baseBranch": "master" 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/changeset-release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@master 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Setup Node.js 14.x 19 | uses: actions/setup-node@master 20 | with: 21 | version: 14.x 22 | 23 | - name: Install Yarn 24 | run: npm install --global yarn 25 | 26 | - name: Install Dependencies 27 | run: yarn 28 | 29 | - name: "Create Pull Request or Publish to npm" 30 | uses: changesets/action@master 31 | with: 32 | publish: yarn release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | tests: 5 | name: Tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | 10 | - name: Set Node.js 14.x 11 | uses: actions/setup-node@master 12 | with: 13 | version: 14.x 14 | 15 | - name: Install Yarn 16 | run: npm install --global yarn 17 | 18 | - name: Install Dependencies 19 | run: yarn 20 | 21 | - name: Test 22 | run: yarn jest 23 | types: 24 | name: Type Checking 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@master 28 | 29 | - name: Set Node.js 14.x 30 | uses: actions/setup-node@master 31 | with: 32 | version: 14.x 33 | 34 | - name: Install Yarn 35 | run: npm install --global yarn 36 | 37 | - name: Install Dependencies 38 | run: yarn 39 | 40 | - name: Check Types 41 | run: yarn tsc 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .next/ 4 | *.log 5 | out/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✨ Magical Forms ✨ 2 | 3 | > Write forms in React that feel like ✨ magic ✨ 4 | 5 | ## Getting Started 6 | 7 | ```bash 8 | yarn add @magical-forms/react 9 | ``` 10 | 11 | ## Thanks/Inspiration 12 | 13 | - [`react-use-form-state`](https://github.com/wsmd/react-use-form-state) for the concept of having functions that return props for a certain kind of input 14 | - [`sarcastic`](https://github.com/jamiebuilds/sarcastic) for some thoughts about writing schemaish things in a way that makes JS type systems happy 15 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | require.resolve("@babel/preset-env"), 4 | require.resolve("@babel/preset-typescript"), 5 | require.resolve("@babel/preset-react"), 6 | ], 7 | plugins: [ 8 | require.resolve("@babel/plugin-transform-runtime"), 9 | require.resolve("@babel/plugin-proposal-class-properties"), 10 | require.resolve("babel-plugin-macros"), 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@magical-forms/monorepo", 3 | "private": true, 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "scripts": { 7 | "postinstall": "preconstruct dev", 8 | "changeset": "changeset", 9 | "release": "preconstruct build && changeset publish", 10 | "start": "cd test-app && yarn next dev", 11 | "test": "jest" 12 | }, 13 | "preconstruct": { 14 | "packages": [ 15 | "packages/*" 16 | ], 17 | "distFilenameStrategy": "unscoped-package-name" 18 | }, 19 | "workspaces": [ 20 | "packages/*", 21 | "test-app" 22 | ], 23 | "resolutions": { 24 | "**/@babel/parser": "7.9.4" 25 | }, 26 | "dependencies": { 27 | "@babel/cli": "^7.10.4", 28 | "@babel/core": "^7.10.4", 29 | "@babel/plugin-proposal-class-properties": "^7.10.4", 30 | "@babel/plugin-transform-runtime": "^7.10.4", 31 | "@babel/preset-env": "^7.10.4", 32 | "@babel/preset-react": "^7.10.4", 33 | "@babel/preset-typescript": "^7.10.4", 34 | "@changesets/changelog-github": "^0.4.2", 35 | "@changesets/cli": "^2.20.0", 36 | "@emotion/core": "^10.0.22", 37 | "@manypkg/cli": "^0.19.1", 38 | "@preconstruct/cli": "^2.1.5", 39 | "@testing-library/react": "^10.3.0", 40 | "@types/babel__core": "^7.1.2", 41 | "@types/jest": "^24.0.15", 42 | "@types/react": "^16.8.23", 43 | "@types/react-dom": "^16.8.4", 44 | "babel-jest": "^24.8.0", 45 | "babel-plugin-macros": "^2.8.0", 46 | "jest": "^24.8.0", 47 | "jest-emotion": "^10.0.17", 48 | "jest-in-case": "^1.0.2", 49 | "prettier": "^2.0.4", 50 | "react": "^17.0.2", 51 | "react-dom": "^17.0.2", 52 | "typescript": "^4.5.2" 53 | } 54 | } -------------------------------------------------------------------------------- /packages/react-next/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @magical-forms/react-next 2 | 3 | ## 0.3.2 4 | 5 | ### Patch Changes 6 | 7 | - [`3f69b45`](https://github.com/Thinkmill/magical-forms/commit/3f69b45abafdec1a747d7cad9f68fe9ed0301e5d) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - `setState` on `Form` is now a method so that `Form>` can be assigned to `Form`. 8 | 9 | ## 0.3.1 10 | 11 | ### Patch Changes 12 | 13 | - [`72c9c66`](https://github.com/Thinkmill/magical-forms/commit/72c9c665f551633e47c45759f040d60d6d16eea4) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fixed setting array fields to an empty array not working 14 | 15 | ## 0.3.0 16 | 17 | ### Minor Changes 18 | 19 | - [`f59b99d`](https://github.com/Thinkmill/magical-forms/commit/f59b99dd5d78d81863c6ac8339c021bc29a8dab2) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Added `array` field which is similar to the `object` field except that you provide it a single field and it stores an array of that field(the field that's provided could be another array field, object field or scalar field). Unless you provide an initial value for an array field, it will default to an empty array. 20 | 21 | An example form: 22 | 23 | ```jsx 24 | import { 25 | array, 26 | getInitialState, 27 | object, 28 | scalar, 29 | useForm, 30 | validation 31 | } from "@magical-forms/react-next"; 32 | 33 | const text = scalar({ 34 | props: ({ onBlur, onChange, touched, error, value }) => { 35 | return { onChange, onBlur, value, error: touched ? error : undefined }; 36 | }, 37 | initialValue: (input: string | undefined) => input || "" 38 | }); 39 | 40 | let i = 0; 41 | 42 | const key = scalar({ 43 | props: ({ value }) => value, 44 | initialValue: () => i++ 45 | }); 46 | 47 | const element = object({ 48 | key: key(), 49 | value: text({ 50 | validate(value) { 51 | if (!value.length) { 52 | return validation.invalid("Must not be empty"); 53 | } 54 | return validation.valid(value); 55 | } 56 | }) 57 | }); 58 | 59 | const schema = array(element); 60 | 61 | export default function Index() { 62 | let form = useForm(schema, [{}]); 63 | return ( 64 |
65 |
    66 | {form.elements.map((element, i) => { 67 | return ( 68 |
  • 69 |
    72 | 73 | 82 |
    83 |
  • 84 | ); 85 | })} 86 |
87 | 94 |
95 | ); 96 | } 97 | ``` 98 | 99 | Some important things going on here: 100 | 101 | - Like the fields inside of an `object` field are on `form.fields`, the fields inside of an `array` field are on `form.elements` 102 | - There is a `key` field, this is so that we have a consistent key to provide to React when removing/re-ordering elements. This isn't that important in this particular example but would be important if fields had components with their own state inside that should be preserved when removing/re-ordering elements. 103 | - When adding a new item, we can use `getInitialState(element)` to easily default the state of an element using the same logic that is used when doing `useForm(element)` 104 | 105 | * [`f59b99d`](https://github.com/Thinkmill/magical-forms/commit/f59b99dd5d78d81863c6ac8339c021bc29a8dab2) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Switched the order of the arguments provided to `stateFromChange` in scalar fields so that it is `(next, current)` to be consistent with `stateFromChange` in object fields and the new `array` field. `current` may also now be undefined when adding a new element to an array field. 106 | 107 | ### Patch Changes 108 | 109 | - [`f59b99d`](https://github.com/Thinkmill/magical-forms/commit/f59b99dd5d78d81863c6ac8339c021bc29a8dab2) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fix "Type instantiation is excessively deep and possibly infinite" error on newer versions of TypeScript 110 | 111 | ## 0.2.3 112 | 113 | ### Patch Changes 114 | 115 | - [`cafd7fb`](https://github.com/Thinkmill/magical-forms/commit/cafd7fb7c250139e9bda0125943fe4b60c155205) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Rename `getInitialValue` to `getInitialState` and fix the return type 116 | 117 | ## 0.2.2 118 | 119 | ### Patch Changes 120 | 121 | - [`b6e56f6`](https://github.com/Thinkmill/magical-forms/commit/b6e56f6a523739deba7d82acfc148bbcb3596aa6) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Add `getInitialValue` 122 | 123 | ## 0.2.1 124 | 125 | ### Patch Changes 126 | 127 | - [`2ea82a1`](https://github.com/Thinkmill/magical-forms/commit/2ea82a13f697ae2f2516717f49c87218b8944049) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Add `resetForm` 128 | 129 | ## 0.2.0 130 | 131 | ### Minor Changes 132 | 133 | - [`d61e4c8`](https://github.com/Thinkmill/magical-forms/commit/d61e4c8905c4287b2938070b30eb1a4acc1ecb55) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Expose some more internal types 134 | 135 | ## 0.1.5 136 | 137 | ### Patch Changes 138 | 139 | - [`be847c4`](https://github.com/Thinkmill/magical-forms/commit/be847c4be4bed455cfd9c41774c355e10d5c5801) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fix a type bug 140 | 141 | ## 0.1.4 142 | 143 | ### Patch Changes 144 | 145 | - [`0982f00`](https://github.com/Thinkmill/magical-forms/commit/0982f00c6918a3af50d798d55c297d4d116de4f6) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Add \_field property to field instances 146 | 147 | ## 0.1.3 148 | 149 | ### Patch Changes 150 | 151 | - [`aab7aa0`](https://github.com/Thinkmill/magical-forms/commit/aab7aa052b69f10e8d7ec168e94d423e938d4a80) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fix a bug with various types 152 | 153 | ## 0.1.2 154 | 155 | ### Patch Changes 156 | 157 | - [`2fabeb1`](https://github.com/Thinkmill/magical-forms/commit/2fabeb1115c83aca309cfd63dfff2b0d1495dec1) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Add touched to scalar field props function input 158 | 159 | ## 0.1.1 160 | 161 | ### Patch Changes 162 | 163 | - [`a7ffae9`](https://github.com/Thinkmill/magical-forms/commit/a7ffae9195b0fff2bbc92a996d738faaf19ed472) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fix some bugs with validation 164 | 165 | ## 0.1.0 166 | 167 | ### Minor Changes 168 | 169 | - [`3f7aa3e`](https://github.com/Thinkmill/magical-forms/commit/3f7aa3e7a8e0fd466b33c3aa98f0f0cbb95819cd) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Initial release 170 | -------------------------------------------------------------------------------- /packages/react-next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@magical-forms/react-next", 3 | "version": "0.3.2", 4 | "main": "dist/react-next.cjs.js", 5 | "module": "dist/react-next.esm.js", 6 | "dependencies": { 7 | "@babel/runtime": "^7.7.2" 8 | }, 9 | "peerDependencies": { 10 | "react": "^16.9.0 || 17" 11 | }, 12 | "devDependencies": { 13 | "@testing-library/react": "^10.3.0", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2" 16 | } 17 | } -------------------------------------------------------------------------------- /packages/react-next/src/array.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getFieldValidity, 3 | ObjectField, 4 | ValidationFnInObjectValidation, 5 | ValidationObj, 6 | } from "./object"; 7 | import { ScalarField } from "./scalar"; 8 | import { FormState, Field, FormValue, ValidatedFormValue, Form } from "./types"; 9 | import { getValueFromState, getFieldInstance } from "./utils"; 10 | 11 | export type ArrayFieldInstance> = ( 12 | | { 13 | readonly validity: "valid"; 14 | readonly value: ValidatedFormValue; 15 | } 16 | | { 17 | readonly validity: "invalid"; 18 | readonly value: FormValue; 19 | } 20 | ) & { 21 | setState(elements: FormState): void; 22 | readonly state: FormState; 23 | readonly elements: readonly Form[]; 24 | readonly _field: TArrayField; 25 | }; 26 | 27 | export function getArrayFieldInstance( 28 | field: ArrayField, 29 | state: readonly any[], 30 | setState: (state: (prevState: readonly any[]) => readonly any[]) => void, 31 | validationResult: any 32 | ): ArrayFieldInstance { 33 | const elements = state.map((stateElement, i) => { 34 | return getFieldInstance( 35 | field.element, 36 | stateElement, 37 | (thing: any) => { 38 | setState((prevState) => { 39 | const newVal = [...prevState]; 40 | newVal[i] = thing(newVal[i]); 41 | return newVal; 42 | }); 43 | }, 44 | validationResult[i] 45 | ); 46 | }); 47 | 48 | return { 49 | elements, 50 | state, 51 | setState: (elements) => { 52 | setState(() => elements as any); 53 | }, 54 | validity: getFieldValidity(field, validationResult), 55 | value: getValueFromState(field, state), 56 | _field: field, 57 | }; 58 | } 59 | 60 | type ArrayFieldValidation< 61 | Element extends Field, 62 | Value 63 | > = Element extends ScalarField 64 | ? ValidationFnInObjectValidation 65 | : Element extends ObjectField 66 | ? ValidationObj 67 | : never | undefined; 68 | 69 | export type ArrayField = { 70 | readonly kind: "array"; 71 | readonly element: Element; 72 | readonly validate: 73 | | ArrayFieldValidation[]> 74 | | undefined; 75 | // this API is still def bad but meh 76 | readonly stateFromChange: 77 | | (( 78 | next: readonly FormState[], 79 | current: readonly FormState[] | undefined 80 | ) => readonly FormState[]) 81 | | undefined; 82 | }; 83 | 84 | export function array( 85 | element: Element, 86 | options?: { 87 | validate?: ArrayFieldValidation[]>; 88 | stateFromChange?: ( 89 | next: readonly FormState[], 90 | current?: readonly FormState[] 91 | ) => readonly FormState[]; 92 | } 93 | ): ArrayField { 94 | return { 95 | kind: "array", 96 | element, 97 | validate: options?.validate, 98 | stateFromChange: options?.stateFromChange, 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /packages/react-next/src/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useForm, 3 | object, 4 | scalar, 5 | validation, 6 | InitialValueInput, 7 | Form, 8 | ValidatedFormValue, 9 | FormState, 10 | FormValue, 11 | resetForm, 12 | } from "."; 13 | import { render } from "@testing-library/react"; 14 | import React from "react"; 15 | import { act } from "react-dom/test-utils"; 16 | import { array } from "./array"; 17 | 18 | // let text = scalar({ 19 | // props: () => ({}), 20 | // initialValue: (thing: string) => thing, 21 | // }); 22 | 23 | // let thing = object({ thing: text() }); 24 | 25 | // let result = useForm(thing, { thing: "" }); 26 | 27 | // result.fields.thing.setState({ 28 | // touched: true, 29 | // value: "", 30 | // }); 31 | 32 | let text = scalar({ 33 | props: (x) => x, 34 | initialValue: (value?: string | undefined) => value || "", 35 | }); 36 | 37 | test("validation order is correct", () => { 38 | let calls: string[] = []; 39 | let formSchema = object( 40 | { 41 | something: object( 42 | { 43 | thing: text({ 44 | validate: (value) => { 45 | calls.push("inner"); 46 | if (value === "nope") { 47 | return validation.invalid("nope inner"); 48 | } 49 | return validation.valid(value); 50 | }, 51 | }), 52 | }, 53 | { 54 | validate: { 55 | thing: (value, other) => { 56 | expect(other).toEqual({ 57 | thing: value, 58 | }); 59 | 60 | calls.push("middle"); 61 | if (value === "middle") { 62 | return validation.invalid("nope middle"); 63 | } 64 | return validation.valid(value); 65 | }, 66 | }, 67 | } 68 | ), 69 | }, 70 | { 71 | validate: { 72 | something: { 73 | thing: (value, other) => { 74 | expect(other).toEqual({ 75 | something: { thing: value }, 76 | }); 77 | calls.push("outer"); 78 | if (value === "outer") { 79 | return validation.invalid("nope outer"); 80 | } 81 | return validation.valid(value); 82 | }, 83 | }, 84 | }, 85 | } 86 | ); 87 | let form: Form | undefined; 88 | function Comp() { 89 | form = useForm(formSchema); 90 | return null; 91 | } 92 | render(); 93 | if (!form) { 94 | throw new Error("form not rendered"); 95 | } 96 | expect(calls).toEqual(["inner", "middle", "outer"]); 97 | expect(form).toMatchInlineSnapshot(` 98 | Object { 99 | "_field": Object { 100 | "fields": Object { 101 | "something": Object { 102 | "fields": Object { 103 | "thing": Object { 104 | "initialValue": [Function], 105 | "kind": "scalar", 106 | "props": [Function], 107 | "stateFromChange": undefined, 108 | "validate": [Function], 109 | }, 110 | }, 111 | "kind": "object", 112 | "stateFromChange": undefined, 113 | "validate": Object { 114 | "thing": [Function], 115 | }, 116 | }, 117 | }, 118 | "kind": "object", 119 | "stateFromChange": undefined, 120 | "validate": Object { 121 | "something": Object { 122 | "thing": [Function], 123 | }, 124 | }, 125 | }, 126 | "fields": Object { 127 | "something": Object { 128 | "_field": Object { 129 | "fields": Object { 130 | "thing": Object { 131 | "initialValue": [Function], 132 | "kind": "scalar", 133 | "props": [Function], 134 | "stateFromChange": undefined, 135 | "validate": [Function], 136 | }, 137 | }, 138 | "kind": "object", 139 | "stateFromChange": undefined, 140 | "validate": Object { 141 | "thing": [Function], 142 | }, 143 | }, 144 | "fields": Object { 145 | "thing": Object { 146 | "_field": Object { 147 | "initialValue": [Function], 148 | "kind": "scalar", 149 | "props": [Function], 150 | "stateFromChange": undefined, 151 | "validate": [Function], 152 | }, 153 | "error": undefined, 154 | "props": Object { 155 | "error": undefined, 156 | "onBlur": [Function], 157 | "onChange": [Function], 158 | "onFocus": [Function], 159 | "touched": false, 160 | "validity": "valid", 161 | "value": "", 162 | }, 163 | "setState": [Function], 164 | "state": Object { 165 | "touched": false, 166 | "value": "", 167 | }, 168 | "validity": "valid", 169 | "value": "", 170 | }, 171 | }, 172 | "setState": [Function], 173 | "state": Object { 174 | "thing": Object { 175 | "touched": false, 176 | "value": "", 177 | }, 178 | }, 179 | "validity": "valid", 180 | "value": Object { 181 | "thing": "", 182 | }, 183 | }, 184 | }, 185 | "setState": [Function], 186 | "state": Object { 187 | "something": Object { 188 | "thing": Object { 189 | "touched": false, 190 | "value": "", 191 | }, 192 | }, 193 | }, 194 | "validity": "valid", 195 | "value": Object { 196 | "something": Object { 197 | "thing": "", 198 | }, 199 | }, 200 | } 201 | `); 202 | calls = []; 203 | act(() => { 204 | form!.setState({ 205 | something: { 206 | thing: { 207 | touched: form!.state.something.thing.touched, 208 | value: "nope", 209 | }, 210 | }, 211 | }); 212 | }); 213 | expect(form.validity).toBe("invalid"); 214 | expect(form.fields.something.fields.thing.error).toBe("nope inner"); 215 | expect(form).toMatchInlineSnapshot(` 216 | Object { 217 | "_field": Object { 218 | "fields": Object { 219 | "something": Object { 220 | "fields": Object { 221 | "thing": Object { 222 | "initialValue": [Function], 223 | "kind": "scalar", 224 | "props": [Function], 225 | "stateFromChange": undefined, 226 | "validate": [Function], 227 | }, 228 | }, 229 | "kind": "object", 230 | "stateFromChange": undefined, 231 | "validate": Object { 232 | "thing": [Function], 233 | }, 234 | }, 235 | }, 236 | "kind": "object", 237 | "stateFromChange": undefined, 238 | "validate": Object { 239 | "something": Object { 240 | "thing": [Function], 241 | }, 242 | }, 243 | }, 244 | "fields": Object { 245 | "something": Object { 246 | "_field": Object { 247 | "fields": Object { 248 | "thing": Object { 249 | "initialValue": [Function], 250 | "kind": "scalar", 251 | "props": [Function], 252 | "stateFromChange": undefined, 253 | "validate": [Function], 254 | }, 255 | }, 256 | "kind": "object", 257 | "stateFromChange": undefined, 258 | "validate": Object { 259 | "thing": [Function], 260 | }, 261 | }, 262 | "fields": Object { 263 | "thing": Object { 264 | "_field": Object { 265 | "initialValue": [Function], 266 | "kind": "scalar", 267 | "props": [Function], 268 | "stateFromChange": undefined, 269 | "validate": [Function], 270 | }, 271 | "error": "nope inner", 272 | "props": Object { 273 | "error": "nope inner", 274 | "onBlur": [Function], 275 | "onChange": [Function], 276 | "onFocus": [Function], 277 | "touched": false, 278 | "validity": "invalid", 279 | "value": "nope", 280 | }, 281 | "setState": [Function], 282 | "state": Object { 283 | "touched": false, 284 | "value": "nope", 285 | }, 286 | "validity": "invalid", 287 | "value": "nope", 288 | }, 289 | }, 290 | "setState": [Function], 291 | "state": Object { 292 | "thing": Object { 293 | "touched": false, 294 | "value": "nope", 295 | }, 296 | }, 297 | "validity": "invalid", 298 | "value": Object { 299 | "thing": "nope", 300 | }, 301 | }, 302 | }, 303 | "setState": [Function], 304 | "state": Object { 305 | "something": Object { 306 | "thing": Object { 307 | "touched": false, 308 | "value": "nope", 309 | }, 310 | }, 311 | }, 312 | "validity": "invalid", 313 | "value": Object { 314 | "something": Object { 315 | "thing": "nope", 316 | }, 317 | }, 318 | } 319 | `); 320 | act(() => { 321 | resetForm(form!, { something: { thing: "" } }); 322 | }); 323 | expect(form.value).toMatchInlineSnapshot(` 324 | Object { 325 | "something": Object { 326 | "thing": "", 327 | }, 328 | } 329 | `); 330 | act(() => { 331 | resetForm(form!, { something: { thing: "something" } }); 332 | }); 333 | expect(form.value).toMatchInlineSnapshot(` 334 | Object { 335 | "something": Object { 336 | "thing": "something", 337 | }, 338 | } 339 | `); 340 | }); 341 | 342 | test("validation order is correct with an array", () => { 343 | let calls: string[] = []; 344 | let formSchema = array( 345 | object( 346 | { 347 | something: object( 348 | { 349 | thing: text({ 350 | validate: (value) => { 351 | calls.push("inner"); 352 | if (value === "nope") { 353 | return validation.invalid("nope inner"); 354 | } 355 | return validation.valid(value); 356 | }, 357 | }), 358 | }, 359 | { 360 | validate: { 361 | thing: (value, other) => { 362 | expect(other).toEqual({ 363 | thing: value, 364 | }); 365 | 366 | calls.push("middle"); 367 | if (value === "middle") { 368 | return validation.invalid("nope middle"); 369 | } 370 | return validation.valid(value); 371 | }, 372 | }, 373 | } 374 | ), 375 | }, 376 | { 377 | validate: { 378 | something: { 379 | thing: (value, other) => { 380 | expect(other).toEqual({ 381 | something: { thing: value }, 382 | }); 383 | calls.push("outer"); 384 | if (value === "outer") { 385 | return validation.invalid("nope outer"); 386 | } 387 | return validation.valid(value); 388 | }, 389 | }, 390 | }, 391 | } 392 | ), 393 | { 394 | validate: { 395 | something: { 396 | thing(value, other) { 397 | expect(other).toEqual([ 398 | { 399 | something: { thing: value }, 400 | }, 401 | ]); 402 | calls.push("full outer"); 403 | if (value === "outer") { 404 | return validation.invalid("nope full outer"); 405 | } 406 | return validation.valid(value); 407 | }, 408 | }, 409 | }, 410 | } 411 | ); 412 | let form: Form | undefined; 413 | function Comp() { 414 | form = useForm(formSchema, [{ something: "" }]); 415 | return null; 416 | } 417 | render(); 418 | if (!form) { 419 | throw new Error("form not rendered"); 420 | } 421 | expect(calls).toEqual(["inner", "middle", "outer", "full outer"]); 422 | debugger; 423 | expect(form).toMatchInlineSnapshot(` 424 | Object { 425 | "_field": Object { 426 | "element": Object { 427 | "fields": Object { 428 | "something": Object { 429 | "fields": Object { 430 | "thing": Object { 431 | "initialValue": [Function], 432 | "kind": "scalar", 433 | "props": [Function], 434 | "stateFromChange": undefined, 435 | "validate": [Function], 436 | }, 437 | }, 438 | "kind": "object", 439 | "stateFromChange": undefined, 440 | "validate": Object { 441 | "thing": [Function], 442 | }, 443 | }, 444 | }, 445 | "kind": "object", 446 | "stateFromChange": undefined, 447 | "validate": Object { 448 | "something": Object { 449 | "thing": [Function], 450 | }, 451 | }, 452 | }, 453 | "kind": "array", 454 | "stateFromChange": undefined, 455 | "validate": Object { 456 | "something": Object { 457 | "thing": [Function], 458 | }, 459 | }, 460 | }, 461 | "elements": Array [ 462 | Object { 463 | "_field": Object { 464 | "fields": Object { 465 | "something": Object { 466 | "fields": Object { 467 | "thing": Object { 468 | "initialValue": [Function], 469 | "kind": "scalar", 470 | "props": [Function], 471 | "stateFromChange": undefined, 472 | "validate": [Function], 473 | }, 474 | }, 475 | "kind": "object", 476 | "stateFromChange": undefined, 477 | "validate": Object { 478 | "thing": [Function], 479 | }, 480 | }, 481 | }, 482 | "kind": "object", 483 | "stateFromChange": undefined, 484 | "validate": Object { 485 | "something": Object { 486 | "thing": [Function], 487 | }, 488 | }, 489 | }, 490 | "fields": Object { 491 | "something": Object { 492 | "_field": Object { 493 | "fields": Object { 494 | "thing": Object { 495 | "initialValue": [Function], 496 | "kind": "scalar", 497 | "props": [Function], 498 | "stateFromChange": undefined, 499 | "validate": [Function], 500 | }, 501 | }, 502 | "kind": "object", 503 | "stateFromChange": undefined, 504 | "validate": Object { 505 | "thing": [Function], 506 | }, 507 | }, 508 | "fields": Object { 509 | "thing": Object { 510 | "_field": Object { 511 | "initialValue": [Function], 512 | "kind": "scalar", 513 | "props": [Function], 514 | "stateFromChange": undefined, 515 | "validate": [Function], 516 | }, 517 | "error": undefined, 518 | "props": Object { 519 | "error": undefined, 520 | "onBlur": [Function], 521 | "onChange": [Function], 522 | "onFocus": [Function], 523 | "touched": false, 524 | "validity": "valid", 525 | "value": "", 526 | }, 527 | "setState": [Function], 528 | "state": Object { 529 | "touched": false, 530 | "value": "", 531 | }, 532 | "validity": "valid", 533 | "value": "", 534 | }, 535 | }, 536 | "setState": [Function], 537 | "state": Object { 538 | "thing": Object { 539 | "touched": false, 540 | "value": "", 541 | }, 542 | }, 543 | "validity": "valid", 544 | "value": Object { 545 | "thing": "", 546 | }, 547 | }, 548 | }, 549 | "setState": [Function], 550 | "state": Object { 551 | "something": Object { 552 | "thing": Object { 553 | "touched": false, 554 | "value": "", 555 | }, 556 | }, 557 | }, 558 | "validity": "valid", 559 | "value": Object { 560 | "something": Object { 561 | "thing": "", 562 | }, 563 | }, 564 | }, 565 | ], 566 | "setState": [Function], 567 | "state": Array [ 568 | Object { 569 | "something": Object { 570 | "thing": Object { 571 | "touched": false, 572 | "value": "", 573 | }, 574 | }, 575 | }, 576 | ], 577 | "validity": "valid", 578 | "value": Array [ 579 | Object { 580 | "something": Object { 581 | "thing": "", 582 | }, 583 | }, 584 | ], 585 | } 586 | `); 587 | calls = []; 588 | act(() => { 589 | form!.setState([ 590 | { 591 | something: { 592 | thing: { 593 | touched: form!.state[0].something.thing.touched, 594 | value: "nope", 595 | }, 596 | }, 597 | }, 598 | ]); 599 | }); 600 | expect(form.validity).toBe("invalid"); 601 | expect(form.elements[0].fields.something.fields.thing.error).toBe( 602 | "nope inner" 603 | ); 604 | expect(form).toMatchInlineSnapshot(` 605 | Object { 606 | "_field": Object { 607 | "element": Object { 608 | "fields": Object { 609 | "something": Object { 610 | "fields": Object { 611 | "thing": Object { 612 | "initialValue": [Function], 613 | "kind": "scalar", 614 | "props": [Function], 615 | "stateFromChange": undefined, 616 | "validate": [Function], 617 | }, 618 | }, 619 | "kind": "object", 620 | "stateFromChange": undefined, 621 | "validate": Object { 622 | "thing": [Function], 623 | }, 624 | }, 625 | }, 626 | "kind": "object", 627 | "stateFromChange": undefined, 628 | "validate": Object { 629 | "something": Object { 630 | "thing": [Function], 631 | }, 632 | }, 633 | }, 634 | "kind": "array", 635 | "stateFromChange": undefined, 636 | "validate": Object { 637 | "something": Object { 638 | "thing": [Function], 639 | }, 640 | }, 641 | }, 642 | "elements": Array [ 643 | Object { 644 | "_field": Object { 645 | "fields": Object { 646 | "something": Object { 647 | "fields": Object { 648 | "thing": Object { 649 | "initialValue": [Function], 650 | "kind": "scalar", 651 | "props": [Function], 652 | "stateFromChange": undefined, 653 | "validate": [Function], 654 | }, 655 | }, 656 | "kind": "object", 657 | "stateFromChange": undefined, 658 | "validate": Object { 659 | "thing": [Function], 660 | }, 661 | }, 662 | }, 663 | "kind": "object", 664 | "stateFromChange": undefined, 665 | "validate": Object { 666 | "something": Object { 667 | "thing": [Function], 668 | }, 669 | }, 670 | }, 671 | "fields": Object { 672 | "something": Object { 673 | "_field": Object { 674 | "fields": Object { 675 | "thing": Object { 676 | "initialValue": [Function], 677 | "kind": "scalar", 678 | "props": [Function], 679 | "stateFromChange": undefined, 680 | "validate": [Function], 681 | }, 682 | }, 683 | "kind": "object", 684 | "stateFromChange": undefined, 685 | "validate": Object { 686 | "thing": [Function], 687 | }, 688 | }, 689 | "fields": Object { 690 | "thing": Object { 691 | "_field": Object { 692 | "initialValue": [Function], 693 | "kind": "scalar", 694 | "props": [Function], 695 | "stateFromChange": undefined, 696 | "validate": [Function], 697 | }, 698 | "error": "nope inner", 699 | "props": Object { 700 | "error": "nope inner", 701 | "onBlur": [Function], 702 | "onChange": [Function], 703 | "onFocus": [Function], 704 | "touched": false, 705 | "validity": "invalid", 706 | "value": "nope", 707 | }, 708 | "setState": [Function], 709 | "state": Object { 710 | "touched": false, 711 | "value": "nope", 712 | }, 713 | "validity": "invalid", 714 | "value": "nope", 715 | }, 716 | }, 717 | "setState": [Function], 718 | "state": Object { 719 | "thing": Object { 720 | "touched": false, 721 | "value": "nope", 722 | }, 723 | }, 724 | "validity": "invalid", 725 | "value": Object { 726 | "thing": "nope", 727 | }, 728 | }, 729 | }, 730 | "setState": [Function], 731 | "state": Object { 732 | "something": Object { 733 | "thing": Object { 734 | "touched": false, 735 | "value": "nope", 736 | }, 737 | }, 738 | }, 739 | "validity": "invalid", 740 | "value": Object { 741 | "something": Object { 742 | "thing": "nope", 743 | }, 744 | }, 745 | }, 746 | ], 747 | "setState": [Function], 748 | "state": Array [ 749 | Object { 750 | "something": Object { 751 | "thing": Object { 752 | "touched": false, 753 | "value": "nope", 754 | }, 755 | }, 756 | }, 757 | ], 758 | "validity": "invalid", 759 | "value": Array [ 760 | Object { 761 | "something": Object { 762 | "thing": "nope", 763 | }, 764 | }, 765 | ], 766 | } 767 | `); 768 | act(() => { 769 | resetForm(form!, [{ something: { thing: "" } }]); 770 | }); 771 | expect(form.value).toMatchInlineSnapshot(` 772 | Array [ 773 | Object { 774 | "something": Object { 775 | "thing": "", 776 | }, 777 | }, 778 | ] 779 | `); 780 | act(() => { 781 | resetForm(form!, [{ something: { thing: "something" } }]); 782 | }); 783 | expect(form.value).toMatchInlineSnapshot(` 784 | Array [ 785 | Object { 786 | "something": Object { 787 | "thing": "something", 788 | }, 789 | }, 790 | ] 791 | `); 792 | }); 793 | 794 | test("setting array field to an empty array works", () => { 795 | let formSchema = array(text()); 796 | let form: Form | undefined; 797 | function Comp() { 798 | form = useForm(formSchema, ["blah"]); 799 | return null; 800 | } 801 | render(); 802 | if (!form) { 803 | throw new Error("form not rendered"); 804 | } 805 | act(() => { 806 | form!.setState([]); 807 | }); 808 | expect(form.value).toEqual([]); 809 | }); 810 | -------------------------------------------------------------------------------- /packages/react-next/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Form, InitialValueInput, Field, FormState, FormValue } from "./types"; 2 | import { ScalarValidationFn, ScalarValidationResult } from "./scalar"; 3 | import { useState } from "react"; 4 | import { mapObject } from "./map-obj"; 5 | import { getFieldInstance, getValueFromState } from "./utils"; 6 | export * from "./object"; 7 | export * from "./array"; 8 | export * from "./scalar"; 9 | 10 | export * from "./types"; 11 | 12 | export const validation = { 13 | valid(value: ValidValue) { 14 | return { validity: "valid", value } as const; 15 | }, 16 | invalid(error: string) { 17 | return { validity: "invalid", error } as const; 18 | }, 19 | }; 20 | 21 | function runValidationFunction( 22 | validationFn: ScalarValidationFn, 23 | value: Value 24 | ): ScalarValidationResult { 25 | let result = validationFn(value); 26 | if (result.validity === "valid") { 27 | return { 28 | validity: "valid", 29 | value: result.value, 30 | error: undefined, 31 | }; 32 | } 33 | return { 34 | validity: "invalid", 35 | value, 36 | error: result.error, 37 | }; 38 | } 39 | 40 | export function getInitialState( 41 | rootField: TField, 42 | ...initialValue: AllowEmptyIfUndefined> 43 | ): FormState { 44 | // @ts-ignore 45 | return getInitialValueFromField(rootField, initialValue[0]); 46 | } 47 | 48 | function getInitialValueFromField(field: Field, initialValue: any): any { 49 | if (field.kind === "object") { 50 | if (initialValue === undefined) { 51 | initialValue = {}; 52 | } 53 | return mapObject(field.fields, (key, value) => { 54 | return getInitialValueFromField(value, initialValue[key]); 55 | }); 56 | } 57 | if (field.kind === "array") { 58 | if (initialValue === undefined) { 59 | initialValue = []; 60 | } 61 | return (initialValue as any[]).map((element) => { 62 | return getInitialValueFromField(field.element, element); 63 | }); 64 | } 65 | return { 66 | touched: false, 67 | value: field.initialValue(initialValue), 68 | }; 69 | } 70 | 71 | function getNewState( 72 | field: Field, 73 | newState: any, 74 | prevState: undefined | Record 75 | ): any { 76 | if (field.kind === "scalar") { 77 | if (field.stateFromChange) { 78 | newState = field.stateFromChange(newState, prevState as any); 79 | } 80 | return newState; 81 | } 82 | if (field.kind === "array") { 83 | let hasAElementDifferent = newState.length !== prevState?.length; 84 | let newArrayState = (newState as any[]).map((newStateElement, i) => { 85 | let result = getNewState(field.element, newStateElement, prevState?.[i]); 86 | hasAElementDifferent = hasAElementDifferent || result !== prevState?.[i]; 87 | return result; 88 | }); 89 | if (field.stateFromChange) { 90 | let storedNewArrayState = newArrayState; 91 | newArrayState = field.stateFromChange( 92 | newArrayState, 93 | prevState as any 94 | ) as any; 95 | hasAElementDifferent = 96 | hasAElementDifferent || newArrayState !== storedNewArrayState; 97 | } 98 | if (hasAElementDifferent) { 99 | return newArrayState; 100 | } 101 | return prevState; 102 | } 103 | let newObjState: any = {}; 104 | // we don't want to have a new reference if we don't need to 105 | // TODO: maybe optimise the calling of getNewState 106 | let hasAFieldDifferent = false; 107 | mapObject(field.fields, (key, field) => { 108 | let result = getNewState(field, newState[key], prevState?.[key]); 109 | hasAFieldDifferent = hasAFieldDifferent || result !== prevState?.[key]; 110 | newObjState[key] = result; 111 | }); 112 | if (field.stateFromChange) { 113 | let storedNewObjState = newObjState; 114 | newObjState = field.stateFromChange(newObjState, prevState as any); 115 | hasAFieldDifferent = 116 | hasAFieldDifferent || newObjState !== storedNewObjState; 117 | } 118 | if (hasAFieldDifferent) { 119 | return newObjState; 120 | } 121 | return prevState; 122 | } 123 | 124 | type ObjectValidator = { [key: string]: Validator }; 125 | 126 | type Validator = ObjectValidator | Validator[] | ScalarValidationFn; 127 | 128 | function makeBaseValidator( 129 | field: TField, 130 | value: any 131 | ): Validator { 132 | if (field.kind === "scalar") { 133 | return field.validate; 134 | } 135 | if (field.kind === "array") { 136 | return (value as any[]).map((val) => makeBaseValidator(field.element, val)); 137 | } 138 | let validationObject: Record = {}; 139 | Object.keys(field.fields).forEach((key) => { 140 | validationObject[key] = makeBaseValidator(field.fields[key], value[key]); 141 | }); 142 | return validationObject; 143 | } 144 | 145 | function getValidationResults(field: Field, state: any) { 146 | const value = getValueFromState(field, state); 147 | let validator = makeBaseValidator(field, value); 148 | validator = recursivelyAddValidators(validator, field, value); 149 | return executeValidation(field, validator, state); 150 | } 151 | 152 | function executeValidation( 153 | field: Field, 154 | validator: Validator, 155 | state: any 156 | ): any { 157 | if (field.kind === "object") { 158 | let result: any = {}; 159 | Object.keys(field.fields).forEach((key) => { 160 | result[key] = executeValidation( 161 | field.fields[key], 162 | (validator as ObjectValidator)[key], 163 | state[key] 164 | ); 165 | }); 166 | return result; 167 | } 168 | if (field.kind === "array") { 169 | return (state as any[]).map((stateElement, i) => { 170 | return executeValidation( 171 | field.element, 172 | (validator as Validator[])[i], 173 | stateElement 174 | ); 175 | }); 176 | } 177 | 178 | return runValidationFunction((value) => { 179 | return (validator as any)(value); 180 | }, state.value); 181 | } 182 | 183 | function recursivelyAddValidators( 184 | validator: Validator, 185 | field: Field, 186 | value: any 187 | ): Validator { 188 | if (field.kind === "array") { 189 | validator = (validator as Validator[]).map((innerValidator, i) => 190 | recursivelyAddValidators(innerValidator, field.element, value[i]) 191 | ); 192 | } 193 | if (field.kind === "object") { 194 | Object.keys(field.fields).forEach((key) => { 195 | (validator as ObjectValidator)[key] = recursivelyAddValidators( 196 | (validator as ObjectValidator)[key], 197 | field.fields[key], 198 | value[key] 199 | ); 200 | }); 201 | } 202 | if (field.kind === "scalar") { 203 | return validator; 204 | } 205 | return addValidators(validator, field.validate as any, value); 206 | } 207 | 208 | function addValidators( 209 | inputValidator: Validator, 210 | validatorToAdd: any, 211 | value: any 212 | ): Validator { 213 | if ( 214 | typeof inputValidator === "function" && 215 | typeof validatorToAdd === "function" 216 | ) { 217 | return (val: any) => { 218 | const inner = inputValidator(val); 219 | if (inner.validity === "invalid") { 220 | return inner; 221 | } 222 | return value === undefined 223 | ? validatorToAdd(val) 224 | : validatorToAdd(val, value); 225 | }; 226 | } 227 | if (Array.isArray(inputValidator)) { 228 | return (inputValidator as Validator[]).map((inputValidator) => { 229 | return addValidators(inputValidator, validatorToAdd, value); 230 | }); 231 | } 232 | if ( 233 | typeof inputValidator === "object" && 234 | typeof validatorToAdd === "object" 235 | ) { 236 | Object.keys(validatorToAdd).forEach((key) => { 237 | const innerInputValidator = inputValidator[key]; 238 | const innerValidatorToAdd = validatorToAdd[key]; 239 | inputValidator[key] = addValidators( 240 | innerInputValidator, 241 | innerValidatorToAdd, 242 | value 243 | ); 244 | }); 245 | } 246 | return inputValidator; 247 | } 248 | 249 | export const resetForm: ( 250 | ...args: undefined extends InitialValueInput 251 | ? [Form] | [Form, InitialValueInput] 252 | : [Form, InitialValueInput] 253 | ) => void = function (form: Form, initialValue: any) { 254 | (form.setState as any)(getInitialValueFromField(form._field, initialValue)); 255 | } as any; 256 | 257 | type AllowEmptyIfUndefined = (undefined extends T ? [] : never) | [T]; 258 | 259 | export const useForm: ( 260 | rootField: TField, 261 | ...initialValue: AllowEmptyIfUndefined> 262 | ) => Form = function ( 263 | rootField: Field, 264 | initialValue: InitialValueInput 265 | ) { 266 | let [state, _setState] = useState>(() => 267 | getInitialValueFromField(rootField, initialValue) 268 | ); 269 | 270 | let setState = ( 271 | newStateDescription: (prevState: FormState) => FormState 272 | ) => { 273 | _setState((prevState) => { 274 | let newState = newStateDescription(prevState); 275 | return getNewState(rootField, newState, prevState); 276 | }); 277 | }; 278 | 279 | return getFieldInstance( 280 | rootField, 281 | state, 282 | setState, 283 | getValidationResults(rootField, state) 284 | ); 285 | } as any; 286 | -------------------------------------------------------------------------------- /packages/react-next/src/map-obj.ts: -------------------------------------------------------------------------------- 1 | type Mapper< 2 | SourceObjectType extends { readonly [key: string]: any }, 3 | MappedObjectValueType 4 | > = ( 5 | sourceKey: keyof SourceObjectType, 6 | sourceValue: SourceObjectType[keyof SourceObjectType] 7 | ) => MappedObjectValueType; 8 | 9 | export function mapObject< 10 | SourceObjectType extends { readonly [key: string]: any }, 11 | MappedObjectValueType 12 | >( 13 | source: SourceObjectType, 14 | mapper: Mapper 15 | ): { readonly [K in keyof SourceObjectType]: MappedObjectValueType } { 16 | let target: any = {}; 17 | for (const [key, value] of Object.entries(source)) { 18 | let newValue = mapper(key, value); 19 | 20 | target[key] = newValue; 21 | } 22 | 23 | return target; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-next/src/object.ts: -------------------------------------------------------------------------------- 1 | import { ScalarField, ValidatedFormValueFromScalarField } from "./scalar"; 2 | import { 3 | FormState, 4 | Field, 5 | FormValue, 6 | ValidatedFormValue, 7 | InitialValueInput, 8 | Form, 9 | } from "./types"; 10 | import { getValueFromState, getFieldInstance } from "./utils"; 11 | 12 | export type FormValueFromFieldsObj< 13 | Fields extends { readonly [Key in keyof Fields]: Field } 14 | > = { 15 | readonly [Key in keyof Fields]: FormValue; 16 | }; 17 | 18 | export type FormStateFromFieldsObj< 19 | Fields extends { readonly [Key in keyof Fields]: Field } 20 | > = { 21 | readonly [Key in keyof Fields]: FormState; 22 | }; 23 | 24 | export type ValidationFnInObjectValidation< 25 | Field extends ScalarField, 26 | ObjectValue 27 | > = ( 28 | value: ValidatedFormValueFromScalarField, 29 | objectValue: ObjectValue 30 | ) => 31 | | { 32 | readonly validity: "valid"; 33 | readonly value: ValidatedFormValueFromScalarField; 34 | } 35 | | { 36 | readonly validity: "invalid"; 37 | readonly error: string; 38 | }; 39 | 40 | export type ValidationObj = { 41 | readonly [Key in keyof FieldsObj]?: ValidationObjInner< 42 | FieldsObj[Key], 43 | ObjectValue 44 | >; 45 | }; 46 | 47 | type ValidationObjInner = Field extends ScalarField 48 | ? ValidationFnInObjectValidation 49 | : Field extends ObjectField 50 | ? ValidationObj 51 | : never; 52 | 53 | export type ValidatedFormValueFromFieldsObj< 54 | Fields extends { readonly [Key in keyof Fields]: Field } 55 | > = { 56 | readonly [Key in keyof Fields]: ValidatedFormValue; 57 | }; 58 | 59 | type ObjectFieldInitialInfoThing> = { 60 | [Key in keyof TObjectField["fields"]]: InitialValueInput< 61 | TObjectField["fields"][Key] 62 | >; 63 | }; 64 | 65 | type UndefinedKeys = { 66 | [Key in keyof Obj]: undefined extends Obj[Key] ? Key : never; 67 | }[keyof Obj]; 68 | 69 | type AllowUndefinedIfEmptyObject = {} extends Thing 70 | ? undefined | Thing 71 | : Thing; 72 | 73 | export type InitialValueFromObjectField< 74 | TObjectField extends ObjectField, 75 | Thing extends ObjectFieldInitialInfoThing< 76 | TObjectField 77 | > = ObjectFieldInitialInfoThing 78 | > = AllowUndefinedIfEmptyObject< 79 | Pick, UndefinedKeys> & Omit> 80 | >; 81 | 82 | export type ObjectFieldInstance> = ( 83 | | { 84 | readonly validity: "valid"; 85 | readonly value: ValidatedFormValueFromFieldsObj; 86 | } 87 | | { 88 | readonly validity: "invalid"; 89 | readonly value: FormValueFromFieldsObj; 90 | } 91 | ) & { 92 | setState( 93 | object: Partial> 94 | ): void; 95 | readonly state: FormStateFromFieldsObj; 96 | readonly fields: { 97 | readonly [Key in keyof TObjectField["fields"]]: Form< 98 | TObjectField["fields"][Key] 99 | >; 100 | }; 101 | readonly _field: TObjectField; 102 | }; 103 | 104 | export function getFieldValidity( 105 | field: Field, 106 | validationResult: any 107 | ): "valid" | "invalid" { 108 | if (field.kind === "scalar") { 109 | return validationResult.validity; 110 | } 111 | if (field.kind === "array") { 112 | return (validationResult as any[]).every((validationResultElement) => { 113 | return ( 114 | getFieldValidity(field.element, validationResultElement) === "valid" 115 | ); 116 | }) 117 | ? "valid" 118 | : "invalid"; 119 | } 120 | return Object.keys(field.fields).every( 121 | (key) => 122 | getFieldValidity(field.fields[key], validationResult[key]) === "valid" 123 | ) 124 | ? "valid" 125 | : "invalid"; 126 | } 127 | 128 | export function getObjectFieldInstance( 129 | field: ObjectField, 130 | state: FormStateFromFieldsObj, 131 | setState: ( 132 | state: ( 133 | prevState: FormStateFromFieldsObj 134 | ) => FormStateFromFieldsObj 135 | ) => void, 136 | validationResult: any 137 | ): ObjectFieldInstance { 138 | let fields: any = {}; 139 | Object.keys(field.fields).forEach((key) => { 140 | fields[key] = getFieldInstance( 141 | field.fields[key], 142 | state[key], 143 | (thing: any) => { 144 | setState((prevState) => { 145 | let newInnerState = thing(prevState[key]); 146 | return { 147 | ...prevState, 148 | [key]: newInnerState, 149 | }; 150 | }); 151 | }, 152 | validationResult[key] 153 | ); 154 | }); 155 | return { 156 | fields, 157 | state, 158 | setState: (partial) => { 159 | setState((prevState) => { 160 | let newState: any = { ...prevState }; 161 | Object.keys(partial).forEach((key) => { 162 | if (partial[key] !== undefined) { 163 | newState[key] = partial[key]; 164 | } 165 | }); 166 | return newState; 167 | }); 168 | }, 169 | validity: getFieldValidity(field, validationResult), 170 | value: getValueFromState(field, state), 171 | _field: field, 172 | }; 173 | } 174 | 175 | export type ObjectField< 176 | Fields extends { readonly [Key in keyof Fields]: Field } 177 | > = { 178 | readonly kind: "object"; 179 | readonly fields: Fields; 180 | readonly validate: 181 | | ValidationObj> 182 | | undefined; 183 | // this API is still def bad but meh 184 | readonly stateFromChange: 185 | | (( 186 | next: FormStateFromFieldsObj, 187 | current?: FormStateFromFieldsObj 188 | ) => FormStateFromFieldsObj) 189 | | undefined; 190 | }; 191 | 192 | export function object< 193 | Fields extends { readonly [Key in keyof Fields]: Field } 194 | >( 195 | fields: Fields, 196 | options?: { 197 | validate?: ValidationObj>; 198 | stateFromChange?: ( 199 | next: FormStateFromFieldsObj, 200 | current?: FormStateFromFieldsObj 201 | ) => FormStateFromFieldsObj; 202 | } 203 | ): ObjectField { 204 | return { 205 | kind: "object", 206 | fields, 207 | validate: options?.validate, 208 | stateFromChange: options?.stateFromChange, 209 | }; 210 | } 211 | -------------------------------------------------------------------------------- /packages/react-next/src/scalar.ts: -------------------------------------------------------------------------------- 1 | export type ScalarValidationFn = ( 2 | value: Value 3 | ) => 4 | | { readonly validity: "valid"; readonly value: ValidatedValue } 5 | | { readonly validity: "invalid"; readonly error: string }; 6 | 7 | export type ScalarValidationResult = 8 | | { 9 | readonly validity: "valid"; 10 | readonly value: ValidatedValue; 11 | readonly error?: string; 12 | } 13 | | { 14 | readonly validity: "invalid"; 15 | readonly error: string; 16 | readonly value: Value; 17 | }; 18 | 19 | type FormPropsInput = { 20 | readonly value: Value; 21 | readonly touched: boolean; 22 | readonly onBlur: () => void; 23 | readonly onFocus: () => void; 24 | readonly onChange: (value: Value) => void; 25 | } & ( 26 | | { 27 | readonly validity: "valid"; 28 | readonly error?: string; 29 | } 30 | | { readonly validity: "invalid"; readonly error: string } 31 | ); 32 | 33 | let emptyFn = () => {}; 34 | 35 | export function getScalarFieldInstance( 36 | field: TScalarField, 37 | state: ScalarState, 38 | setState: ( 39 | state: (prevState: ScalarState) => ScalarState 40 | ) => void, 41 | validationResult: ScalarValidationResult< 42 | FormValueFromScalarField, 43 | ValidatedFormValueFromScalarField 44 | > 45 | ): ScalarFieldInstance { 46 | return { 47 | ...validationResult, 48 | state, 49 | props: field.props({ 50 | onBlur: () => 51 | setState((prevState) => { 52 | if (prevState.touched) { 53 | return prevState; 54 | } 55 | return { 56 | value: prevState.value, 57 | touched: true, 58 | }; 59 | }), 60 | onChange: (value) => { 61 | setState((prevState) => { 62 | if (prevState.value === value) { 63 | return prevState; 64 | } 65 | return { 66 | value: value, 67 | touched: prevState.touched, 68 | }; 69 | }); 70 | }, 71 | onFocus: emptyFn, 72 | touched: state.touched, 73 | ...validationResult, 74 | }), 75 | setState: (stateUpdate) => { 76 | if (typeof stateUpdate === "function") { 77 | // @ts-ignore 78 | setState(stateUpdate); 79 | } else { 80 | setState(() => stateUpdate); 81 | } 82 | }, 83 | _field: field, 84 | }; 85 | } 86 | 87 | export type ScalarFieldInstance< 88 | Field extends ScalarField 89 | > = ScalarValidationResult< 90 | FormValueFromScalarField, 91 | ValidatedFormValueFromScalarField 92 | > & { 93 | readonly props: ReturnType; 94 | setState( 95 | state: 96 | | ScalarState> 97 | | (( 98 | prevState: ScalarState> 99 | ) => ScalarState>) 100 | ): void; 101 | readonly state: ScalarState>; 102 | readonly _field: Field; 103 | }; 104 | 105 | export type ScalarState = { value: Value; touched: boolean }; 106 | 107 | export type ScalarField< 108 | Value = any, 109 | InitialValue = any, 110 | TValidationFn extends ScalarValidationFn = ScalarValidationFn< 111 | Value, 112 | Value 113 | >, 114 | Props = any 115 | > = { 116 | kind: "scalar"; 117 | validate: TValidationFn; 118 | props: (input: FormPropsInput) => Props; 119 | stateFromChange?: ( 120 | next: ScalarState, 121 | current?: ScalarState 122 | ) => ScalarState; 123 | initialValue: (initialValue: InitialValue) => Value; 124 | }; 125 | 126 | type NonUndefinedScalarOptionsBase = { 127 | validate?: ScalarValidationFn; 128 | stateFromChange?: ( 129 | next: ScalarState, 130 | current?: ScalarState 131 | ) => ScalarState; 132 | }; 133 | 134 | type ScalarOptionsBase = 135 | | NonUndefinedScalarOptionsBase 136 | | undefined; 137 | 138 | type ValidationFnFromOptions< 139 | Value, 140 | Options extends ScalarOptionsBase 141 | > = Options extends NonUndefinedScalarOptionsBase 142 | ? Options["validate"] extends ScalarValidationFn 143 | ? Options["validate"] 144 | : ScalarValidationFn 145 | : ScalarValidationFn; 146 | 147 | export type ValidatedFormValueFromScalarField< 148 | Field extends ScalarField 149 | > = Field["validate"] extends ScalarValidationFn 150 | ? ValidatedValue 151 | : never; 152 | 153 | export type FormStateFromScalarField = { 154 | value: FormValueFromScalarField; 155 | touched: boolean; 156 | }; 157 | 158 | export type FormValueFromScalarField = ReturnType< 159 | Field["initialValue"] 160 | >; 161 | 162 | export function scalar(scalarFieldOptions: { 163 | props: (input: FormPropsInput) => Props; 164 | initialValue: (initialValueInput: InitialValueInput) => Value; 165 | }) { 166 | return function >( 167 | options?: Options 168 | ): ScalarField< 169 | Value, 170 | InitialValueInput, 171 | ValidationFnFromOptions, 172 | Props 173 | > { 174 | return { 175 | kind: "scalar", 176 | initialValue: scalarFieldOptions.initialValue, 177 | stateFromChange: options?.stateFromChange, 178 | props: scalarFieldOptions.props, 179 | // @ts-ignore 180 | validate: 181 | options?.validate || ((value) => ({ validity: "valid", value })), 182 | }; 183 | }; 184 | } 185 | 186 | export type InitialValueFromScalarField< 187 | TScalarField extends ScalarField 188 | > = Parameters[0]; 189 | -------------------------------------------------------------------------------- /packages/react-next/src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ScalarField, 3 | FormValueFromScalarField, 4 | FormStateFromScalarField, 5 | ScalarFieldInstance, 6 | ValidatedFormValueFromScalarField, 7 | InitialValueFromScalarField, 8 | } from "./scalar"; 9 | import { 10 | ObjectField, 11 | FormValueFromFieldsObj, 12 | FormStateFromFieldsObj, 13 | ObjectFieldInstance, 14 | ValidatedFormValueFromFieldsObj, 15 | InitialValueFromObjectField, 16 | } from "./object"; 17 | import { ArrayField, ArrayFieldInstance } from "./array"; 18 | 19 | export type FormValue = TField extends ScalarField 20 | ? FormValueFromScalarField 21 | : TField extends ObjectField 22 | ? FormValueFromFieldsObj 23 | : TField extends ArrayField 24 | ? readonly FormValue[] 25 | : never; 26 | 27 | export type Field = ScalarField | ObjectField | ArrayField; 28 | 29 | export type FormState = TField extends ScalarField 30 | ? FormStateFromScalarField 31 | : TField extends ObjectField 32 | ? FormStateFromFieldsObj 33 | : TField extends ArrayField 34 | ? readonly FormState[] 35 | : never; 36 | 37 | export type InitialValueInput = TField extends ScalarField 38 | ? InitialValueFromScalarField 39 | : TField extends ObjectField 40 | ? InitialValueFromObjectField 41 | : TField extends ArrayField 42 | ? readonly InitialValueInput[] | undefined 43 | : never; 44 | 45 | export type ValidatedFormValue< 46 | TField extends Field 47 | > = TField extends ScalarField 48 | ? ValidatedFormValueFromScalarField 49 | : TField extends ObjectField 50 | ? ValidatedFormValueFromFieldsObj 51 | : TField extends ArrayField 52 | ? readonly ValidatedFormValue[] | undefined 53 | : never; 54 | 55 | // maybe rename this to FormInstance 56 | export type Form = TField extends ScalarField 57 | ? ScalarFieldInstance 58 | : TField extends ObjectField 59 | ? ObjectFieldInstance 60 | : TField extends ArrayField 61 | ? ArrayFieldInstance 62 | : never; 63 | -------------------------------------------------------------------------------- /packages/react-next/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Field } from "./types"; 2 | import { getScalarFieldInstance } from "./scalar"; 3 | import { getObjectFieldInstance } from "./object"; 4 | import { getArrayFieldInstance } from "./array"; 5 | 6 | export function getValueFromState(field: Field, state: any): any { 7 | if (field.kind === "scalar") { 8 | return state.value; 9 | } 10 | if (field.kind === "array") { 11 | return (state as any[]).map((x) => getValueFromState(field.element, x)); 12 | } 13 | let obj: any = {}; 14 | Object.keys(field.fields).forEach((key) => { 15 | obj[key] = getValueFromState(field.fields[key], state[key]); 16 | }); 17 | return obj; 18 | } 19 | 20 | export function getFieldInstance( 21 | field: Field, 22 | state: any, 23 | setState: any, 24 | validationResult: any 25 | ) { 26 | if (field.kind === "scalar") { 27 | return getScalarFieldInstance(field, state, setState, validationResult); 28 | } 29 | if (field.kind === "array") { 30 | return getArrayFieldInstance(field, state, setState, validationResult); 31 | } 32 | return getObjectFieldInstance(field, state, setState, validationResult); 33 | } 34 | -------------------------------------------------------------------------------- /packages/react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @magical-forms/react 2 | 3 | ## 0.2.3 4 | 5 | ### Patch Changes 6 | 7 | - [`07565ca`](https://github.com/Thinkmill/magical-forms/commit/07565ca4ec3f31d0b39d28c6ad28a36378a09db5) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Actually fix the bug with stateFromChange 8 | 9 | ## 0.2.2 10 | 11 | ### Patch Changes 12 | 13 | - [`59390bc`](https://github.com/Thinkmill/magical-forms/commit/59390bcbf049af40d09e430012b56511e2a8fa03) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fix a bug 14 | 15 | ## 0.2.1 16 | 17 | ### Patch Changes 18 | 19 | - [`4f959d7`](https://github.com/Thinkmill/magical-forms/commit/4f959d7a56724b48f114cf855a4ac7488045d520) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fix a bug 20 | 21 | ## 0.2.0 22 | 23 | ### Minor Changes 24 | 25 | - [`361fb30`](https://github.com/Thinkmill/magical-forms/commit/361fb304c3441a604dfb218b2aac54c570c7e25d) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Add `stateFromChange` option 26 | 27 | ## 0.1.0 28 | 29 | ### Minor Changes 30 | 31 | - [`1cf6a3f`](https://github.com/Thinkmill/magical-forms/commit/1cf6a3f82d5a97d14ec3a8aaa2cc1e19103b2f41) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Make scalar and object fields introspectable 32 | 33 | ## 0.0.7 34 | 35 | ### Patch Changes 36 | 37 | - [`7d89aa1`](https://github.com/Thinkmill/magical-forms/commit/7d89aa1831034a62d59e8b41f1ba10f6a711e67e) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Expose meta to scalar fields 38 | 39 | ## 0.0.6 40 | 41 | ### Patch Changes 42 | 43 | - [`ad607d0`](https://github.com/Thinkmill/magical-forms/commit/ad607d0663aac99bd03dafa540aec6c2118f4d09) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fix incorrectly passing setValue to props 44 | 45 | ## 0.0.5 46 | 47 | ### Patch Changes 48 | 49 | - [`27aef9f`](https://github.com/Thinkmill/magical-forms/commit/27aef9ff19506ced1397e4e8a171ea544f59d1af) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fix the value type of scalar fields always being `string | undefined` 50 | 51 | ## 0.0.4 52 | 53 | ### Patch Changes 54 | 55 | - [`d74e38c`](https://github.com/Thinkmill/magical-forms/commit/d74e38c51cda2f1fb09b689d54aac7bd8c55853c) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Add a ts-ignore comment for typescript@3.9 56 | 57 | ## 0.0.3 58 | 59 | ### Patch Changes 60 | 61 | - [`d3200c7`](https://github.com/Thinkmill/magical-forms/commit/d3200c7a41f2ec5083338179073fe7b09c1b0c8e) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Add setValue to scalar fields 62 | 63 | ## 0.0.2 64 | 65 | ### Patch Changes 66 | 67 | - [`9066ba9`](https://github.com/Thinkmill/magical-forms/commit/9066ba9222effc6fd3c7841226e5a569b59d3d8b) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Fix object field 68 | 69 | ## 0.0.1 70 | 71 | ### Patch Changes 72 | 73 | - [`7d83975`](https://github.com/Thinkmill/magical-forms/commit/7d8397557cb5a545f1a338a0266673282bc150ff) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - New (experimental) API 74 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@magical-forms/react", 3 | "version": "0.2.3", 4 | "main": "dist/react.cjs.js", 5 | "module": "dist/react.esm.js", 6 | "dependencies": { 7 | "@babel/runtime": "^7.7.2" 8 | }, 9 | "peerDependencies": { 10 | "react": "^16.9.0 || 17" 11 | }, 12 | "devDependencies": { 13 | "react": "^17.0.2" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/react/src/array.ts: -------------------------------------------------------------------------------- 1 | // import { 2 | // Field, 3 | // FormValue, 4 | // InitialFieldValueInput, 5 | // Form, 6 | // ValidationResult, 7 | // ValidationFunctionToValidatedValue, 8 | // } from "./types"; 9 | // import { runValidationFunction, validation } from "./validation"; 10 | // import { 11 | // CompositeTypes, 12 | // OptionsBase, 13 | // ValidatedValueFromOptions, 14 | // ValidationErrorFromOptions, 15 | // } from "./composite-types"; 16 | 17 | // type ArrayFieldBase = Field; 18 | 19 | // type ArrayValue = FormValue< 20 | // InternalField 21 | // >[]; 22 | 23 | // type ArrayValidatedInternalValue< 24 | // InternalField extends ArrayFieldBase 25 | // > = ValidationFunctionToValidatedValue< 26 | // ArrayValue, 27 | // InternalField["validate"] 28 | // >[]; 29 | 30 | // type ArrayValidationResults = ReturnType< 31 | // InternalField["validate"] 32 | // >[]; 33 | 34 | // type ArrayCompositeTypes = CompositeTypes< 35 | // ArrayValue, 36 | // ArrayValidatedInternalValue, 37 | // ArrayValidationResults 38 | // >; 39 | 40 | // type ArrayField< 41 | // InternalField extends ArrayFieldBase, 42 | // ValidatedValue extends FormValue[], 43 | // ValidationError 44 | // > = Field< 45 | // ArrayValue, 46 | // // TODO: think about this some more 47 | // // I'm not sure if this is correct 48 | // InitialFieldValueInput[] | undefined, 49 | // { 50 | // readonly props: { 51 | // readonly value: ArrayValue; 52 | // readonly add: (value: FormValue) => void; 53 | // readonly remove: (index: number) => void; 54 | // }; 55 | // readonly items: Form[]; 56 | // } & ValidationResult< 57 | // ArrayValue, 58 | // ValidatedValue, 59 | // ValidationError 60 | // >, 61 | // { 62 | // items: ReturnType[]; 63 | // }, 64 | // ValidatedValue, 65 | // ValidationError 66 | // >; 67 | 68 | // export const array = < 69 | // InternalField extends ArrayFieldBase, 70 | // Options extends OptionsBase> 71 | // >( 72 | // internalField: InternalField, 73 | // options?: Options 74 | // ): ArrayField< 75 | // InternalField, 76 | // ValidatedValueFromOptions, Options>, 77 | // ValidationErrorFromOptions, Options> 78 | // > => { 79 | // return { 80 | // getField(input) { 81 | // return { 82 | // ...input, 83 | // props: { 84 | // value: input.value, 85 | // add(value) { 86 | // input.setValue( 87 | // (input.value as ArrayValue).concat([value]) 88 | // ); 89 | // }, 90 | // remove(index) { 91 | // let val = [...(input.value as ArrayValue)]; 92 | // val.splice(index, 1); 93 | // input.setValue(val); 94 | // }, 95 | // }, 96 | // items: (input.value as ArrayValue).map( 97 | // (internalValue, index) => { 98 | // return internalField.getField({ 99 | // ...runValidationFunction(internalField.validate, internalValue), 100 | // setValue(newInternalValue) { 101 | // let newVal = [...(input.value as ArrayValue)]; 102 | // newVal[index] = newInternalValue; 103 | // input.setValue(newVal); 104 | // }, 105 | // meta: input.meta.items[index], 106 | // setMeta: input.setMeta, 107 | // }); 108 | // } 109 | // ), 110 | // }; 111 | // }, 112 | // getInitialValue: (initialValueInput = []) => { 113 | // return initialValueInput.map((x) => internalField.getInitialValue(x)); 114 | // }, 115 | // getInitialMeta: (value) => ({ 116 | // items: value.map((x) => internalField.getInitialMeta(x)), 117 | // }), 118 | // validate: (value) => { 119 | // let innerResult = value.map((value) => 120 | // runValidationFunction(internalField.validate, value) 121 | // ); 122 | // let areAllFieldsValid = innerResult.every( 123 | // (value) => value.validity === "valid" 124 | // ); 125 | // if (options === undefined || options.validate === undefined) { 126 | // return areAllFieldsValid 127 | // ? validation.valid(value) 128 | // : validation.invalid(innerResult); 129 | // } 130 | // return options.validate( 131 | // // @ts-ignore 132 | // areAllFieldsValid 133 | // ? { 134 | // validity: "valid" as const, 135 | // value, 136 | // } 137 | // : { 138 | // validity: "invalid", 139 | // value, 140 | // error: innerResult, 141 | // } 142 | // ); 143 | // }, 144 | // }; 145 | // }; 146 | 147 | export {}; 148 | -------------------------------------------------------------------------------- /packages/react/src/composite-types.ts: -------------------------------------------------------------------------------- 1 | import { ValidationResult, ValidationFunctionToValidatedValue } from "./types"; 2 | 3 | export type CompositeTypes< 4 | Value, 5 | InteralValidatedValue extends Value = Value, 6 | InternalValidationResults = unknown, 7 | Meta = unknown 8 | > = { 9 | value: Value; 10 | internalValidated: InteralValidatedValue; 11 | internalValidationResults: InternalValidationResults; 12 | meta: Meta; 13 | }; 14 | 15 | export type ValidationFunctionToValidationError< 16 | SpecificCompositeTypes extends CompositeTypes, 17 | ValidationFunction extends ObjectValidationFn 18 | > = ValidationFunction extends ObjectValidationFn< 19 | SpecificCompositeTypes, 20 | SpecificCompositeTypes["value"], 21 | infer ValidationError 22 | > 23 | ? ValidationError 24 | : undefined; 25 | 26 | type ObjectValidationFn< 27 | SpecificCompositeTypes extends CompositeTypes, 28 | ValidatedValue extends SpecificCompositeTypes["value"] = SpecificCompositeTypes["value"], 29 | ValidationError = unknown 30 | > = ( 31 | value: PreviousResult 32 | ) => 33 | | { validity: "valid"; value: ValidatedValue } 34 | | { validity: "invalid"; error: ValidationError }; 35 | 36 | type DefaultObjectValidationFn< 37 | SpecificCompositeTypes extends CompositeTypes 38 | > = ObjectValidationFn< 39 | SpecificCompositeTypes, 40 | SpecificCompositeTypes["internalValidated"], 41 | SpecificCompositeTypes["internalValidationResults"] 42 | >; 43 | 44 | type ValidationOptionToValidationFn< 45 | SpecificCompositeTypes extends CompositeTypes, 46 | ValidationFunction extends 47 | | ObjectValidationFn 48 | | undefined 49 | > = [ValidationFunction] extends [ObjectValidationFn] 50 | ? ValidationFunction 51 | : DefaultObjectValidationFn; 52 | 53 | type ObjectOptionsToDefaultOptions< 54 | SpecificCompositeTypes extends CompositeTypes, 55 | Options extends OptionsBase 56 | > = { 57 | validate: [Options] extends [OptionsBaseNonNullable] 58 | ? ValidationOptionToValidationFn< 59 | SpecificCompositeTypes, 60 | Options["validate"] 61 | > 62 | : DefaultObjectValidationFn; 63 | }; 64 | 65 | type PreviousResult< 66 | SpecificCompositeTypes extends CompositeTypes 67 | > = ValidationResult< 68 | SpecificCompositeTypes["value"], 69 | SpecificCompositeTypes["internalValidated"], 70 | SpecificCompositeTypes["internalValidationResults"] 71 | >; 72 | 73 | export type OptionsBase< 74 | SpecificCompositeTypes extends CompositeTypes 75 | > = OptionsBaseNonNullable | undefined; 76 | type OptionsBaseNonNullable< 77 | SpecificCompositeTypes extends CompositeTypes 78 | > = { 79 | validate?: ObjectValidationFn; 80 | stateFromChange?: ( 81 | changedState: { 82 | value: SpecificCompositeTypes["value"]; 83 | meta: SpecificCompositeTypes["meta"]; 84 | }, 85 | currentState: { 86 | value: SpecificCompositeTypes["value"]; 87 | meta: SpecificCompositeTypes["meta"]; 88 | } 89 | ) => { 90 | value: SpecificCompositeTypes["value"]; 91 | meta: SpecificCompositeTypes["meta"]; 92 | }; 93 | }; 94 | 95 | export type ValidatedValueFromOptions< 96 | SpecificCompositeTypes extends CompositeTypes, 97 | Options extends OptionsBase 98 | > = ValidationFunctionToValidatedValue< 99 | SpecificCompositeTypes["value"], 100 | ObjectOptionsToDefaultOptions["validate"] 101 | >; 102 | 103 | export type ValidationErrorFromOptions< 104 | SpecificCompositeTypes extends CompositeTypes, 105 | Options extends OptionsBase 106 | > = ValidationFunctionToValidationError< 107 | SpecificCompositeTypes, 108 | ObjectOptionsToDefaultOptions["validate"] 109 | >; 110 | -------------------------------------------------------------------------------- /packages/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | export { useForm } from "./useForm"; 2 | export { validation } from "./validation"; 3 | export * from "./types"; 4 | import * as types from "./value-types"; 5 | 6 | export { types }; 7 | -------------------------------------------------------------------------------- /packages/react/src/map-obj.ts: -------------------------------------------------------------------------------- 1 | type Mapper< 2 | SourceObjectType extends { readonly [key: string]: any }, 3 | MappedObjectValueType 4 | > = ( 5 | sourceKey: keyof SourceObjectType, 6 | sourceValue: SourceObjectType[keyof SourceObjectType] 7 | ) => MappedObjectValueType; 8 | 9 | export function mapObject< 10 | SourceObjectType extends { readonly [key: string]: any }, 11 | MappedObjectValueType 12 | >( 13 | source: SourceObjectType, 14 | mapper: Mapper 15 | ): { readonly [K in keyof SourceObjectType]: MappedObjectValueType } { 16 | let target: any = {}; 17 | for (const [key, value] of Object.entries(source)) { 18 | let newValue = mapper(key, value); 19 | 20 | target[key] = newValue; 21 | } 22 | 23 | return target; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react/src/object.ts: -------------------------------------------------------------------------------- 1 | import { mapObject } from "./map-obj"; 2 | 3 | import { 4 | Field, 5 | InitialFieldValueInput, 6 | ValidationResult, 7 | ValidationFunctionToValidatedValue, 8 | FormValue, 9 | } from "./types"; 10 | import { runValidationFunction, validation } from "./validation"; 11 | import { 12 | OptionsBase, 13 | ValidatedValueFromOptions, 14 | ValidationErrorFromOptions, 15 | CompositeTypes, 16 | } from "./composite-types"; 17 | 18 | type ObjectFieldBase = { 19 | [key: string]: Field; 20 | }; 21 | 22 | type ObjectValue = { 23 | readonly [Key in keyof ObjectFieldMap]: FormValue; 24 | }; 25 | 26 | type ObjectValidatedInternalValue = { 27 | readonly [Key in keyof ObjectFieldMap]: ValidationFunctionToValidatedValue< 28 | ObjectValue, 29 | ObjectFieldMap[Key]["validate"] 30 | >; 31 | }; 32 | 33 | type ObjectValidationResults = { 34 | readonly [Key in keyof ObjectFieldMap]: ReturnType< 35 | ObjectFieldMap[Key]["validate"] 36 | >; 37 | }; 38 | 39 | type ObjectCompositeTypes< 40 | ObjectFieldMap extends ObjectFieldBase 41 | > = CompositeTypes< 42 | ObjectValue, 43 | ObjectValidatedInternalValue, 44 | ObjectValidationResults, 45 | { 46 | readonly fields: { 47 | readonly [Key in keyof ObjectFieldMap]: ReturnType< 48 | ObjectFieldMap[Key]["getInitialMeta"] 49 | >; 50 | }; 51 | } 52 | >; 53 | 54 | type ObjectFieldMapToField< 55 | ObjectFieldMap extends ObjectFieldBase, 56 | SpecificCompositeTypes extends ObjectCompositeTypes, 57 | ValidatedValue extends SpecificCompositeTypes["value"], 58 | ValidationError 59 | > = Field< 60 | SpecificCompositeTypes["value"], 61 | | { 62 | [Key in keyof ObjectFieldMap]?: InitialFieldValueInput< 63 | ObjectFieldMap[Key] 64 | >; 65 | } 66 | | undefined, 67 | { 68 | readonly fields: { 69 | readonly [Key in keyof ObjectFieldMap]: ReturnType< 70 | ObjectFieldMap[Key]["getField"] 71 | >; 72 | }; 73 | } & ValidationResult< 74 | ObjectValue, 75 | ValidatedValue, 76 | ValidationError 77 | >, 78 | SpecificCompositeTypes["meta"], 79 | ValidatedValue, 80 | ValidationError 81 | >; 82 | 83 | export function object< 84 | ObjectFieldMap extends ObjectFieldBase, 85 | Options extends OptionsBase> 86 | >( 87 | fields: ObjectFieldMap, 88 | options?: Options 89 | ): ObjectFieldMapToField< 90 | ObjectFieldMap, 91 | ObjectCompositeTypes, 92 | ValidatedValueFromOptions, Options>, 93 | ValidationErrorFromOptions, Options> 94 | > { 95 | let hasChildGetDerivedStateFromState = Object.values(fields).some( 96 | (x) => x.getDerivedStateFromState 97 | ); 98 | let getDerivedStateFromState: any; 99 | if (!hasChildGetDerivedStateFromState && options?.stateFromChange) { 100 | getDerivedStateFromState = options.stateFromChange; 101 | } else if (hasChildGetDerivedStateFromState) { 102 | getDerivedStateFromState = (changed: any, current: any) => { 103 | let value: any = {}; 104 | let meta: any = {}; 105 | Object.keys(fields).forEach((key) => { 106 | if (fields[key].getDerivedStateFromState) { 107 | // @ts-ignore 108 | let state = fields[key].getDerivedStateFromState( 109 | { value: changed.value[key], meta: changed.meta.fields[key] }, 110 | { value: current.value[key], meta: current.meta.fields[key] } 111 | ); 112 | value[key] = state.value; 113 | meta[key] = state.meta; 114 | } else { 115 | value[key] = changed.value[key]; 116 | meta[key] = changed.meta.fields[key]; 117 | } 118 | }); 119 | let state = { value, meta: { fields: meta } }; 120 | if (options?.stateFromChange) { 121 | state = options.stateFromChange(state, current); 122 | } 123 | return state; 124 | }; 125 | } 126 | 127 | return { 128 | // @ts-ignore 129 | type: "object", 130 | // @ts-ignore 131 | fields, 132 | getField(input) { 133 | return { 134 | ...input, 135 | fields: mapObject(fields, (sourceKey, sourceValue) => 136 | sourceValue.getField({ 137 | ...runValidationFunction( 138 | sourceValue.validate, 139 | input.value[sourceKey] 140 | ), 141 | setValue: (val: any) => { 142 | input.setValue({ ...input.value, [sourceKey]: val }); 143 | }, 144 | meta: input.meta.fields[sourceKey], 145 | setState: (val) => { 146 | input.setState({ 147 | value: { ...input.value, [sourceKey]: val.value }, 148 | meta: { 149 | fields: { ...input.meta.fields, [sourceKey]: val.meta }, 150 | }, 151 | }); 152 | }, 153 | }) 154 | ), 155 | }; 156 | }, 157 | getDerivedStateFromState, 158 | getInitialValue: (initialValue = {}) => 159 | mapObject(fields, (sourceKey, sourceValue) => 160 | sourceValue.getInitialValue(initialValue[sourceKey]) 161 | ), 162 | getInitialMeta: (value) => ({ 163 | fields: mapObject(fields, (sourceKey, sourceValue) => 164 | sourceValue.getInitialMeta(value[sourceKey]) 165 | ), 166 | }), 167 | // @ts-ignore 168 | validate: (value) => { 169 | let innerResult = mapObject(fields, (sourceKey, sourceValue) => 170 | runValidationFunction(sourceValue.validate, value[sourceKey]) 171 | ); 172 | let areAllFieldsValid = Object.values(innerResult).every( 173 | (value) => value.validity === "valid" 174 | ); 175 | if (options === undefined || options.validate === undefined) { 176 | return areAllFieldsValid 177 | ? validation.valid(value) 178 | : validation.invalid(innerResult); 179 | } 180 | return options.validate( 181 | // @ts-ignore 182 | areAllFieldsValid 183 | ? { 184 | validity: "valid" as const, 185 | value, 186 | } 187 | : { 188 | validity: "invalid", 189 | value, 190 | error: innerResult, 191 | } 192 | ); 193 | }, 194 | }; 195 | } 196 | -------------------------------------------------------------------------------- /packages/react/src/types.ts: -------------------------------------------------------------------------------- 1 | export type BasicFieldInput< 2 | FieldValue, 3 | Meta, 4 | ValidatedValue, 5 | ValidationError 6 | > = { 7 | setValue: (value: FieldValue) => void; 8 | 9 | setState: (state: { value: FieldValue; meta: Meta }) => void; 10 | meta: Meta; 11 | } & ValidationResult; 12 | 13 | export type Form< 14 | FormField extends Field 15 | > = FormField extends Field 16 | ? Input 17 | : never; 18 | 19 | export type ValidatedFormValue< 20 | FormField extends Field 21 | > = FormField extends Field 22 | ? ValidatedValue 23 | : never; 24 | 25 | export type FormValidationError< 26 | FormField extends Field 27 | > = FormField extends Field 28 | ? FormValidationError 29 | : never; 30 | 31 | export type ValidationFn< 32 | Value, 33 | ValidatedValue extends Value, 34 | ValidationError 35 | > = ( 36 | value: Value 37 | ) => 38 | | { readonly validity: "valid"; readonly value: ValidatedValue } 39 | | { readonly validity: "invalid"; readonly error: ValidationError }; 40 | 41 | export type FormValue< 42 | FormField extends Field 43 | > = ReturnType; 44 | 45 | export type InitialFieldValueInput< 46 | FormField extends Field 47 | > = Parameters[0]; 48 | 49 | export type InvalidValidationResult = { 50 | readonly validity: "invalid"; 51 | readonly value: Value; 52 | readonly error: ValidationError; 53 | }; 54 | 55 | export type ValidValidationResult = { 56 | readonly validity: "valid"; 57 | readonly value: ValidatedValue; 58 | readonly error: undefined; 59 | }; 60 | 61 | export type ValidationResult = 62 | | InvalidValidationResult 63 | | ValidValidationResult; 64 | 65 | export type Field< 66 | Value, 67 | InitialFieldValueInputType, 68 | Input extends ValidationResult, 69 | Meta, 70 | ValidatedValue extends Value, 71 | ValidationError 72 | > = { 73 | readonly getInitialValue: ( 74 | initialValueInput: InitialFieldValueInputType 75 | ) => Value; 76 | readonly getInitialMeta: (value: Value) => Meta; 77 | readonly getField: ( 78 | input: BasicFieldInput 79 | ) => Input; 80 | readonly validate: ValidationFn; 81 | readonly getDerivedStateFromState?: ( 82 | newState: { value: Value; meta: Meta }, 83 | oldState: { value: Value; meta: Meta } 84 | ) => { value: Value; meta: Meta }; 85 | }; 86 | 87 | export type BasicOptions = NonNullableBaseOptions | undefined; 88 | 89 | type NonNullableBaseOptions = { 90 | stateFromChange?: ( 91 | changedState: { value: Value; meta: { touched: boolean } }, 92 | currentState: { value: Value; meta: { touched: boolean } } 93 | ) => { value: Value; meta: { touched: boolean } }; 94 | validate?: ValidationFn | undefined; 95 | }; 96 | 97 | type ValidationOptionToValidationFn< 98 | Value, 99 | ValidationFunction extends ValidationFn | undefined 100 | > = [ValidationFunction] extends [ValidationFn] 101 | ? ValidationFunction 102 | : ValidationFn; 103 | 104 | export type OptionsToDefaultOptions> = [ 105 | Obj 106 | ] extends [NonNullableBaseOptions] 107 | ? { 108 | validate: ValidationOptionToValidationFn; 109 | } 110 | : { 111 | validate: ValidationFn; 112 | }; 113 | 114 | export type ValidationFunctionToValidatedValue< 115 | Value, 116 | ValidationFunction extends ( 117 | ...args: any 118 | ) => 119 | | { readonly validity: "valid"; readonly value: Value } 120 | | { readonly validity: "invalid"; readonly error: unknown } 121 | > = Extract, { validity: "valid" }>["value"]; 122 | 123 | export type ValidationFunctionToValidationError< 124 | Value, 125 | ValidationFunction extends ValidationFn 126 | > = ValidationFunction extends ValidationFn 127 | ? ValidationError 128 | : undefined; 129 | 130 | export type BasicField< 131 | Value, 132 | Props, 133 | Options, 134 | Meta = { touched: boolean }, 135 | InputType = Value | undefined 136 | > = Field< 137 | Value, 138 | InputType, 139 | ValidationResult< 140 | Value, 141 | ValidationFunctionToValidatedValue< 142 | Value, 143 | OptionsToDefaultOptions["validate"] 144 | >, 145 | ValidationFunctionToValidationError< 146 | Value, 147 | OptionsToDefaultOptions["validate"] 148 | > 149 | > & { 150 | props: Props; 151 | setValue: (value: Value) => void; 152 | }, 153 | Meta, 154 | ValidationFunctionToValidatedValue< 155 | Value, 156 | OptionsToDefaultOptions["validate"] 157 | >, 158 | ValidationFunctionToValidationError< 159 | Value, 160 | OptionsToDefaultOptions["validate"] 161 | > 162 | >; 163 | -------------------------------------------------------------------------------- /packages/react/src/useForm.ts: -------------------------------------------------------------------------------- 1 | import { InitialFieldValueInput, Field, Form } from "./types"; 2 | import { useState } from "react"; 3 | import { runValidationFunction } from "./validation"; 4 | 5 | export function useForm>( 6 | field: FormField, 7 | initialValue?: InitialFieldValueInput 8 | ): Form { 9 | let [state, setState] = useState(() => { 10 | let value = field.getInitialValue(initialValue); 11 | return { 12 | value, 13 | meta: field.getInitialMeta(value), 14 | }; 15 | }); 16 | 17 | return field.getField({ 18 | ...runValidationFunction(field.validate, state.value), 19 | setValue: (val) => { 20 | setState((prev) => 21 | field.getDerivedStateFromState 22 | ? field.getDerivedStateFromState( 23 | { value: val, meta: prev.meta }, 24 | prev 25 | ) 26 | : { value: val, meta: prev.meta } 27 | ); 28 | }, 29 | setState: (val) => { 30 | setState((prev) => 31 | field.getDerivedStateFromState 32 | ? field.getDerivedStateFromState(val, prev) 33 | : val 34 | ); 35 | }, 36 | meta: state.meta, 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /packages/react/src/validation.ts: -------------------------------------------------------------------------------- 1 | import { ValidationFn, ValidationResult, ValidValidationResult } from "./types"; 2 | 3 | export function runValidationFunction< 4 | Value, 5 | ValidationError, 6 | ValidatedValue extends Value 7 | >( 8 | validationFn: ValidationFn, 9 | value: Value 10 | ): ValidationResult { 11 | let result = validationFn(value); 12 | if (result.validity === "valid") { 13 | return { 14 | validity: "valid", 15 | value: result.value, 16 | error: undefined, 17 | } as const; 18 | } 19 | return { 20 | validity: "invalid", 21 | value, 22 | error: result.error, 23 | } as const; 24 | } 25 | 26 | export function getDefaultValidate(options: any) { 27 | return options !== undefined && options.validate !== undefined 28 | ? options.validate 29 | : (val: any) => validation.valid(val); 30 | } 31 | 32 | export const validation = { 33 | valid(value: ValidValue) { 34 | return { validity: "valid" as const, value } as const; 35 | }, 36 | invalid(error: Error) { 37 | return { validity: "invalid" as const, error } as const; 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/react/src/value-types.ts: -------------------------------------------------------------------------------- 1 | import { BasicOptions, BasicField } from "./types"; 2 | import { getDefaultValidate } from "./validation"; 3 | 4 | export { object } from "./object"; 5 | // export { array } from "./array"; 6 | 7 | export function scalar() { 8 | return (field: { 9 | props: ( 10 | field: { 11 | onFocus: () => void; 12 | onBlur: () => void; 13 | onChange: (value: ValueType) => void; 14 | value: ValueType; 15 | meta: { touched: boolean }; 16 | } & ( 17 | | { validity: "valid"; error?: ErrorType } 18 | | { validity: "invalid"; error: ErrorType } 19 | ) 20 | ) => Props; 21 | initialValue: (inputInitialValue: InputValueType | undefined) => ValueType; 22 | }) => { 23 | return >( 24 | options?: Options 25 | ): BasicField< 26 | ValueType, 27 | Props, 28 | Options, 29 | { touched: boolean }, 30 | InputValueType 31 | > => { 32 | return { 33 | // @ts-ignore 34 | type: "scalar", 35 | getField: ({ setState, setValue, meta, ...input }) => ({ 36 | ...input, 37 | setValue, 38 | // @ts-ignore 39 | props: field.props({ 40 | ...input, 41 | meta, 42 | onBlur: () => { 43 | setState({ value: input.value, meta: { touched: true } }); 44 | }, 45 | onFocus: () => {}, 46 | onChange: setValue, 47 | }), 48 | }), 49 | getInitialValue: field.initialValue, 50 | getInitialMeta: () => ({ touched: false }), 51 | validate: getDefaultValidate(options), 52 | getDerivedStateFromState: options?.stateFromChange, 53 | }; 54 | }; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /test-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /test-app/next.config.js: -------------------------------------------------------------------------------- 1 | const withPreconstruct = require("@preconstruct/next"); 2 | 3 | module.exports = withPreconstruct(); 4 | -------------------------------------------------------------------------------- /test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@magical-forms/test-app", 3 | "version": "1.0.2", 4 | "private": true, 5 | "main": "index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@preconstruct/next": "3.0.1", 9 | "next": "^12.0.9", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2" 12 | } 13 | } -------------------------------------------------------------------------------- /test-app/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | array, 4 | ArrayField, 5 | Field, 6 | Form, 7 | getInitialState, 8 | object, 9 | scalar, 10 | useForm, 11 | validation, 12 | } from "@magical-forms/react-next"; 13 | 14 | const text = scalar({ 15 | props: ({ onBlur, onChange, touched, error, value }) => { 16 | return { 17 | onChange, 18 | onBlur, 19 | value, 20 | error: touched ? error : undefined, 21 | }; 22 | }, 23 | initialValue: (input: string | undefined) => input || "", 24 | }); 25 | 26 | let i = 0; 27 | 28 | const key = scalar({ 29 | props: ({ value }) => value, 30 | initialValue: () => i++, 31 | }); 32 | 33 | const element = object({ 34 | key: key(), 35 | value: text({ 36 | validate(value) { 37 | if (!value.length) { 38 | return validation.invalid("Must not be empty"); 39 | } 40 | return validation.valid(value); 41 | }, 42 | }), 43 | }); 44 | 45 | const schema = array(element); 46 | 47 | export default function Index() { 48 | let form = useForm(schema, [{}]); 49 | return ( 50 |
51 |
    52 | {form.elements.map((element, i) => { 53 | return ( 54 |
  • 55 |
    56 | 57 | 66 |
    67 |
  • 68 | ); 69 | })} 70 |
71 | 78 | 85 |
86 | ); 87 | } 88 | 89 | function TextInput(props: { 90 | value: string; 91 | onChange(value: string): void; 92 | onBlur(): void; 93 | error?: string; 94 | }) { 95 | return ( 96 |
97 | { 100 | props.onBlur(); 101 | }} 102 | onChange={(event) => { 103 | props.onChange(event.target.value); 104 | }} 105 | /> 106 |
{props.error}
107 |
108 | ); 109 | } 110 | 111 | const a: Form> = undefined as any; 112 | 113 | const b: Form = a; 114 | 115 | console.log(b); 116 | -------------------------------------------------------------------------------- /test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "jsx": "preserve", 13 | "esModuleInterop": true, 14 | "incremental": false, 15 | "strict": true 16 | }, 17 | "exclude": ["node_modules"], 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | "noEmit": true /* Do not emit outputs. */, 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */, 24 | /* Strict Type-Checking Options */ 25 | "strict": true /* Enable all strict type-checking options. */, 26 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 33 | 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | 40 | /* Module Resolution Options */ 41 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 45 | // "typeRoots": [], /* List of folders to include type definitions from. */ 46 | // "types": [], /* Type declaration files to be included in compilation. */ 47 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 48 | // "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 50 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 51 | 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | "declaration": true, 62 | "declarationMap": true 63 | } 64 | } 65 | --------------------------------------------------------------------------------