├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── chromatic.yml │ └── production.yml ├── .gitignore ├── .storybook ├── main.js └── preview.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── jest.setup.ts ├── package.json ├── src ├── Form.tsx ├── FormConfigProvider.tsx ├── FormItem.tsx ├── createFormService.ts ├── index.test.tsx ├── index.tsx ├── stories │ ├── Browser.stories.tsx │ ├── MaterialUI.stories.tsx │ └── Usages.stories.tsx ├── useForm.ts ├── useFormConfig.ts ├── useFormItem.ts └── useWatch.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | webpack.config.js 3 | babel.config.js 4 | jest.config.js 5 | .eslintrc.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | jest: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:react/recommended', 10 | 'plugin:@typescript-eslint/eslint-recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:prettier/recommended', 13 | 'prettier', 14 | 'prettier/react', 15 | 'prettier/@typescript-eslint', 16 | ], 17 | globals: { 18 | Atomics: 'readonly', 19 | SharedArrayBuffer: 'readonly', 20 | }, 21 | parser: '@typescript-eslint/parser', 22 | parserOptions: { 23 | ecmaFeatures: { 24 | jsx: true, 25 | }, 26 | ecmaVersion: 2018, 27 | sourceType: 'module', 28 | warnOnUnsupportedTypeScriptVersion: true, 29 | }, 30 | plugins: ['react', 'simple-import-sort', '@typescript-eslint', 'react-hooks'], 31 | settings: { 32 | react: { 33 | version: 'detect', 34 | }, 35 | }, 36 | rules: { 37 | // eslint rules 38 | 'react/prop-types': 'off', 39 | '@typescript-eslint/explicit-function-return-type': 'off', 40 | 'lines-between-class-members': [ 41 | 'error', 42 | 'always', 43 | { exceptAfterSingleLine: true }, 44 | ], 45 | 'padding-line-between-statements': [ 46 | 'error', 47 | { 48 | blankLine: 'always', 49 | prev: '*', 50 | next: ['return', 'block-like'], 51 | }, 52 | ], 53 | // simple-import-sort plugin 54 | 'simple-import-sort/sort': 'error', 55 | // react-hooks plugin 56 | 'react-hooks/rules-of-hooks': 'error', 57 | 'react-hooks/exhaustive-deps': 'warn', 58 | // typescript 59 | '@typescript-eslint/explicit-module-boundary-types': 'off', 60 | '@typescript-eslint/no-explicit-any': 'off' 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /.github/workflows/chromatic.yml: -------------------------------------------------------------------------------- 1 | name: 'chromatic' 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'src/**' 8 | 9 | jobs: 10 | deploy-storybook: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v1 14 | - uses: borales/actions-yarn@v2.0.0 15 | with: 16 | cmd: install 17 | - uses: borales/actions-yarn@v2.0.0 18 | with: 19 | cmd: build-storybook 20 | - uses: chromaui/action@v1 21 | with: 22 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | storybookBuildDir: storybook-static -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | name: 'production' 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'src/**' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Install dependencies 15 | uses: borales/actions-yarn@v2.0.0 16 | with: 17 | cmd: install 18 | - name: Run lint 19 | uses: borales/actions-yarn@v2.0.0 20 | with: 21 | cmd: lint 22 | - name: Run test 23 | uses: borales/actions-yarn@v2.0.0 24 | with: 25 | cmd: test 26 | - name: Upload coverage 27 | uses: codecov/codecov-action@v1 28 | with: 29 | token: ${{ secrets.CODECOV_TOKEN }} 30 | fail_ci_if_error: true 31 | - name: Run build 32 | uses: borales/actions-yarn@v2.0.0 33 | with: 34 | cmd: build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # packages 2 | node_modules 3 | 4 | # build 5 | dist 6 | 7 | # storybook 8 | storybook-static 9 | build-storybook.log 10 | 11 | # coverage 12 | coverage -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials" 9 | ] 10 | } -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | 2 | export const parameters = { 3 | actions: { argTypesRegex: "^on[A-Z].*" }, 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kim Seonghyeon 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-form 2 | 3 | ![Github Actions](https://github.com/seonghyeonkimm/react-form/workflows/chromatic/badge.svg) 4 | ![Github Actions](https://github.com/seonghyeonkimm/react-form/workflows/production/badge.svg) 5 | 6 | 7 | React library to help to build form easier 8 | 9 | ## Requirement 10 | 11 | - nvm 12 | - node@lts 13 | - yarn 14 | 15 | ## Install 16 | 17 | ```shellscript 18 | yarn install 19 | ``` 20 | 21 | ## Storybook 22 | 23 | ```shellscript 24 | yarn storybook 25 | ``` 26 | 27 | ## Test 28 | 29 | ```shellscript 30 | yarn test 31 | ``` 32 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = module.exports = api => { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | ['@babel/preset-env', { modules: false }], 6 | '@babel/preset-react', 7 | '@babel/preset-typescript', 8 | ]; 9 | const plugins = ["transform-class-properties"]; 10 | 11 | return { presets, plugins }; 12 | }; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | preset: 'ts-jest', 4 | setupFilesAfterEnv: ['./jest.setup.ts'], 5 | collectCoverage: true, 6 | }; 7 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-form", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "webpack --env.NODE_ENV=production", 7 | "test": "jest --passWithNoTests", 8 | "test:watch": "jest --passWithNoTests --watch -o", 9 | "lint": "eslint -c .eslintrc.js --fix --ext .tsx,.ts,.js", 10 | "analyze": "webpack --env.NODE_ENV=production --env.ANALYZE=*", 11 | "storybook": "start-storybook -p 6006", 12 | "build-storybook": "build-storybook" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.11.0", 16 | "@babel/preset-env": "^7.11.0", 17 | "@babel/preset-react": "^7.10.4", 18 | "@babel/preset-typescript": "^7.10.4", 19 | "@date-io/date-fns": "1.x", 20 | "@material-ui/core": "^4.11.0", 21 | "@material-ui/icons": "^4.9.1", 22 | "@material-ui/pickers": "^3.2.10", 23 | "@storybook/addon-actions": "^6.0.26", 24 | "@storybook/addon-essentials": "^6.0.26", 25 | "@storybook/addon-links": "^6.0.26", 26 | "@storybook/react": "^6.0.26", 27 | "@testing-library/jest-dom": "^5.11.4", 28 | "@testing-library/react": "^11.0.4", 29 | "@types/dot-object": "^2.1.2", 30 | "@types/jest": "^26.0.7", 31 | "@types/lodash.get": "^4.4.6", 32 | "@types/lodash.set": "^4.3.6", 33 | "@types/lodash.unset": "^4.5.6", 34 | "@types/react": "^16.9.50", 35 | "@types/react-dom": "^16.9.8", 36 | "@typescript-eslint/eslint-plugin": "^4.3.0", 37 | "@typescript-eslint/parser": "^4.3.0", 38 | "babel-loader": "^8.1.0", 39 | "babel-plugin-transform-class-properties": "^6.24.1", 40 | "circular-dependency-plugin": "^5.2.0", 41 | "clean-webpack-plugin": "^3.0.0", 42 | "date-fns": "^2.16.1", 43 | "eslint": "^7.5.0", 44 | "eslint-config-prettier": "^6.11.0", 45 | "eslint-loader": "^4.0.2", 46 | "eslint-plugin-prettier": "^3.1.4", 47 | "eslint-plugin-react": "^7.20.5", 48 | "eslint-plugin-react-hooks": "^4.0.8", 49 | "eslint-plugin-simple-import-sort": "^5.0.3", 50 | "fork-ts-checker-webpack-plugin": "^5.0.12", 51 | "husky": "^4.2.5", 52 | "jest": "^26.2.1", 53 | "lint-staged": "^10.2.11", 54 | "prettier": "^2.0.5", 55 | "react": "^16.13.1", 56 | "react-dom": "^16.13.1", 57 | "react-is": "^16.13.1", 58 | "source-map-loader": "^1.0.1", 59 | "ts-jest": "^26.1.4", 60 | "tsconfig-paths-webpack-plugin": "^3.3.0", 61 | "typescript": "^4.0.3", 62 | "webpack": "^4.44.1", 63 | "webpack-bundle-analyzer": "^3.8.0", 64 | "webpack-cli": "^3.3.12" 65 | }, 66 | "peerDependencies": { 67 | "react": "^16.13.1" 68 | }, 69 | "husky": { 70 | "hooks": { 71 | "pre-commit": "lint-staged" 72 | } 73 | }, 74 | "lint-staged": { 75 | "src/**/*.{js,jsx,ts,tsx}": [ 76 | "yarn lint", 77 | "yarn test" 78 | ] 79 | }, 80 | "dependencies": { 81 | "lodash.get": "^4.4.2", 82 | "lodash.set": "^4.3.2", 83 | "lodash.unset": "^4.5.2" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Form.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useMemo } from "react"; 2 | 3 | import createFormService, { FormServiceProps } from "./createFormService"; 4 | 5 | export const FormContext = React.createContext< 6 | ReturnType 7 | >(null as any); 8 | 9 | export interface FormProps extends FormServiceProps { 10 | children: ReactNode; 11 | } 12 | 13 | function Form({ children, ...props }: FormProps) { 14 | const form = useMemo(() => { 15 | const formService = createFormService(props); 16 | 17 | return formService; 18 | }, [props]); 19 | 20 | return ( 21 | 22 |
23 | {children} 24 |
25 |
26 | ); 27 | } 28 | 29 | export default Form; 30 | -------------------------------------------------------------------------------- /src/FormConfigProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useMemo } from "react"; 2 | 3 | export type ValidateModeType = "blur" | "change"; 4 | export interface FormConfigProps { 5 | children: ReactNode; 6 | validateMode?: ValidateModeType; 7 | makeErrorProps?: (errors: string[]) => Record; 8 | validateMessage?: { 9 | required?: string; 10 | maxLength?: string; 11 | minLength?: string; 12 | max?: string; 13 | min?: string; 14 | pattern?: string; 15 | }; 16 | } 17 | 18 | export const FormConfigContext = React.createContext<{ 19 | validateMode: ValidateModeType; 20 | makeErrorProps: (errors: string[]) => Record; 21 | validateMessage: Exclude< 22 | Required, 23 | undefined 24 | >; 25 | }>(null as any); 26 | 27 | const DEFAULT_VALIDATE_MESSAGE = { 28 | required: "This field is required", 29 | maxLength: "Should not be longer than maximum length", 30 | minLength: "Should not be shorter than minimum length", 31 | max: "Should not be greater than maximum value", 32 | min: "Should not be smaller than minimum value", 33 | pattern: "Not correct pattern", 34 | }; 35 | 36 | const makeDefaultErrorProps = (errors: string[]) => ({ 37 | helperText: errors.join(", "), 38 | error: errors.length > 0, 39 | }); 40 | 41 | function FormConfigProvider({ 42 | children, 43 | validateMode = "blur", 44 | validateMessage = {}, 45 | makeErrorProps = makeDefaultErrorProps, 46 | }: FormConfigProps) { 47 | const value = useMemo(() => { 48 | return { 49 | validateMode, 50 | makeErrorProps, 51 | validateMessage: { ...DEFAULT_VALIDATE_MESSAGE, ...validateMessage }, 52 | }; 53 | }, [validateMessage, makeErrorProps, validateMode]); 54 | 55 | return ( 56 | 57 | {children} 58 | 59 | ); 60 | } 61 | 62 | export default FormConfigProvider; 63 | -------------------------------------------------------------------------------- /src/FormItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, RefObject, SetStateAction } from "react"; 2 | 3 | import type { ItemPathType, ValueType } from "./createFormService"; 4 | import useFormItem, { ItemRuleType, ValuePropNameType } from "./useFormItem"; 5 | 6 | export interface FormItemProps { 7 | name: string | ItemPathType; 8 | defaultValue?: ValueType; 9 | valuePropName?: ValuePropNameType; 10 | makeErrorProps?: (errors: string[]) => Record; 11 | rules?: ItemRuleType; 12 | validate?: (value: ValueType) => string[]; 13 | children: 14 | | React.ReactNode 15 | | ((props: { 16 | formProps: { 17 | setValue: Dispatch>; 18 | setErrors: Dispatch>; 19 | }; 20 | inputProps: { 21 | ref: RefObject; 22 | onBlur: React.FocusEventHandler; 23 | onChange: React.ChangeEventHandler; 24 | value?: any; 25 | checked?: boolean; 26 | }; 27 | errorProps: Record; 28 | }) => React.ReactNode); 29 | } 30 | 31 | const FormItem: React.FC = ({ 32 | name, 33 | rules, 34 | children, 35 | validate, 36 | makeErrorProps, 37 | defaultValue, 38 | valuePropName = "value", 39 | }: FormItemProps) => { 40 | const { 41 | value, 42 | errorProps, 43 | ref, 44 | onChange, 45 | onBlur, 46 | setValue, 47 | setErrors, 48 | } = useFormItem({ 49 | name, 50 | rules, 51 | validate, 52 | defaultValue, 53 | valuePropName, 54 | makeErrorProps, 55 | }); 56 | 57 | if (isFunction(children)) { 58 | return ( 59 | <> 60 | {children({ 61 | formProps: { 62 | setValue, 63 | setErrors, 64 | }, 65 | errorProps, 66 | inputProps: { 67 | ref, 68 | onBlur, 69 | onChange, 70 | [valuePropName]: value, 71 | }, 72 | })} 73 | 74 | ); 75 | } 76 | 77 | if (React.Children.count(children) > 1) { 78 | console.warn( 79 | "[REACT-FORM Wanring] FormItem accepts only one child. other children will be dropped" 80 | ); 81 | } 82 | 83 | const childrenWithProps = React.Children.map(children, (child, index) => { 84 | if (index > 0) return null; 85 | 86 | if (React.isValidElement(child)) { 87 | return React.cloneElement(child, { 88 | ref, 89 | onBlur, 90 | onChange, 91 | [valuePropName]: value, 92 | ...errorProps, 93 | }); 94 | } 95 | 96 | return child; 97 | }); 98 | 99 | return <>{childrenWithProps}; 100 | }; 101 | 102 | type IsFunction = T extends (...args: any[]) => unknown ? T : never; 103 | const isFunction = (value: T): value is IsFunction => 104 | typeof value === "function"; 105 | 106 | export default FormItem; 107 | -------------------------------------------------------------------------------- /src/createFormService.ts: -------------------------------------------------------------------------------- 1 | import _get from "lodash.get"; 2 | import _set from "lodash.set"; 3 | import _unset from "lodash.unset"; 4 | import React, { ReactText, SetStateAction } from "react"; 5 | 6 | export type ValueType = 7 | | ReactText 8 | | boolean 9 | | Record 10 | | undefined 11 | | null 12 | | ValueType[]; 13 | 14 | export type ItemPathType = [string, ...ReactText[]]; 15 | 16 | export type StoreValueType = { 17 | value?: ValueType; 18 | errors?: string[]; 19 | defaultValue?: ValueType; 20 | setValue?: (value: ValueType) => void; 21 | setErrors?: (errors: string[]) => void; 22 | validate?: () => boolean; 23 | instance?: HTMLElement; 24 | }; 25 | 26 | export interface FormServiceProps { 27 | onSubmit?: (values: Record) => void; 28 | initialValues?: Record; 29 | } 30 | 31 | class FormService { 32 | private store: Record< 33 | string, 34 | StoreValueType | Record 35 | >; 36 | 37 | private registeredItemPaths: ItemPathType[]; 38 | private onSubmit: FormServiceProps["onSubmit"]; 39 | private initialValues: FormServiceProps["initialValues"]; 40 | private subscriptions: Record; 41 | 42 | constructor({ onSubmit, initialValues = {} }: FormServiceProps) { 43 | this.store = {}; 44 | this.onSubmit = onSubmit; 45 | this.registeredItemPaths = []; 46 | this.subscriptions = {}; 47 | this.initialValues = initialValues; 48 | } 49 | 50 | submit = (e?: React.FormEvent) => { 51 | e?.preventDefault(); 52 | const { registeredItemPaths, onSubmit, getItemValue, validateForm } = this; 53 | 54 | this.resetErrors(); 55 | const values = registeredItemPaths.reduce((result, itemPath) => { 56 | _set(result, itemPath, getItemValue(itemPath)); 57 | 58 | return result; 59 | }, {}); 60 | 61 | const isFormValid = validateForm(); 62 | if (!isFormValid) return; 63 | onSubmit && onSubmit(values); 64 | }; 65 | 66 | reset = (e?: React.FormEvent) => { 67 | e?.preventDefault(); 68 | const { registeredItemPaths, setItemValue, getItemValue } = this; 69 | 70 | registeredItemPaths.forEach((itemPath) => { 71 | const current = getItemValue(itemPath); 72 | const initial = this.getItemInitialValue(itemPath); 73 | if (current === initial) return; 74 | setItemValue(itemPath, initial); 75 | }); 76 | 77 | this.resetErrors(); 78 | }; 79 | 80 | resetErrors = () => { 81 | const { registeredItemPaths } = this; 82 | registeredItemPaths.forEach((itemPath) => { 83 | const current = this.getItemErrors(itemPath); 84 | 85 | if (current.length > 0) { 86 | this.setItemError(itemPath, []); 87 | } 88 | }); 89 | }; 90 | 91 | validateForm = () => { 92 | const { store, registeredItemPaths } = this; 93 | let isValid = true; 94 | registeredItemPaths.forEach((itemPath) => { 95 | const current = this.getItemValue(itemPath); 96 | const validateItem = _get(store, [...itemPath, "current", "validate"]); 97 | const result = validateItem(current); 98 | if (!result && isValid) isValid = false; 99 | }); 100 | 101 | return isValid; 102 | }; 103 | 104 | getItemValue = (itemPath: ItemPathType) => { 105 | const { store } = this; 106 | 107 | return _get(store, [...itemPath, "current", "value"]); 108 | }; 109 | 110 | setItemValue = (itemPath: ItemPathType, value: ValueType) => { 111 | const { store } = this; 112 | 113 | const setValue = _get(store, [...itemPath, "current", "setValue"]); 114 | setValue(value); 115 | }; 116 | 117 | getItemErrors = (itemPath: ItemPathType) => { 118 | const { store } = this; 119 | 120 | return _get(store, [...itemPath, "current", "errors"]); 121 | }; 122 | 123 | setItemError = (itemPath: ItemPathType, errors: string[]) => { 124 | const { store } = this; 125 | const setErrors = _get(store, [...itemPath, "current", "setErrors"]); 126 | setErrors(errors); 127 | }; 128 | 129 | getItemInitialValue = (name: string | ItemPathType) => { 130 | const { store, initialValues } = this; 131 | 132 | const itemPath = typeof name === "string" ? [name] : name; 133 | const defaultValue = _get(store, [...itemPath, "current", "defaultValue"]); 134 | 135 | return _get(initialValues, name) || defaultValue; 136 | }; 137 | 138 | subscribe = ( 139 | name: string | ItemPathType, 140 | func: SetStateAction 141 | ) => { 142 | const { subscriptions } = this; 143 | 144 | const current = _get(subscriptions, name) || []; 145 | _set(subscriptions, name, [...current, func]); 146 | }; 147 | 148 | observe = (name: string | ItemPathType, value: ValueType) => { 149 | const subscriptions = _get(this.subscriptions, name) || []; 150 | subscriptions.forEach((action: SetStateAction) => action(value)); 151 | }; 152 | 153 | createOrGetItemRef = (name: string | ItemPathType) => { 154 | const { store, registeredItemPaths } = this; 155 | 156 | const savedRef = _get(store, name); 157 | if (savedRef) return savedRef; 158 | 159 | registeredItemPaths.push(typeof name === "string" ? [name] : name); 160 | const newRef = React.createRef(); 161 | _set(store, name, newRef); 162 | 163 | return newRef; 164 | }; 165 | 166 | removeItemRef = (name: string | ItemPathType) => { 167 | this.registeredItemPaths = this.registeredItemPaths.filter( 168 | (itemPath) => 169 | itemPath.join() !== (typeof name === "string" ? name : name.join()) 170 | ); 171 | _unset(this.store, name); 172 | }; 173 | } 174 | 175 | export default (props: FormServiceProps) => new FormService(props); 176 | -------------------------------------------------------------------------------- /src/index.test.tsx: -------------------------------------------------------------------------------- 1 | // import { render, screen } from "@testing-library/react"; 2 | // import React from "react"; 3 | 4 | // import Main from "./index"; 5 | 6 | describe("Main", () => { 7 | test("return props properly", () => { 8 | expect(true).toEqual(true); 9 | // const name = "demian"; 10 | // render(
); 11 | 12 | // expect(screen.queryByText(new RegExp(name))).toBeInTheDocument(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | // components 2 | export { default as FormConfigProvider } from "./FormConfigProvider"; 3 | export { default as Form } from "./Form"; 4 | export { default as FormItem } from "./FormItem"; 5 | 6 | // hooks 7 | export { default as useForm } from "./useForm"; 8 | export { default as useFormConfig } from "./useFormConfig"; 9 | export { default as useFormItem } from "./useFormItem"; 10 | export { default as useWatch } from "./useWatch"; 11 | -------------------------------------------------------------------------------- /src/stories/Browser.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react/types-6-0"; 2 | import React, { useState } from "react"; 3 | 4 | import type { ValueType } from "../createFormService"; 5 | import Form, { FormProps } from "../Form"; 6 | import FormConfigProvider from "../FormConfigProvider"; 7 | import FormItem from "../FormItem"; 8 | 9 | export default { 10 | title: "react-form/Browser", 11 | component: Form, 12 | subcomponents: { Form, FormItem, FormConfigProvider }, 13 | } as Meta; 14 | 15 | const Exmaple1Template: Story = (args) => { 16 | const [values, setValues] = useState>(); 17 | 18 | const handleFormSubmit = (storeValues: Record) => { 19 | setValues(storeValues); 20 | }; 21 | 22 | return ( 23 | 24 |
25 |
33 | 37 | {({ inputProps, errorProps }) => { 38 | return ( 39 | 44 | 45 | 46 | ); 47 | }} 48 | 49 | 57 | {({ inputProps, errorProps }) => { 58 | return ( 59 | 64 | 65 | 66 | ); 67 | }} 68 | 69 | 77 | {({ inputProps, errorProps }) => { 78 | return ( 79 | 84 |