├── .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 |
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 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | ```
62 |
63 | will renders into:
64 |
65 | ```html
66 |
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 |
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 |
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 |
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 |
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 |
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 |
33 | `;
34 |
35 | exports[`Field should render error correctly 1`] = `
36 |
63 | `;
64 |
65 | exports[`Form should render correctly 1`] = `
66 |
69 | `;
70 |
71 | exports[`Form should render correctly 2`] = `
72 |
77 | `;
78 |
79 | exports[`Form should render correctly 3`] = `
80 |
90 | `;
91 |
92 | exports[`Item should render correctly 1`] = `
93 |
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()
17 | expect(wrapper2).toMatchSnapshot()
18 |
19 | const wrapper3 = render(
20 |
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 |
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 |
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 |
--------------------------------------------------------------------------------