diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9196583 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Node +node_modules +pnpm-lock.yaml +package-lock.json + +# Parcel +.cache +public + +# Generated +dist \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..6818979 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +* +!lib/ \ No newline at end of file diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..0014c57 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,84 @@ +import React, { useState } from 'react' + + + +export type useFormExtractor = (from: any) => any + +export type useFormAutoOptions = { + getter?: string, + setter?: string, + extractor?: useFormExtractor +} + +export type useFormOptions = { + extractor?: useFormExtractor, +} + +export type useFormValidatorFunction = ((s: any) => boolean | Promise) +export type useFormValidatorMethod = useFormValidatorFunction | RegExp + +export type useFormValidatorObject = { + validator: useFormValidatorMethod, + message?: string, +} + +export type useFormValidator = useFormValidatorMethod | useFormValidatorObject + +export const HTMLInputExtractor: useFormExtractor = (e: React.FormEvent) => e.currentTarget.value +export const HTMLCheckboxExtractor: useFormExtractor = (e: React.FormEvent) => e.currentTarget.checked + +export const useForm = (init: T, validators: Partial = {}, options: useFormOptions = {}) => { + const [form, setForm] = useState(init) + + const [errors, setErrors] = useState>({}) + + const _set = (key: keyof T, value: any) => { + setForm({ + ...form, + [key]: value, + }) + } + + const _validateAll = async (value: any, validator: useFormValidatorMethod): Promise => { + if (validator.constructor.name === 'Function' || validator.constructor.name === 'AsyncFunction') + return (validator as useFormValidatorFunction)(value) + else if (validator.constructor.name === 'RegExp') + return (validator as RegExp).test(value) + else return false + } + + const _getValidatorMessage = (key: keyof T): string => { + // @ts-ignore + if (validators[key] && validators[key].message) return validators[key].message + else return `Error in: ${key}` + } + + const _validate = (key: keyof T, value: any) => { + const validator: useFormValidator | undefined = validators[key] + if (!validator) return + + // @ts-ignore + _validateAll(value, validator.constructor.name === 'Object' ? (validator as useFormValidatorObject).validator : validator) + .then((valid: boolean) => { + setErrors({ + ...errors, + [key]: valid + ? undefined + : _getValidatorMessage(key), + }) + }) + } + + const update = (key: keyof T, extractor = options.extractor) => (value: any) => { + const extracted = extractor ? extractor(value) : HTMLInputExtractor(value) + _set(key, extracted) + _validate(key, extracted) + } + + const auto = (key: keyof T, opts: useFormAutoOptions = {}) => ({ + [opts.getter || 'onChange']: update(key, opts.extractor), + [opts.setter || 'value']: form[key] as any, + }) + + return { form, update, auto, errors } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..9f5414a --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "formhero", + "version": "0.0.1", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "scripts": { + "test": "parcel -d public ./test/index.html", + "build": "tsc -w", + "dev": "pnpm run build & pnpm run test" + }, + "browserslist": [ + "last 1 year" + ], + "devDependencies": { + "@types/react": "^16.9.3", + "@types/react-dom": "^16.9.1", + "parcel-bundler": "^1.12.3", + "react": "^16.9.0", + "react-dom": "^16.9.0", + "typescript": "^3.6.2" + } +} diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..00d7936 --- /dev/null +++ b/test/index.html @@ -0,0 +1,27 @@ + + + + + + + Form Hero + + + + + +
+ + + + \ No newline at end of file diff --git a/test/test.tsx b/test/test.tsx new file mode 100644 index 0000000..71b5500 --- /dev/null +++ b/test/test.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react' +import ReactDOM from 'react-dom' + +import { useForm, HTMLInputExtractor } from '../' + + +const TextError: React.FC<{ error?: string }> = ({ error }) => !error + ? null + :
{error}
+ +const Index: React.FC = () => { + + const { auto, form, update, errors } = useForm({ + username: '', + password: '', + type: 'formhero', + awesome: true, + }, { + username: /^test/, + password: { + validator: /^.{3,}$/, + message: 'To short', + }, + awesome: (value) => !!value + }, { extractor: HTMLInputExtractor }) + + const _submit = (e: React.MouseEvent) => { + e.preventDefault() + console.log(form, errors) + } + + return ( +
+
+
Username
+ + +
+ +
Password
+ + +
+ +
Which one to choose?
+ +
+ +
Is it awesome?
+ e.target.checked })} /> + +
+ + + +
+ ) +} + +ReactDOM.render(, document.getElementById('root')) + +// @ts-ignore +// if (module.hot) module.hot.accept() \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..348bde8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "jsx": "react", + "outDir": "./dist", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": [ + "./lib" + ] +} \ No newline at end of file