├── .gitignore ├── .prettierrc ├── .yarnrc ├── LICENSE.md ├── README.md ├── lerna.json ├── package.json ├── packages ├── define-form │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.d.ts │ │ └── index.js └── react-define-form │ ├── README.md │ ├── package.json │ └── src │ ├── ExtractType.tsx │ ├── FieldProps.tsx │ ├── FormProps.tsx │ ├── FormSpyProps.tsx │ ├── RenderableProps.tsx │ ├── createField.tsx │ ├── index.tsx │ └── test.tsx ├── postinstall.js ├── prebuild.js ├── prepare.js ├── tsconfig.base.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | 13 | # Compiled binary addons (http://nodejs.org/api/addons.html) 14 | build/Release 15 | 16 | # Dependency directory 17 | node_modules 18 | 19 | # Users Environment Variables 20 | .lock-wscript 21 | 22 | # build output 23 | 24 | packages/*/build 25 | packages/*/lib 26 | packages/*/LICENSE.md 27 | packages/*/tsconfig.build.json 28 | packages/*/tsconfig.json 29 | packages/*/.cache 30 | 31 | .DS_Store 32 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | workspaces-experimental true -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2017 [Forbes Lindesay](https://github.com/ForbesLindesay) 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | > SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Define Form 2 | 3 | A way to create strongly typed forms using [final-form](https://github.com/final-form/final-form). 4 | 5 | See define-form and react-define-form in packages for more info. 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "npmClient": "yarn", 7 | "useWorkspaces": true, 8 | "version": "independent", 9 | "registry": "https://registry.npmjs.org/" 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "scripts": { 7 | "postinstall": "node postinstall", 8 | "bootstrap": "lerna bootstrap", 9 | "build": "git add . && lerna run prepublish --since master", 10 | "build:all": "lerna run prepublish", 11 | "prerelease": "yarn build:all", 12 | "prettier": "prettier --write \"packages/*/src/**/*.{ts,tsx}\"", 13 | "release": "lerna publish", 14 | "test": "jest && lerna run test --parallel", 15 | "watch:jest": "jest --watch", 16 | "clean": "rimraf packages/*/lib && rimraf packages/*/build && rimraf packages/*/node_modules && rimraf node_modules" 17 | }, 18 | "devDependencies": { 19 | "@moped/babel-preset": "^0.0.5", 20 | "@types/jest": "^21.1.5", 21 | "@types/mkdirp": "^0.5.1", 22 | "@types/node": "^8.0.33", 23 | "@types/react": "*", 24 | "@types/react-dom": "*", 25 | "@types/rimraf": "^2.0.2", 26 | "babel-core": "^6.26.0", 27 | "jest": "^21.2.1", 28 | "lerna": "^2.11.0", 29 | "lsr": "^2.0.0", 30 | "prettier": "^1.8.2", 31 | "rimraf": "^2.6.2", 32 | "ts-jest": "^21.1.4", 33 | "typescript": "^2.9.2" 34 | }, 35 | "jest": { 36 | "transformIgnorePatterns": [ 37 | ".*(node_modules)(?!.*define-form.*).*$" 38 | ], 39 | "transform": { 40 | "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js" 41 | }, 42 | "testRegex": "/__tests__/.+\\.test\\.(tsx?)$", 43 | "moduleFileExtensions": [ 44 | "ts", 45 | "tsx", 46 | "js", 47 | "jsx", 48 | "json" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/define-form/README.md: -------------------------------------------------------------------------------- 1 | # Define Form 2 | 3 | Define form offers alternative typescript bindings for [final-form](https://github.com/final-form/final-form). The key difference is that the form data is now a strongly typed object, rather than an any. This makes the `initialValues` config option required. 4 | 5 | ## Installation 6 | 7 | ``` 8 | yarn add define-form 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```typescript 14 | import defineForm from 'define-form'; 15 | 16 | interface FormState { 17 | name: string; 18 | } 19 | type ErrorValue = string; 20 | const form = defineForm({ 21 | initialValues: {name: ''}, // required 22 | onSubmit, // required 23 | validate 24 | }) 25 | 26 | // Subscribe to form state updates 27 | const unsubscribe = form.subscribe( 28 | formState => { 29 | // Update UI 30 | }, 31 | { // FormSubscription: the list of values you want to be updated about 32 | dirty: true, 33 | valid: true, 34 | values: true 35 | } 36 | }) 37 | 38 | // Subscribe to field state updates 39 | const unregisterField = form.registerField( 40 | 'username', 41 | fieldState => { 42 | // Update field UI 43 | const { blur, change, focus, ...rest } = fieldState 44 | // In addition to the values you subscribe to, field state also 45 | // includes functions that your inputs need to update their state. 46 | }, 47 | { // FieldSubscription: the list of values you want to be updated about 48 | active: true, 49 | dirty: true, 50 | touched: true, 51 | valid: true, 52 | value: true 53 | } 54 | ) 55 | 56 | // Submit 57 | form.submit() // only submits if all validation passes 58 | ``` 59 | 60 | If you like this, you may also want to check out [react-define-form](https://www.npmjs.com/package/react-define-form) 61 | 62 | ## License 63 | 64 | MIT 65 | -------------------------------------------------------------------------------- /packages/define-form/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "define-form", 3 | "version": "2.0.1", 4 | "description": "", 5 | "main": "./src/index.js", 6 | "types": "./src/index.d.ts", 7 | "dependencies": { 8 | "@types/node": "^8.0.33", 9 | "final-form": "^4.2.0" 10 | }, 11 | "repository": "https://github.com/ForbesLindesay/define-form/tree/master/packages/define-form", 12 | "bugs": "https://github.com/ForbesLindesay/define-form/issues", 13 | "license": "GPL-3.0", 14 | "publishConfig": { 15 | "access": "public" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/define-form/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface AnyObject { 2 | [key: string]: any; 3 | } 4 | export type Errors = { 5 | readonly [P in keyof FormData]?: ErrorValue 6 | }; 7 | 8 | export interface Subscription { 9 | [key: string]: boolean | undefined; 10 | } 11 | export type Subscriber = (value: V) => void; 12 | export type IsEqual = (a: V, b: V) => boolean; 13 | 14 | export interface FormSubscription extends Subscription { 15 | active?: boolean; 16 | dirty?: boolean; 17 | dirtySinceLastSubmit?: boolean; 18 | error?: boolean; 19 | errors?: boolean; 20 | initialValues?: boolean; 21 | invalid?: boolean; 22 | pristine?: boolean; 23 | submitError?: boolean; 24 | submitErrors?: boolean; 25 | submitFailed?: boolean; 26 | submitSucceeded?: boolean; 27 | submitting?: boolean; 28 | valid?: boolean; 29 | validating?: boolean; 30 | values?: boolean; 31 | } 32 | 33 | export interface FormState { 34 | // by default: all values are subscribed. if subscription is specified, some values may be undefined 35 | active: undefined | string; 36 | dirty: boolean; 37 | dirtySinceLastSubmit: boolean; 38 | error: ErrorValue; 39 | errors: Errors; 40 | initialValues: FormData; 41 | invalid: boolean; 42 | pristine: boolean; 43 | submitError: ErrorValue; 44 | submitErrors: Errors; 45 | submitFailed: boolean; 46 | submitSucceeded: boolean; 47 | submitting: boolean; 48 | valid: boolean; 49 | validating: boolean; 50 | values: FormData; 51 | } 52 | 53 | export type FormSubscriber< 54 | FormData extends AnyObject, 55 | ErrorValue = any 56 | > = Subscriber>; 57 | 58 | export interface FieldState { 59 | active?: boolean; 60 | blur: () => void; 61 | change: (value: FieldValue) => void; 62 | data?: object; 63 | dirty?: boolean; 64 | dirtySinceLastSubmit?: boolean; 65 | error?: ErrorValue; 66 | focus: () => void; 67 | initial?: FieldValue; 68 | invalid?: boolean; 69 | length?: number; 70 | name: string; 71 | pristine?: boolean; 72 | submitError?: ErrorValue; 73 | submitFailed?: boolean; 74 | submitSucceeded?: boolean; 75 | touched?: boolean; 76 | valid?: boolean; 77 | value?: FieldValue; 78 | visited?: boolean; 79 | } 80 | 81 | export interface FieldSubscription extends Subscription { 82 | active: boolean; 83 | data: boolean; 84 | dirty: boolean; 85 | dirtySinceLastSubmit: boolean; 86 | error: boolean; 87 | initial: boolean; 88 | invalid: boolean; 89 | length: boolean; 90 | pristine: boolean; 91 | submitError: boolean; 92 | submitFailed: boolean; 93 | submitSucceeded: boolean; 94 | touched: boolean; 95 | valid: boolean; 96 | value: boolean; 97 | visited: boolean; 98 | } 99 | 100 | export type FieldSubscriber = Subscriber< 101 | FieldState 102 | >; 103 | 104 | export type Unsubscribe = () => void; 105 | 106 | type FieldValidator = ( 107 | value: FieldValue, 108 | allValues: FormData, 109 | ) => ErrorValue | Promise; 110 | type GetFieldValidator< 111 | FieldValue, 112 | FormData, 113 | ErrorValue = any 114 | > = () => FieldValidator; 115 | 116 | export interface FieldConfig { 117 | isEqual?: IsEqual; 118 | getValidator?: GetFieldValidator; 119 | validateFields?: string[]; 120 | } 121 | 122 | export type RegisterField = ( 123 | name: string, 124 | subscriber: FieldSubscriber, 125 | subscription: FieldSubscription, 126 | config: FieldConfig, 127 | ) => Unsubscribe; 128 | 129 | export interface InternalFieldState { 130 | active: boolean; 131 | blur: () => void; 132 | change: (value: FieldValue) => void; 133 | data: object; 134 | error?: ErrorValue; 135 | focus: () => void; 136 | isEqual: IsEqual; 137 | lastFieldState?: FieldState; 138 | length?: any; 139 | name: string; 140 | submitError?: ErrorValue; 141 | pristine: boolean; 142 | touched: boolean; 143 | validateFields?: string[]; 144 | validators: { 145 | [index: number]: GetFieldValidator; 146 | }; 147 | valid: boolean; 148 | visited: boolean; 149 | } 150 | 151 | export interface InternalFormState { 152 | active?: string; 153 | dirtySinceLastSubmit: boolean; 154 | error?: ErrorValue; 155 | errors: Errors; 156 | initialValues: FormData; 157 | lastSubmittedValues?: FormData; 158 | pristine: boolean; 159 | submitError?: ErrorValue; 160 | submitErrors?: Errors; 161 | submitFailed: boolean; 162 | submitSucceeded: boolean; 163 | submitting: boolean; 164 | valid: boolean; 165 | validating: number; 166 | values: FormData; 167 | } 168 | 169 | export interface FormApi { 170 | batch: (fn: () => void) => void; 171 | blur: (name: string) => void; 172 | change: (name: string, value?: any) => void; 173 | focus: (name: string) => void; 174 | initialize: (values: FormData) => void; 175 | getRegisteredFields: () => string[]; 176 | getState: () => FormState; 177 | mutators?: {[key: string]: Function}; 178 | submit: () => Promise | undefined> | undefined; 179 | subscribe: ( 180 | subscriber: FormSubscriber, 181 | subscription: FormSubscription, 182 | ) => Unsubscribe; 183 | registerField: RegisterField; 184 | reset: () => void; 185 | } 186 | 187 | export type DebugFunction = ( 188 | state: FormState, 189 | fieldStates: {[key in keyof FormData]: FieldState}, 190 | ) => void; 191 | 192 | export interface MutableState { 193 | formState: InternalFormState; 194 | fields: { 195 | [key: string]: { 196 | [key in keyof FormData]: InternalFieldState< 197 | FormData[key], 198 | FormData, 199 | ErrorValue 200 | > 201 | }; 202 | }; 203 | } 204 | 205 | export type GetIn = (state: object, complexKey: string) => any; 206 | export type SetIn = (state: object, key: string, value: any) => object; 207 | export type ChangeValue = < 208 | Key extends keyof FormData, 209 | FormData, 210 | ErrorValue = any 211 | >( 212 | state: MutableState, 213 | name: Key, 214 | mutate: (value: FormData[Key]) => FormData[Key], 215 | ) => void; 216 | export interface Tools { 217 | changeValue: ChangeValue; 218 | getIn: GetIn; 219 | setIn: SetIn; 220 | shallowEqual: IsEqual; 221 | } 222 | 223 | export type Mutator = ( 224 | args: any[], 225 | state: MutableState, 226 | tools: Tools, 227 | ) => any; 228 | 229 | export interface Config { 230 | debug?: DebugFunction; 231 | initialValues: FormData; 232 | mutators?: {[key: string]: Mutator}; 233 | onSubmit: ( 234 | values: FormDataParsed, 235 | form: FormApi, 236 | ) => 237 | | Errors 238 | | Promise | undefined | void> 239 | | undefined 240 | | void; 241 | validate?: ( 242 | values: FormData, 243 | ) => 244 | | Errors 245 | | Promise | undefined>; 246 | validateOnBlur?: boolean; 247 | } 248 | 249 | export type Decorator = ( 250 | form: FormApi, 251 | ) => Unsubscribe; 252 | 253 | export function createForm( 254 | config: Config, 255 | ): FormApi; 256 | export const fieldSubscriptionItems: string[]; 257 | export const formSubscriptionItems: string[]; 258 | export const FORM_ERROR: any; 259 | export function getIn(state: object, complexKey: string): any; 260 | export function setIn(state: object, key: string, value: any): object; 261 | export const version: string; 262 | -------------------------------------------------------------------------------- /packages/define-form/src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('final-form'); 2 | -------------------------------------------------------------------------------- /packages/react-define-form/README.md: -------------------------------------------------------------------------------- 1 | # React Define Form 2 | 3 | React define form offers alternative typescript bindings for [react-final-form](https://github.com/final-form/react-final-form). It requires you to "define" a form type, specifying the type of the form data. 4 | 5 | ## Installation 6 | 7 | ``` 8 | yarn add react-define-form 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```typescript 14 | import * as React from 'react'; 15 | import defineForm from 'react-define-form'; 16 | 17 | const { Form, Fields } = defineForm(f => ({ 18 | name: f(), 19 | bio: f(), 20 | phone: f() 21 | })); 22 | 23 | const MyForm = () => ( 24 |
{ 27 | console.log(values); 28 | }} 29 | validate={values => { 30 | return {}; 31 | }} 32 | render={({ handleSubmit, pristine, invalid }) => ( 33 | 34 |

Simple Default Input

35 |
36 | 37 | 38 |
39 | 40 |

Render Function

41 | ( 43 |
44 | 45 |