├── .babelrc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── README.zh-CN.md ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── form-field.tsx ├── form-item.tsx ├── form-options-context.ts ├── form-store-context.ts ├── form-store.ts ├── form.tsx ├── index.ts ├── style.less ├── use-field-change.ts ├── use-form-change.ts ├── use-form-store.ts └── utils.ts ├── stories ├── .babelrc ├── .storybook │ ├── addons.js │ ├── config.js │ └── webpack.config.js ├── package-lock.json ├── package.json ├── src │ ├── form-field-hooks.tsx │ ├── form-field.tsx │ └── form-item.tsx └── tsconfig.json ├── test ├── __snapshots__ │ └── Form.test.tsx.snap ├── form-store.test.tsx └── form.test.tsx ├── tsconfig.json └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "browsers": ["last 2 Chrome versions"] 8 | } 9 | } 10 | ], 11 | "@babel/preset-react", 12 | "@babel/preset-typescript" 13 | ], 14 | "plugins": ["@babel/plugin-proposal-class-properties"] 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | stories/build/ 2 | node_modules/ 3 | lib/ 4 | 5 | *.log 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "printWidth": 100, 5 | "tabWidth": 2, 6 | "semi": false, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 varHarrie 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 all 13 | 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `react-hero-form` 2 | 3 | > A full-featured form component. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install react-hero-form --save 9 | # or 10 | yarn add react-hero-form 11 | ``` 12 | 13 | ## Basic Usage 14 | 15 | Simply create a `FormStore` and pass into `Form` component. `value` and `onChange` of form controls (such as `input`) are unnecessary. 16 | 17 | ```javascript 18 | import { Form, FormStore } from "react-hero-form"; 19 | 20 | class App extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.store = new FormStore(); 24 | } 25 | 26 | onSubmit = e => { 27 | e.preventDefault(); 28 | 29 | const values = this.store.get(); 30 | console.log(values); 31 | }; 32 | 33 | render() { 34 | return ( 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | ); 44 | } 45 | } 46 | ``` 47 | 48 | ## Simple Field 49 | 50 | `Form.Item` is a simplified version of `Form.Field`, without any extra nodes. 51 | 52 | ```javascript 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | ``` 62 | 63 | will renders into: 64 | 65 | ```html 66 |
67 | 68 | 69 |
70 | ``` 71 | 72 | ## Default Values 73 | 74 | To set default values, you can pass an object as the first parameter. Use `reset()` to restore defaults at any time. 75 | 76 | ```javascript 77 | const store = new FormStore({ name: "Harry" }); 78 | // ... 79 | store.reset(); 80 | ``` 81 | 82 | ## Form Validation 83 | 84 | The second parameter is used to pass the form rules. 85 | 86 | Using `store.validate()` to check entire form, and returns a tuple with error message and form values. Or directly gets form values by `store.get()` without validation. 87 | 88 | ```javascript 89 | function assert(condition, message) { 90 | if (!condition) throw new Error(message) 91 | } 92 | 93 | const rules = { 94 | name: (val) => assert(!!val && !!val.trim(), "Name is required") 95 | }; 96 | 97 | const store = new FormStore({}, rules); 98 | // ... 99 | try { 100 | const values = await store.validate(); 101 | console.log('values:', values); 102 | } catch (error) { 103 | console.log('error:', error); 104 | } 105 | ``` 106 | 107 | ## APIs 108 | 109 | ### Form Props 110 | 111 | - `className` Form element class name, `optional`. 112 | - `store` Form store, `required`. 113 | - `inline` Inline layout for fields, default to `false`. 114 | - `compact` Hides error message, default to `false`. 115 | - `required` Displays star mark, does not include validation, default to `false`. 116 | - `labelWidth` Customized label width, `optional`. 117 | - `gutter` Customized distance between label and control, `optional`. 118 | - `errorClassName` Adds customized class name when field has error message, `optional`. 119 | - `onSubmit` Submit callback, `optional`. 120 | 121 | ### Form Field Props 122 | 123 | - `className` Field element class name, `optional`. 124 | - `label` Field label, `optional`. 125 | - `name` Field name, `optional`. 126 | - `valueProp` Value prop name of child component, default to `'value'`. 127 | - `valueGetter` The way to parse value from change event, `optional`. 128 | - `suffix` Suffix nodes, `optional`. 129 | 130 | ### Form Item Props 131 | 132 | - `className` Field element class name, `optional`. 133 | - `name` Field name, `optional`. 134 | - `valueProp` Value prop name of child component, default to `'value'`. 135 | - `valueGetter` The way to parse value from change event, `optional`. 136 | 137 | ### FormStore Methods 138 | 139 | - `new FormStore(defaultValues?, rules?)` Creates form store. 140 | - `store.get()` Returns entire form values. 141 | - `store.get(name)` Returns field value by name. 142 | - `store.set()` Sets entire form values. 143 | - `store.set(name, value)` Sets field value by name. 144 | - `store.set(name, value, true)` Sets field value by name and validate. 145 | - `store.reset()` Resets form with default values. 146 | - `store.validate()` Validates entire form and returns values. 147 | - `store.validate(name)` Validates field value by name and returns value. 148 | - `store.error()` Returns the all error messages. 149 | - `store.error(index)` Returns the nth error message. 150 | - `store.error(name)` Returns error message by name. 151 | - `store.error(name, message)` Sets error message by name. 152 | - `store.subscribe(listener)` Adds listener and returns unsubscribe callback. 153 | 154 | ### Hooks 155 | 156 | - `useFormStore(defaultValues?, rules?)` Creates form store with hooks. 157 | - `useFormChange(store, onChange)` Add form listener with hooks. 158 | - `useFieldChange(store, onChange)` Add field listener with hooks. 159 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # `react-hero-form` 2 | 3 | > 一个功能齐全的表单组件。 4 | 5 | ## 安装 6 | 7 | ```bash 8 | npm install react-hero-form --save 9 | # 或者 10 | yarn add react-hero-form 11 | ``` 12 | 13 | ## 基础用法 14 | 15 | 只需简单地创建一个`FormStore`实例并传递到`Form`组件上。对于表单组件(如`input`),无需再传递`value`和`onChange`了。 16 | 17 | ```javascript 18 | import { Form, FormStore } from "react-hero-form"; 19 | 20 | class App extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.store = new FormStore(); 24 | } 25 | 26 | onSubmit = e => { 27 | e.preventDefault(); 28 | 29 | const values = this.store.get(); 30 | console.log(values); 31 | }; 32 | 33 | render() { 34 | return ( 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | ); 44 | } 45 | } 46 | ``` 47 | 48 | ## 默认值 49 | 50 | 如要设置默认值,只需提供一个对象给第一个参数。并且你可以在任何时候通过`reset()`恢复到这个默认值。 51 | 52 | ```javascript 53 | const store = new FormStore({ name: "Harry" }); 54 | // ... 55 | store.reset(); 56 | ``` 57 | 58 | ## 表单校验 59 | 60 | 第二个参数用于设置校验规则,当校验函数抛出异常时,代表校验不通过。 61 | 62 | 使用`store.validate()`可以校验整个表单,并且返回一个包含错误信息和表单值的数组。 63 | 64 | ```javascript 65 | function assert(condition, message) { 66 | if (!condition) throw new Error(message) 67 | } 68 | 69 | const rules = { 70 | name: (val) => assert(!!val && !!val.trim(), "Name is required") 71 | }; 72 | 73 | const store = new FormStore({}, rules); 74 | // ... 75 | try { 76 | const values = await store.validate(); 77 | console.log('values:', values); 78 | } catch (error) { 79 | console.log('error:', error); 80 | } 81 | ``` 82 | 83 | ## APIs 84 | 85 | ### Form Props 86 | 87 | - `className` 表单元素类名,`可选`。 88 | - `store` 表单数据存储,`必须`。 89 | - `inline` 设置行内布局,默认值为`false`。 90 | - `compact` 是否隐藏错误信息,默认值为`false`。 91 | - `required` 是否显示星号,不包含表单校验,仅用于显示,默认值为`false`。 92 | - `labelWidth` 自定义标签宽度,`可选`。 93 | - `gutter` 自定义标签和表单组件间的距离,`可选`。 94 | - `errorClassName` 当有错误信息时,添加一个自定义类名,`可选`。 95 | - `onSubmit` 表单提交回调,`可选`。 96 | 97 | ### Form Field Props 98 | 99 | - `className` 表单域类名,`可选`。 100 | - `label` 表单域标签,`可选`。 101 | - `name` 表单域字段名,`可选`。 102 | - `valueProp` 填写到子组件的值属性名,默认值为`'value'`。 103 | - `valueGetter` 从表单事件中获取表单值的方式,`可选`。 104 | - `suffix` 后缀节点,`可选`。 105 | 106 | ### FormStore Methods 107 | 108 | - `new FormStore(defaultValues?, rules?)` 创建表单存储。 109 | - `store.get()` 返回整个表单的值。 110 | - `store.get(name)` 根据字段名返回表单域的值。 111 | - `store.set()` 设置整个表单的值。 112 | - `store.set(name, value)` 根据字段名设置表单域的值。 113 | - `store.set(name, value, true)` 根据字段名设置表单域的值,并校验。 114 | - `store.reset()` 重置表单。 115 | - `store.validate()` 校验整个表单,并返回错误信息和表单值。 116 | - `store.validate(name)` 根据字段名校验表单域的值,并返回错误信息和表单值。 117 | - `store.error()` 返回所有错误信息。 118 | - `store.error(index)` 返回第 index 条错误信息。 119 | - `store.error(name)` 根据字段名返回错误信息。 120 | - `store.error(name, message)` 根据字段名设置错误信息。 121 | - `store.subscribe(listener)` 订阅表单变动,并返回一个用于取消订阅的函数。 122 | 123 | ### Hooks 124 | 125 | - `useFormStore(defaultValues?, rules?)` 使用 hooks 创建 FormStore。 126 | - `useFormChange(store, onChange)` 使用 hooks 创建表单监听。 127 | - `useFieldChange(store, onChange)` 使用 hooks 创建表单域监听。 128 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | snapshotSerializers: ['enzyme-to-json/serializer'], 4 | testMatch: ['/test/**/*.test.{ts,tsx}'], 5 | globals: { 6 | 'ts-jest': { 7 | diagnostics: false 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hero-form", 3 | "version": "1.0.0-alpha.0", 4 | "description": "React component.", 5 | "keywords": [ 6 | "react", 7 | "component", 8 | "form" 9 | ], 10 | "author": "varHarrie ", 11 | "homepage": "https://github.com/varHarrie/react-hero-form#readme", 12 | "license": "MIT", 13 | "main": "lib/cjs/index.js", 14 | "module": "lib/esm/index.js", 15 | "unpack": "lib/umd/index.js", 16 | "types": "lib/index.d.ts", 17 | "directories": { 18 | "lib": "lib", 19 | "test": "__tests__" 20 | }, 21 | "files": [ 22 | "lib" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/varHarrie/react-hero-form.git" 30 | }, 31 | "scripts": { 32 | "build": "rollup -c && tsc --emitDeclarationOnly", 33 | "test": "jest" 34 | }, 35 | "husky": { 36 | "hooks": { 37 | "pre-commit": "lint-staged" 38 | } 39 | }, 40 | "lint-staged": { 41 | "{src,test,webpack}/**/*.{js,ts,tsx,json,css,md}": [ 42 | "prettier --config ./.prettierrc --write", 43 | "git add" 44 | ], 45 | "src/**/*.{ts,tsx}": [ 46 | "tslint --project ./tsconfig.json --fix", 47 | "git add" 48 | ] 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/varHarrie/react-hero-form/issues" 52 | }, 53 | "peerDependencies": { 54 | "react": ">=16.8.0" 55 | }, 56 | "devDependencies": { 57 | "@babel/plugin-proposal-class-properties": "^7.5.5", 58 | "@babel/preset-env": "^7.5.5", 59 | "@babel/preset-react": "^7.0.0", 60 | "@babel/preset-typescript": "^7.3.3", 61 | "@types/enzyme": "^3.10.3", 62 | "@types/enzyme-adapter-react-16": "^1.0.5", 63 | "@types/jest": "^24.0.18", 64 | "@types/react": "^16.8.20", 65 | "enzyme": "^3.10.0", 66 | "enzyme-adapter-react-16": "^1.14.0", 67 | "enzyme-to-json": "^3.4.0", 68 | "husky": "^3.0.4", 69 | "jest": "^24.9.0", 70 | "less": "^3.10.1", 71 | "lint-staged": "^9.2.3", 72 | "prettier": "^1.18.2", 73 | "react": "^16.8.6", 74 | "react-dom": "^16.8.6", 75 | "rollup": "^1.19.4", 76 | "rollup-plugin-babel": "^4.3.3", 77 | "rollup-plugin-commonjs": "^10.0.2", 78 | "rollup-plugin-node-resolve": "^5.2.0", 79 | "rollup-plugin-peer-deps-external": "^2.2.0", 80 | "rollup-plugin-postcss": "^2.0.3", 81 | "ts-jest": "^24.0.2", 82 | "tslint": "^5.18.0", 83 | "tslint-config-standard": "^8.0.1", 84 | "tslint-react": "^4.0.0", 85 | "typescript": "^3.8.3" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import external from 'rollup-plugin-peer-deps-external' 4 | import postcss from 'rollup-plugin-postcss' 5 | import resolve from 'rollup-plugin-node-resolve' 6 | import pkg from './package.json' 7 | 8 | export default [ 9 | { 10 | input: 'src/index.ts', 11 | output: { 12 | file: pkg.module, 13 | format: 'esm', 14 | sourcemap: true 15 | }, 16 | plugins: [ 17 | babel({ extensions: ['.js', '.ts', '.tsx'] }), 18 | resolve({ extensions: ['.js', '.ts', '.tsx'] }), 19 | external(), 20 | commonjs(), 21 | postcss() 22 | ] 23 | }, 24 | { 25 | input: 'src/index.ts', 26 | output: { 27 | file: pkg.main, 28 | format: 'cjs', 29 | sourcemap: true 30 | }, 31 | plugins: [ 32 | babel({ extensions: ['.js', '.ts', '.tsx'] }), 33 | resolve({ extensions: ['.js', '.ts', '.tsx'] }), 34 | external(), 35 | commonjs(), 36 | postcss() 37 | ] 38 | }, 39 | { 40 | input: 'src/index.ts', 41 | output: { 42 | file: pkg.unpack, 43 | format: 'umd', 44 | name: 'ReactHeroForm', 45 | sourcemap: true, 46 | globals: { 47 | react: 'React' 48 | } 49 | }, 50 | plugins: [ 51 | babel({ extensions: ['.js', '.ts', '.tsx'] }), 52 | resolve({ extensions: ['.js', '.ts', '.tsx'] }), 53 | external(), 54 | commonjs(), 55 | postcss() 56 | ] 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /src/form-field.tsx: -------------------------------------------------------------------------------- 1 | import React, { cloneElement, isValidElement, useCallback, useContext, useState } from 'react' 2 | 3 | import { FormStoreContext } from './form-store-context' 4 | import { useFieldChange } from './use-field-change' 5 | import { FormOptions, FormOptionsContext } from './form-options-context' 6 | import { getPropName, getValueFromEvent } from './utils' 7 | 8 | export interface FormFieldProps extends FormOptions { 9 | className?: string 10 | label?: string 11 | name?: string 12 | valueProp?: string | ((type: any) => string) 13 | valueGetter?: (...args: any[]) => any 14 | suffix?: React.ReactNode 15 | children?: React.ReactNode 16 | } 17 | 18 | export function FormField (props: FormFieldProps) { 19 | const { 20 | className, 21 | label, 22 | name, 23 | valueProp = 'value', 24 | valueGetter = getValueFromEvent, 25 | suffix, 26 | children, 27 | ...restProps 28 | } = props 29 | 30 | const store = useContext(FormStoreContext) 31 | const options = useContext(FormOptionsContext) 32 | const [value, setValue] = useState(name && store ? store.get(name) : undefined) 33 | const [error, setError] = useState(name && store ? store.error(name) : undefined) 34 | 35 | const onChange = useCallback( 36 | (...args: any[]) => name && store && store.set(name, valueGetter(...args), true), 37 | [name, store, valueGetter] 38 | ) 39 | 40 | useFieldChange(store, name, () => { 41 | setValue(store!.get(name!)) 42 | setError(store!.error(name!)) 43 | }) 44 | 45 | const { inline, compact, required, labelWidth, gutter, errorClassName = 'error' } = { 46 | ...options, 47 | ...restProps 48 | } 49 | 50 | let child: any = children 51 | 52 | if (name && store && isValidElement(child)) { 53 | const prop = getPropName(valueProp, child && child.type) 54 | 55 | let childClassName = (child.props && (child.props as any).className) || '' 56 | if (error) childClassName += ' ' + errorClassName 57 | 58 | const childProps = { className: childClassName, [prop]: value, onChange } 59 | child = cloneElement(child, childProps) 60 | } 61 | 62 | const classNames = [ 63 | classes.field, 64 | inline ? classes.inline : '', 65 | compact ? classes.compact : '', 66 | required ? classes.required : '', 67 | error ? classes.error : '', 68 | className ? className : '' 69 | ].join('') 70 | 71 | const headerStyle = { 72 | width: labelWidth, 73 | marginRight: gutter 74 | } 75 | 76 | return ( 77 |
78 | {label !== undefined && ( 79 |
80 | {label} 81 |
82 | )} 83 |
84 |
{child}
85 |
{error}
86 |
87 | {suffix !== undefined &&
{suffix}
} 88 |
89 | ) 90 | } 91 | 92 | const classes = { 93 | field: 'rh-form-field ', 94 | inline: 'rh-form-field--inline ', 95 | compact: 'rh-form-field--compact ', 96 | required: 'rh-form-field--required ', 97 | error: 'rh-form-field--error ', 98 | 99 | header: 'rh-form-field__header', 100 | container: 'rh-form-field__container', 101 | control: 'rh-form-field__control', 102 | message: 'rh-form-field__message', 103 | footer: 'rh-form-field__footer' 104 | } 105 | -------------------------------------------------------------------------------- /src/form-item.tsx: -------------------------------------------------------------------------------- 1 | import React, { cloneElement, isValidElement, useCallback, useContext, useState } from 'react' 2 | 3 | import { FormStoreContext } from './form-store-context' 4 | import { useFieldChange } from './use-field-change' 5 | import { getPropName, getValueFromEvent } from './utils' 6 | import { FormOptionsContext } from './form-options-context' 7 | 8 | export interface FormItemProps { 9 | name?: string 10 | valueProp?: string | ((type: any) => string) 11 | valueGetter?: (...args: any[]) => any 12 | children?: React.ReactNode 13 | } 14 | 15 | export function FormItem (props: FormItemProps) { 16 | const { name, valueProp = 'value', valueGetter = getValueFromEvent, children } = props 17 | 18 | const store = useContext(FormStoreContext) 19 | const options = useContext(FormOptionsContext) 20 | const [value, setValue] = useState(name && store ? store.get(name) : undefined) 21 | const [error, setError] = useState(name && store ? store.error(name) : undefined) 22 | 23 | const onChange = useCallback( 24 | (...args: any[]) => name && store && store.set(name, valueGetter(...args), true), 25 | [name, store, valueGetter] 26 | ) 27 | 28 | useFieldChange(store, name, () => { 29 | setValue(store!.get(name!)) 30 | setError(store!.error(name!)) 31 | }) 32 | 33 | let child: any = children 34 | 35 | if (name && store && isValidElement(child)) { 36 | const { errorClassName = 'error' } = options 37 | const prop = getPropName(valueProp, child && child.type) 38 | 39 | let className = (child.props && (child.props as any).className) || '' 40 | if (error) className += ' ' + errorClassName 41 | 42 | const childProps = { className, [prop]: value, onChange } 43 | child = cloneElement(child, childProps) 44 | } 45 | 46 | return child 47 | } 48 | -------------------------------------------------------------------------------- /src/form-options-context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface FormOptions { 4 | inline?: boolean 5 | compact?: boolean 6 | required?: boolean 7 | labelWidth?: number 8 | gutter?: number 9 | errorClassName?: string 10 | } 11 | 12 | export const FormOptionsContext = React.createContext({}) 13 | -------------------------------------------------------------------------------- /src/form-store-context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { FormStore } from './form-store' 4 | 5 | export const FormStoreContext = React.createContext(undefined) 6 | -------------------------------------------------------------------------------- /src/form-store.ts: -------------------------------------------------------------------------------- 1 | import { deepCopy, deepGet, deepSet } from './utils' 2 | 3 | export type FormListener = (name: string) => void 4 | 5 | export type FormValidator = (value: any, values: any) => void | Promise 6 | 7 | export type FormRules = { [key: string]: FormValidator } 8 | 9 | export type FormErrors = { [key: string]: string | undefined } 10 | 11 | export class FormStore { 12 | private initialValues: T 13 | 14 | private listeners: FormListener[] = [] 15 | 16 | private values: T 17 | 18 | private rules: FormRules 19 | 20 | private errors: FormErrors = {} 21 | 22 | public constructor (values: Partial = {}, rules: FormRules = {}) { 23 | this.initialValues = values as any 24 | this.values = deepCopy(values) as any 25 | this.rules = rules 26 | 27 | this.get = this.get.bind(this) 28 | this.set = this.set.bind(this) 29 | this.reset = this.reset.bind(this) 30 | this.error = this.error.bind(this) 31 | this.validate = this.validate.bind(this) 32 | this.subscribe = this.subscribe.bind(this) 33 | } 34 | 35 | private notify (name: string) { 36 | this.listeners.forEach((listener) => listener(name)) 37 | } 38 | 39 | public get (name?: string) { 40 | return name === undefined ? { ...this.values } : deepGet(this.values, name) 41 | } 42 | 43 | public async set (values: Partial): Promise 44 | public async set (name: string, value: any, validate?: boolean): Promise 45 | public async set (name: any, value?: any, validate?: boolean) { 46 | if (typeof name === 'string') { 47 | deepSet(this.values, name, value) 48 | this.notify(name) 49 | 50 | if (validate) { 51 | await this.validate(name).catch((err) => err) 52 | this.notify(name) 53 | } 54 | } else if (name) { 55 | await Promise.all(Object.keys(name).map((n) => this.set(n, name[n]))) 56 | } 57 | } 58 | 59 | public reset () { 60 | this.errors = {} 61 | this.values = deepCopy(this.initialValues) 62 | this.notify('*') 63 | } 64 | 65 | public error (): FormErrors 66 | public error (name: number | string): string | undefined 67 | public error (name: string, value: string | undefined): string | undefined 68 | public error (...args: any[]) { 69 | let [name, value] = args 70 | 71 | if (args.length === 0) return this.errors 72 | 73 | if (typeof name === 'number') { 74 | name = Object.keys(this.errors)[name] 75 | } 76 | 77 | if (args.length === 2) { 78 | if (value === undefined) { 79 | delete this.errors[name] 80 | } else { 81 | this.errors[name] = value 82 | } 83 | } 84 | 85 | return this.errors[name] 86 | } 87 | 88 | public async validate (): Promise 89 | public async validate (name: string): Promise 90 | public async validate (name?: string) { 91 | if (name === undefined) { 92 | let error: Error | undefined = undefined 93 | 94 | try { 95 | await Promise.all(Object.keys(this.rules).map((n) => this.validate(n))) 96 | } catch (err) { 97 | error = err 98 | } 99 | 100 | this.notify('*') 101 | if (error) throw error 102 | 103 | return this.get() 104 | } else { 105 | const validator = this.rules[name] 106 | const value = this.get(name) 107 | let error: Error | undefined = undefined 108 | 109 | if (validator) { 110 | try { 111 | await validator(value, this.values) 112 | } catch (err) { 113 | error = err 114 | } 115 | } 116 | 117 | this.error(name, error && error.message) 118 | if (error) throw error 119 | 120 | return value 121 | } 122 | } 123 | 124 | public subscribe (listener: FormListener) { 125 | this.listeners.push(listener) 126 | 127 | return () => { 128 | const index = this.listeners.indexOf(listener) 129 | if (index > -1) this.listeners.splice(index, 1) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/form.tsx: -------------------------------------------------------------------------------- 1 | import './style.less' 2 | 3 | import React from 'react' 4 | 5 | import { FormField } from './form-field' 6 | import { FormItem } from './form-item' 7 | import { FormStore } from './form-store' 8 | import { FormStoreContext } from './form-store-context' 9 | import { FormOptions, FormOptionsContext } from './form-options-context' 10 | 11 | export interface FormProps extends FormOptions { 12 | className?: string 13 | store: FormStore 14 | children?: React.ReactNode 15 | onSubmit?: (e: React.FormEvent) => void 16 | onReset?: (e: React.FormEvent) => void 17 | } 18 | 19 | export function Form (props: FormProps) { 20 | const { className = '', children, store, onSubmit, onReset, ...options } = props 21 | 22 | const classNames = 'rh-form ' + className 23 | 24 | return ( 25 | 26 | 27 |
28 | {children} 29 |
30 |
31 |
32 | ) 33 | } 34 | 35 | Form.Field = FormField 36 | 37 | Form.Item = FormItem 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './form' 2 | export * from './form-field' 3 | export * from './form-item' 4 | export * from './form-store' 5 | export * from './use-form-store' 6 | export * from './use-field-change' 7 | export * from './use-form-change' 8 | -------------------------------------------------------------------------------- /src/style.less: -------------------------------------------------------------------------------- 1 | .rh-form-field { 2 | margin-bottom: 20px; 3 | display: flex; 4 | align-items: flex-start; 5 | min-height: 32px; 6 | 7 | &__header { 8 | margin-right: 20px; 9 | display: flex; 10 | justify-content: flex-end; 11 | align-items: center; 12 | font-size: 14px; 13 | text-align: right; 14 | height: 32px; 15 | width: 120px; 16 | 17 | &::before { 18 | display: none; 19 | content: '*'; 20 | color: red; 21 | } 22 | } 23 | 24 | &__container { 25 | position: relative; 26 | flex: 1; 27 | margin-top: auto; 28 | margin-bottom: auto; 29 | } 30 | 31 | &__message { 32 | position: absolute; 33 | top: 100%; 34 | left: 0; 35 | font-size: 12px; 36 | color: red; 37 | } 38 | 39 | &__footer { 40 | display: flex; 41 | justify-content: flex-end; 42 | align-items: center; 43 | height: 32px; 44 | margin-left: 10px; 45 | font-size: 14px; 46 | } 47 | 48 | & & { 49 | margin-bottom: 0; 50 | } 51 | 52 | &.rh-form-field--inline { 53 | display: inline-flex; 54 | } 55 | 56 | &.rh-form-field--compact { 57 | margin-bottom: 0; 58 | } 59 | 60 | &.rh-form-field--compact &__message { 61 | display: none; 62 | } 63 | 64 | &.rh-form-field--required &__header::before { 65 | display: inline; 66 | content: '*'; 67 | color: red; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/use-field-change.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | import { FormStore } from './form-store' 4 | 5 | export function useFieldChange ( 6 | store: FormStore | undefined, 7 | name: string | undefined, 8 | onChange: (name: string) => void 9 | ) { 10 | useEffect(() => { 11 | if (!name || !store) return 12 | 13 | return store.subscribe((n) => { 14 | if (name === '*' || n === name || n === '*') { 15 | onChange(name) 16 | } 17 | }) 18 | }, [name, store]) 19 | } 20 | -------------------------------------------------------------------------------- /src/use-form-change.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | import { FormStore } from './form-store' 4 | 5 | export function useFormChange ( 6 | store: FormStore | undefined, 7 | onChange: (name: string) => void 8 | ) { 9 | useEffect(() => { 10 | if (!store) return 11 | 12 | return store.subscribe((n) => { 13 | onChange(n) 14 | }) 15 | }, [store]) 16 | } 17 | -------------------------------------------------------------------------------- /src/use-form-store.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | import { FormRules, FormStore } from './form-store' 4 | 5 | export function useFormStore ( 6 | values: Partial = {}, 7 | rules: FormRules = {} 8 | ) { 9 | return useMemo(() => new FormStore(values, rules), []) 10 | } 11 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function isObject (obj: any) { 2 | return obj !== null && typeof obj === 'object' 3 | } 4 | 5 | export function deepGet (obj: any, path: string) { 6 | const parts = path.split('.') 7 | const length = parts.length 8 | 9 | for (let i = 0; i < length; i++) { 10 | if (!isObject(obj)) return undefined 11 | obj = obj[parts[i]] 12 | } 13 | 14 | return obj 15 | } 16 | 17 | export function deepSet (obj: any, path: string, value: any) { 18 | if (!isObject(obj)) return obj 19 | 20 | const root = obj 21 | const parts = path.split('.') 22 | const length = parts.length 23 | 24 | for (let i = 0; i < length; i++) { 25 | const p = parts[i] 26 | 27 | if (i === length - 1) { 28 | obj[p] = value 29 | } else if (!isObject(obj[p])) { 30 | obj[p] = {} 31 | } 32 | 33 | obj = obj[p] 34 | } 35 | 36 | return root 37 | } 38 | 39 | export function deepCopy (target: T): T { 40 | const type = typeof target 41 | 42 | if (target === null || type === 'boolean' || type === 'number' || type === 'string') { 43 | return target 44 | } 45 | 46 | if (target instanceof Date) { 47 | return new Date(target.getTime()) as any 48 | } 49 | 50 | if (Array.isArray(target)) { 51 | return target.map((o) => deepCopy(o)) as any 52 | } 53 | 54 | if (typeof target === 'object') { 55 | const obj: any = {} 56 | 57 | for (let key in target) { 58 | obj[key] = deepCopy(target[key]) 59 | } 60 | 61 | return obj 62 | } 63 | 64 | return undefined as any 65 | } 66 | 67 | export function getPropName (valueProp: string | ((type: any) => string), type: any) { 68 | return typeof valueProp === 'function' ? valueProp(type) : valueProp 69 | } 70 | 71 | export function getValueFromEvent (...args: any[]) { 72 | const e = args[0] as React.ChangeEvent 73 | return e && e.target ? (e.target.type === 'checkbox' ? e.target.checked : e.target.value) : e 74 | } 75 | -------------------------------------------------------------------------------- /stories/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.babelrc" 3 | } 4 | -------------------------------------------------------------------------------- /stories/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register' 2 | -------------------------------------------------------------------------------- /stories/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addDecorator, configure } from '@storybook/react' 2 | import { withKnobs } from '@storybook/addon-knobs' 3 | 4 | const req = require.context('../src', true, /.tsx$/) 5 | 6 | function loadStories() { 7 | req.keys().forEach(req) 8 | } 9 | 10 | addDecorator(withKnobs) 11 | configure(loadStories, module) 12 | -------------------------------------------------------------------------------- /stories/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | exclude: /node_modules/, 5 | loader: require.resolve('babel-loader') 6 | }) 7 | 8 | config.resolve.extensions.push('.js', '.ts', '.tsx') 9 | config.resolve.symlinks = false 10 | 11 | return config 12 | } 13 | -------------------------------------------------------------------------------- /stories/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hero-form-stories", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "start-storybook -c .storybook -p 9001", 8 | "build": "build-storybook -c .storybook -o build" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/react": "^16.9.2", 15 | "@types/react-dom": "^16.9.0", 16 | "react": "^16.9.0", 17 | "react-dom": "^16.9.0", 18 | "react-hero-form": "file:.." 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.5.5", 22 | "@storybook/addon-knobs": "^5.1.11", 23 | "@storybook/react": "^5.1.11", 24 | "@types/storybook__addon-knobs": "^5.0.3", 25 | "@types/storybook__react": "^4.0.2", 26 | "babel-loader": "^8.0.6" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /stories/src/form-field-hooks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { boolean, number } from '@storybook/addon-knobs' 4 | import { Form, useFormChange, useFormStore } from 'react-hero-form' 5 | 6 | function assert (condition: any, message?: string) { 7 | if (!condition) throw new Error(message) 8 | } 9 | 10 | function App () { 11 | const store = useFormStore( 12 | { 13 | username: 'Default', 14 | password: '', 15 | gender: 'male', 16 | contact: { 17 | phone: '', 18 | address: '' 19 | } 20 | }, 21 | { 22 | username: (val) => assert(!!val.trim(), 'Name is required'), 23 | password: (val) => assert(!!val.trim(), 'Password is required'), 24 | 'contact.phone': (val) => assert(/[0-9]{11}/.test(val), 'Phone is invalid'), 25 | 'contact.address': (val) => assert(!!val.trim(), 'Address is required') 26 | } 27 | ) 28 | 29 | useFormChange(store, (name) => { 30 | console.log('change', name, store.get(name)) 31 | }) 32 | 33 | const onReset = React.useCallback((e: React.MouseEvent) => { 34 | e.preventDefault() 35 | store.reset() 36 | }, []) 37 | 38 | const onSubmit = React.useCallback(async (e: React.MouseEvent) => { 39 | e.preventDefault() 40 | 41 | try { 42 | const values = await store.validate() 43 | console.log('values:', values) 44 | } catch (error) { 45 | console.log('error:', error) 46 | } 47 | }, []) 48 | 49 | return ( 50 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | ) 82 | } 83 | 84 | storiesOf('Form', module).add('fields with hooks', () => { 85 | return 86 | }) 87 | -------------------------------------------------------------------------------- /stories/src/form-field.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { boolean, number } from '@storybook/addon-knobs' 4 | import { Form, FormStore } from 'react-hero-form' 5 | 6 | function assert (condition: any, message?: string) { 7 | if (!condition) throw new Error(message) 8 | } 9 | 10 | storiesOf('Form', module).add('fields', () => { 11 | const store = new FormStore( 12 | { 13 | username: 'Default', 14 | password: '', 15 | gender: 'male', 16 | contact: { 17 | phone: '', 18 | address: '' 19 | } 20 | }, 21 | { 22 | username: (val) => assert(!!val.trim(), 'Name is required'), 23 | password: (val) => assert(!!val.trim(), 'Password is required'), 24 | 'contact.phone': (val) => assert(/[0-9]{11}/.test(val), 'Phone is invalid'), 25 | 'contact.address': (val) => assert(!!val.trim(), 'Address is required') 26 | } 27 | ) 28 | 29 | store.subscribe((name) => { 30 | console.log('change', name, store.get(name)) 31 | }) 32 | 33 | const onReset = (e: React.MouseEvent) => { 34 | e.preventDefault() 35 | store.reset() 36 | } 37 | 38 | const onSubmit = async (e: React.MouseEvent) => { 39 | e.preventDefault() 40 | 41 | try { 42 | const values = await store.validate() 43 | console.log('values:', values) 44 | } catch (error) { 45 | console.log('error:', error) 46 | } 47 | } 48 | 49 | return ( 50 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | ) 82 | }) 83 | -------------------------------------------------------------------------------- /stories/src/form-item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { Form, FormStore } from 'react-hero-form' 4 | 5 | function assert (condition: any, message?: string) { 6 | if (!condition) throw new Error(message) 7 | } 8 | 9 | storiesOf('Form', module).add('items', () => { 10 | const store = new FormStore( 11 | { 12 | username: 'Default', 13 | password: '', 14 | gender: 'male', 15 | contact: { 16 | phone: '', 17 | address: '' 18 | } 19 | }, 20 | { 21 | username: (val) => assert(!!val.trim(), 'Name is required'), 22 | password: (val) => assert(!!val.trim(), 'Password is required'), 23 | 'contact.phone': (val) => assert(/[0-9]{11}/.test(val), 'Phone is invalid'), 24 | 'contact.address': async (val) => { 25 | return new Promise((resolve, reject) => { 26 | setTimeout(() => { 27 | if (val.trim()) { 28 | resolve() 29 | } else { 30 | reject(new Error('Address is required')) 31 | } 32 | }, 1000) 33 | }) 34 | } 35 | } 36 | ) 37 | 38 | store.subscribe((name) => { 39 | console.log('change', name, store.get(name)) 40 | }) 41 | 42 | const onReset = (e: React.FormEvent) => { 43 | e.preventDefault() 44 | store.reset() 45 | } 46 | 47 | const onSubmit = async (e: React.FormEvent) => { 48 | e.preventDefault() 49 | 50 | try { 51 | const values = await store.validate() 52 | console.log('values:', values) 53 | } catch (error) { 54 | console.log('error:', error) 55 | } 56 | } 57 | 58 | return ( 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 | ) 84 | }) 85 | -------------------------------------------------------------------------------- /stories/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "sourceRoot": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/__snapshots__/Form.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Field should render correctly 1`] = ` 4 |
7 |
11 | Label 12 |
13 |
16 |
19 | 22 |
23 |
26 |
27 | 32 |
33 | `; 34 | 35 | exports[`Field should render error correctly 1`] = ` 36 |
39 |
40 |
43 |
46 | Label 47 |
48 |
51 |
54 |
57 | Error Message 58 |
59 |
60 |
61 |
62 | 63 | `; 64 | 65 | exports[`Form should render correctly 1`] = ` 66 |
69 | `; 70 | 71 | exports[`Form should render correctly 2`] = ` 72 | 75 | Hello World 76 |
77 | `; 78 | 79 | exports[`Form should render correctly 3`] = ` 80 |
83 |
84 | Hello 85 |
86 |
87 | World 88 |
89 |
90 | `; 91 | 92 | exports[`Item should render correctly 1`] = ` 93 |
96 | 100 |
101 | `; 102 | -------------------------------------------------------------------------------- /test/form-store.test.tsx: -------------------------------------------------------------------------------- 1 | import 'jest' 2 | 3 | import { FormStore } from '..' 4 | 5 | function assert(condition, message) { 6 | if (!condition) throw new Error(message) 7 | } 8 | 9 | describe('FormStore', () => { 10 | it('set', () => { 11 | const store = new FormStore() 12 | 13 | store.set('key', 'value') 14 | expect(store.get('key')).toBe('value') 15 | 16 | store.set({ a: 1, b: 2 }) 17 | expect(store.get('a')).toBe(1) 18 | expect(store.get('b')).toBe(2) 19 | 20 | store.set('this.is.a.deep.key', 'value') 21 | expect(store.get()).toHaveProperty('this.is.a.deep.key', 'value') 22 | expect(store.get('this.is.a.deep.key')).toBe('value') 23 | }) 24 | 25 | it('reset', () => { 26 | const store = new FormStore({ default: 'value' }) 27 | 28 | store.set('default', 'modifiedValue') 29 | store.set('new', 'value') 30 | store.reset() 31 | 32 | expect(store.get()).toEqual({ default: 'value' }) 33 | }) 34 | 35 | it('error', () => { 36 | const store = new FormStore() 37 | 38 | store.error('key0', 'error0') 39 | store.error('key1', 'error1') 40 | store.error('deep.key', 'error2') 41 | 42 | expect(store.error('key1')).toBe('error1') 43 | expect(store.error('deep.key')).toBe('error2') 44 | expect(store.error(0)).toBe('error0') 45 | expect(store.error()).toEqual({ key0: 'error0', key1: 'error1', 'deep.key': 'error2' }) 46 | }) 47 | 48 | it('validate', async () => { 49 | const store = new FormStore( 50 | { 51 | username: '', 52 | password: '', 53 | contacts: { 54 | phone: '123456', 55 | email: 'email' 56 | } 57 | }, 58 | { 59 | username: (val) => assert(val.length > 0, 'Username is required'), 60 | password: (val) => 61 | assert(val.length >= 6 && val.length <= 18, 'Password length is invalid'), 62 | 'contacts.email': (email) => assert(email.includes('@'), 'Email is invalid') 63 | } 64 | ) 65 | 66 | await store.set('username', 'Harrie', true) 67 | await store.set('password', '123', true) 68 | 69 | expect(store.error('username')).toBe(undefined) 70 | expect(store.error('password')).toBe('Password length is invalid') 71 | 72 | const error = await store.validate('password').catch((err) => err) 73 | 74 | expect(error).toBeInstanceOf(Error) 75 | expect(error.message).toBe('Password length is invalid') 76 | 77 | await store.set('password', '123456') 78 | const error2 = await store.validate().catch((err) => err) 79 | const values = store.get() 80 | 81 | expect(error2).toBeInstanceOf(Error) 82 | expect(error2.message).toBe('Email is invalid') 83 | expect(values).toEqual({ 84 | username: 'Harrie', 85 | password: '123456', 86 | contacts: { phone: '123456', email: 'email' } 87 | }) 88 | }) 89 | 90 | it('subscribe', () => { 91 | const store = new FormStore() 92 | 93 | store.subscribe((name) => { 94 | expect(name).toBe('username') 95 | expect(store.get(name)).toBe('Harrie') 96 | }) 97 | 98 | store.set('username', 'Harrie') 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /test/form.test.tsx: -------------------------------------------------------------------------------- 1 | import 'jest' 2 | 3 | import * as React from 'react' 4 | import * as Adapter from 'enzyme-adapter-react-16' 5 | import { configure, render } from 'enzyme' 6 | 7 | import { Form, FormField, FormItem, FormStore } from '..' 8 | 9 | configure({ adapter: new Adapter() }) 10 | 11 | describe('Form', () => { 12 | it('should render correctly', () => { 13 | const wrapper = render(
) 14 | expect(wrapper).toMatchSnapshot() 15 | 16 | const wrapper2 = render(Hello World
) 17 | expect(wrapper2).toMatchSnapshot() 18 | 19 | const wrapper3 = render( 20 |
21 |
Hello
22 |
World
23 |
24 | ) 25 | expect(wrapper3).toMatchSnapshot() 26 | }) 27 | }) 28 | 29 | describe('Field', () => { 30 | it('should render correctly', () => { 31 | const wrapper = render( 32 | 33 | 34 | 35 | ) 36 | expect(wrapper).toMatchSnapshot() 37 | }) 38 | 39 | it('should render error correctly', () => { 40 | const store = new FormStore() 41 | store.error('name', 'Error Message') 42 | 43 | const wrapper = render( 44 |
45 |
46 | 47 |
48 |
49 | ) 50 | expect(wrapper).toMatchSnapshot() 51 | }) 52 | }) 53 | 54 | describe('Item', () => { 55 | it('should render correctly', () => { 56 | const store = new FormStore() 57 | store.error('name', 'Error Message') 58 | 59 | const wrapper = render( 60 |
61 | 62 | 63 | 64 |
65 | ) 66 | expect(wrapper).toMatchSnapshot() 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "declarationDir": "lib", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es2015"], 8 | "sourceMap": true, 9 | "allowJs": false, 10 | "jsx": "react", 11 | "declaration": true, 12 | "moduleResolution": "node", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "allowSyntheticDefaultImports": true, 22 | "experimentalDecorators": true, 23 | "emitDecoratorMetadata": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": ["tslint-config-standard", "tslint-react"], 4 | "rules": { 5 | "align": [true, "parameters", "statements"], 6 | "indent": [true, "spaces", 2], 7 | "interface-name": [true, "never-prefix"], 8 | "jsx-boolean-value": [true, "never"], 9 | "jsx-no-lambda": false, 10 | "jsx-no-multiline-js": false, 11 | "jsx-no-string-ref": true, 12 | "max-classes-per-file": true, 13 | "max-line-length": [true, 100], 14 | "member-ordering": [true, "static-before-instance"], 15 | "member-access": [true, "check-accessor", "check-constructor", "check-parameter-property"], 16 | "no-any": false, 17 | "no-bitwise": false, 18 | "no-console": false, 19 | "no-construct": true, 20 | "no-debugger": true, 21 | "no-empty-interface": false, 22 | "no-floating-promises": false, 23 | "no-shadowed-variable": true, 24 | "no-string-literal": true, 25 | "padded-blocks": [true, { "classes": "always" }], 26 | "switch-default": true 27 | } 28 | } 29 | --------------------------------------------------------------------------------