├── src ├── constants.ts ├── index.ts ├── types.ts ├── utils.ts ├── useForm.ts └── useField.ts ├── .gitignore ├── .babelrc ├── .prettierrc.js ├── scripts ├── deploy.sh ├── definitions.sh ├── build.js └── config.js ├── jest.config.js ├── commitlint.config.js ├── tsconfig.json ├── .eslintrc.json ├── LICENSE ├── package.json └── README.md /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DELAY = 70; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .rpt2_cache 3 | node_modules 4 | *.log 5 | 6 | .vscode -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { useField } from './useField'; 2 | export { useForm } from './useForm'; 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-runtime" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // prettier.config.js or .prettierrc.js 2 | module.exports = { 3 | trailingComma: 'none', 4 | printWidth: 120, 5 | tabWidth: 2, 6 | semi: true, 7 | singleQuote: true, 8 | bracketSpacing: true, 9 | arrowParens: 'avoid', 10 | endOfLine: 'lf' 11 | }; -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # abort on errors 4 | set -e 5 | 6 | # build 7 | npm run docs:build 8 | 9 | # navigate into the build output directory 10 | cd docs/.vuepress/dist 11 | 12 | git init 13 | git add -A 14 | git commit -m 'deploy' 15 | 16 | git push -f git@github.com:logaretm/vue-use-form.git master:gh-pages 17 | cd - 18 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Flag = 2 | | 'valid' 3 | | 'invalid' 4 | | 'validated' 5 | | 'dirty' 6 | | 'pristine' 7 | | 'pending' 8 | | 'touched' 9 | | 'untouched' 10 | | 'changed' 11 | | 'required' 12 | | 'passed' 13 | | 'failed'; 14 | 15 | export interface FormController { 16 | register: (field: { vid: string }) => any; 17 | valueRecords: Record; 18 | } 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ['/tests/setup.js'], 3 | testMatch: ['**/tests/**/*.js', '**/tests/**/*.ts'], 4 | testPathIgnorePatterns: ['/helpers/', '/setup.js'], 5 | collectCoverageFrom: ['src/**/*.ts', '!src/index.ts'], 6 | moduleFileExtensions: ['js', 'ts', 'json', 'vue'], 7 | transform: { 8 | '\\.(ts)$': 'ts-jest', 9 | '^.+\\.jsx?$': 'babel-jest', 10 | '.*\\.(vue)$': '/node_modules/vue-jest' 11 | }, 12 | moduleNameMapper: { 13 | '^vue$': 'vue/dist/vue.common.js', 14 | '^@/(.*)$': '/src/$1' 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /scripts/definitions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "\e[34mRemoving old declarations..." 4 | rm -rf ./dist/types 5 | mkdir -p ./dist/types 6 | echo "\e[92mDone" 7 | 8 | echo "\e[34mGenerating main declarations..." 9 | tsc --emitDeclarationOnly 10 | echo "\e[92mDone" 11 | 12 | echo "\e[34mCleaning up declaration folder..." 13 | mv ./dist/types/src/* ./dist/types 14 | rm -rf ./dist/types/src 15 | echo "\e[92mDone" 16 | 17 | # echo "Generating rule declarations..." 18 | # tsc ./src/rules/index.ts --emitDeclarationOnly --declaration --declarationDir ./dist/types --lib es2017 dom 19 | # echo "Done" 20 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'body-leading-blank': [1, 'always'], 4 | 'footer-leading-blank': [1, 'always'], 5 | 'header-max-length': [2, 'always', 72], 6 | 'scope-case': [2, 'always', 'lower-case'], 7 | 'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']], 8 | 'subject-empty': [2, 'never'], 9 | 'subject-full-stop': [2, 'never', '.'], 10 | 'type-case': [2, 'always', 'lower-case'], 11 | 'type-empty': [2, 'never'], 12 | 'type-enum': [ 13 | 2, 14 | 'always', 15 | ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'] 16 | ] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module": "es2015", 6 | "lib": ["es2015", "es2016", "es2017", "dom"], 7 | "declaration": true, 8 | "declarationDir": "dist/types", 9 | "sourceMap": true, 10 | "outDir": "dist/lib", 11 | "noImplicitAny": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "strictBindCallApply": true, 15 | "strictFunctionTypes": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "typeRoots": ["node_modules/@types"], 19 | "esModuleInterop": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true 22 | }, 23 | "include": ["src", "tests"] 24 | } 25 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const mkdirpNode = require('mkdirp'); 3 | const { promisify } = require('util'); 4 | const { configs, utils, paths } = require('./config'); 5 | 6 | const mkdirp = promisify(mkdirpNode); 7 | 8 | async function build() { 9 | await mkdirp(paths.dist); 10 | // eslint-disable-next-line 11 | console.log(chalk.cyan('Generating ESM builds...')); 12 | await utils.writeBundle(configs.esm, 'vue-use-form.esm.js'); 13 | // eslint-disable-next-line 14 | console.log(chalk.cyan('Done!')); 15 | 16 | // eslint-disable-next-line 17 | console.log(chalk.cyan('Generating UMD build...')); 18 | await utils.writeBundle(configs.umd, 'vue-use-form.js', true); 19 | // eslint-disable-next-line 20 | console.log(chalk.cyan('Done!')); 21 | } 22 | 23 | build(); 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "env": { 7 | "browser": true, 8 | "commonjs": true, 9 | "es6": true, 10 | "jest/globals": true 11 | }, 12 | "extends": [ 13 | "standard", 14 | "plugin:jest/recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "plugin:prettier/recommended", 17 | "prettier/@typescript-eslint" 18 | ], 19 | "plugins": ["jest", "prettier", "@typescript-eslint"], 20 | "rules": { 21 | "@typescript-eslint/camelcase": "off", 22 | "@typescript-eslint/no-explicit-any": "off", 23 | "@typescript-eslint/explicit-function-return-type": "off", 24 | "@typescript-eslint/no-use-before-define": "off" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { isRef, Ref } from '@vue/composition-api'; 2 | 3 | export function isObject(obj: unknown): obj is Record { 4 | return obj !== null && obj && typeof obj === 'object' && !Array.isArray(obj); 5 | } 6 | 7 | export function debounce(wait = 0, fn: T, token = { cancelled: false }) { 8 | if (wait === 0) { 9 | return fn; 10 | } 11 | 12 | let timeout: ReturnType | undefined; 13 | 14 | return (((...args: any[]) => { 15 | const later = () => { 16 | timeout = undefined; 17 | 18 | // check if the fn call was cancelled. 19 | if (!token.cancelled) fn(...args); 20 | }; 21 | 22 | // because we might want to use Node.js setTimout for SSR. 23 | clearTimeout(timeout as any); 24 | timeout = setTimeout(later, wait) as any; 25 | }) as any) as T; 26 | } 27 | 28 | export function hasRefs(obj: unknown): obj is Record> { 29 | if (!isObject(obj)) { 30 | return false; 31 | } 32 | 33 | return Object.keys(obj).some(key => isRef(obj[key])); 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Abdelrahman Awad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-use-form", 3 | "version": "0.0.1", 4 | "description": "Use the Vue.js composition functions API for form validation, powered by vee-validate.", 5 | "module": "dist/vue-use-form.esm.js", 6 | "unpkg": "dist/vue-use-form.js", 7 | "main": "dist/vue-use-form.js", 8 | "typings": "dist/types/index.d.ts", 9 | "author": "Abdelrahman Awad ", 10 | "license": "MIT", 11 | "scripts": { 12 | "test": "jest", 13 | "lint": "eslint . '**/*.{js,jsx,ts,tsx}' --fix", 14 | "build:main": "node scripts/build.js", 15 | "ts:defs": "./scripts/definitions.sh", 16 | "build": "npm run build:main && npm run ts:defs", 17 | "cover": "jest --coverage", 18 | "prepublishOnly": "npm run build" 19 | }, 20 | "files": [ 21 | "dist/*.js", 22 | "dist/types/**/*.d.ts" 23 | ], 24 | "devDependencies": { 25 | "@commitlint/cli": "^8.2.0", 26 | "@types/jest": "^24.0.22", 27 | "@types/mkdirp": "^0.5.2", 28 | "@typescript-eslint/eslint-plugin": "^2.6.1", 29 | "@typescript-eslint/parser": "^2.6.1", 30 | "@vue/composition-api": "^0.3.2", 31 | "@vue/test-utils": "^1.0.0-beta.29", 32 | "eslint": "^6.6.0", 33 | "eslint-config-prettier": "^6.5.0", 34 | "eslint-config-standard": "^14.1.0", 35 | "eslint-plugin-import": "^2.18.2", 36 | "eslint-plugin-jest": "^23.0.2", 37 | "eslint-plugin-prettier": "^3.1.1", 38 | "filesize": "^6.0.0", 39 | "gzip-size": "^5.1.1", 40 | "husky": "^3.0.9", 41 | "jest": "^24.9.0", 42 | "lint-staged": "^9.4.2", 43 | "mkdirp": "^0.5.1", 44 | "prettier": "^1.18.2", 45 | "rollup": "^1.26.3", 46 | "rollup-plugin-commonjs": "^10.1.0", 47 | "rollup-plugin-node-resolve": "^5.2.0", 48 | "rollup-plugin-replace": "^2.2.0", 49 | "rollup-plugin-typescript2": "^0.25.2", 50 | "ts-jest": "^24.1.0", 51 | "ts-node": "^8.4.1", 52 | "typescript": "^3.7.2", 53 | "uglify-js": "^3.6.7", 54 | "vue": "^2.6.10" 55 | }, 56 | "dependencies": { 57 | "vee-validate": "^3.0.11" 58 | }, 59 | "peerDependencies": { 60 | "@vue/composition-api": "^0.3.2", 61 | "vue": "^2.6.10" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/useForm.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, Ref } from '@vue/composition-api'; 2 | import { Flag, FormController } from './types'; 3 | 4 | interface FormOptions {} 5 | 6 | const mergeStrategies: Record = { 7 | valid: 'every', 8 | invalid: 'some', 9 | dirty: 'some', 10 | pristine: 'every', 11 | touched: 'some', 12 | untouched: 'every', 13 | pending: 'some', 14 | validated: 'every', 15 | changed: 'some', 16 | passed: 'every', 17 | failed: 'some', 18 | required: 'some' 19 | }; 20 | 21 | function computeFlags(fields: Ref) { 22 | const flags: Flag[] = Object.keys(mergeStrategies) as Flag[]; 23 | 24 | return flags.reduce( 25 | (acc, flag: Flag) => { 26 | acc[flag] = computed(() => { 27 | return fields.value[mergeStrategies[flag]](field => field[flag]); 28 | }); 29 | 30 | return acc; 31 | }, 32 | {} as Record> 33 | ); 34 | } 35 | 36 | interface FormComposite { 37 | form: FormController; 38 | errors: Ref>; 39 | reset: () => void; 40 | handleSubmit: (fn: Function) => Promise; 41 | validate: () => Promise; 42 | } 43 | 44 | export function useForm(opts?: FormOptions): FormComposite { 45 | const fields: Ref = ref([]); 46 | const fieldsById: Record = {}; // for faster access 47 | const controller: FormController = { 48 | register(field) { 49 | fields.value.push(field); 50 | fieldsById[field.vid] = field; 51 | // TODO: Register watchers for cross field validation. 52 | // Requires: vee-validate exposed normalizeRules function to map fields. 53 | }, 54 | get valueRecords() { 55 | return fields.value.reduce((acc: any, field: any) => { 56 | acc[field.vid] = field.model.value; 57 | 58 | return acc; 59 | }, {}); 60 | } 61 | }; 62 | 63 | const validate = async () => { 64 | const results = await Promise.all( 65 | fields.value.map((f: any) => { 66 | return f.validate(); 67 | }) 68 | ); 69 | 70 | return results.every(r => r.valid); 71 | }; 72 | 73 | const errors = computed(() => { 74 | return fields.value.reduce((acc: Record, field) => { 75 | acc[field.vid] = field.errors.value; 76 | 77 | return acc; 78 | }, {}); 79 | }); 80 | 81 | const reset = () => { 82 | fields.value.forEach((f: any) => f.reset()); 83 | }; 84 | 85 | return { 86 | errors, 87 | ...computeFlags(fields), 88 | form: controller, 89 | validate, 90 | reset, 91 | handleSubmit: (fn: Function) => { 92 | return validate().then(result => { 93 | if (result && typeof fn === 'function') { 94 | return fn(); 95 | } 96 | }); 97 | } 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const { rollup } = require('rollup'); 4 | const filesize = require('filesize'); 5 | const uglify = require('uglify-js'); 6 | const chalk = require('chalk'); 7 | const gzipSize = require('gzip-size'); 8 | const typescript = require('rollup-plugin-typescript2'); 9 | const resolve = require('rollup-plugin-node-resolve'); 10 | const replace = require('rollup-plugin-replace'); 11 | 12 | const version = process.env.VERSION || require('../package.json').version; 13 | 14 | const commons = { 15 | banner: `/** 16 | * vee-validate-fns v${version} 17 | * (c) ${new Date().getFullYear()} Abdelrahman Awad 18 | * @license MIT 19 | */`, 20 | outputFolder: path.join(__dirname, '..', 'dist'), 21 | uglifyOptions: { 22 | compress: true, 23 | mangle: true 24 | } 25 | }; 26 | 27 | const paths = { 28 | dist: commons.outputFolder 29 | }; 30 | 31 | const utils = { 32 | stats({ path, code }) { 33 | const { size } = fs.statSync(path); 34 | const gzipped = gzipSize.sync(code); 35 | 36 | return `| Size: ${filesize(size)} | Gzip: ${filesize(gzipped)}`; 37 | }, 38 | async writeBundle({ input, output }, fileName, minify = false) { 39 | const bundle = await rollup(input); 40 | const { 41 | output: [{ code }] 42 | } = await bundle.generate(output); 43 | 44 | let outputPath = path.join(paths.dist, fileName); 45 | fs.writeFileSync(outputPath, code); 46 | let stats = this.stats({ code, path: outputPath }); 47 | // eslint-disable-next-line 48 | console.log(`${chalk.green('Output File:')} ${fileName} ${stats}`); 49 | 50 | if (minify) { 51 | const minifiedFileName = fileName.replace('.js', '') + '.min.js'; 52 | outputPath = path.join(paths.dist, minifiedFileName); 53 | fs.writeFileSync(outputPath, uglify.minify(code, commons.uglifyOptions).code); 54 | stats = this.stats({ code, path: outputPath }); 55 | // eslint-disable-next-line 56 | console.log(`${chalk.green('Output File:')} ${minifiedFileName} ${stats}`); 57 | } 58 | 59 | return true; 60 | } 61 | }; 62 | 63 | const builds = { 64 | umd: { 65 | input: 'src/index.ts', 66 | format: 'umd', 67 | name: 'VueUseForm', 68 | env: 'production' 69 | }, 70 | esm: { 71 | input: 'src/index.ts', 72 | format: 'es' 73 | } 74 | }; 75 | 76 | function genConfig(options) { 77 | const config = { 78 | input: { 79 | input: options.input, 80 | external: ['vue', 'vee-validate', '@vue/composition-api'], 81 | plugins: [ 82 | typescript({ typescript: require('typescript'), useTsconfigDeclarationDir: true }), 83 | replace({ __VERSION__: version }), 84 | resolve() 85 | ] 86 | }, 87 | output: { 88 | banner: commons.banner, 89 | format: options.format, 90 | name: options.name, 91 | globals: { 92 | vue: 'Vue', 93 | 'vee-validate': 'VeeValidate', 94 | '@vue/composition-api': 'vueCompositionApi' 95 | } 96 | } 97 | }; 98 | 99 | if (options.env) { 100 | config.input.plugins.unshift( 101 | replace({ 102 | 'process.env.NODE_ENV': JSON.stringify(options.env) 103 | }) 104 | ); 105 | } 106 | 107 | return config; 108 | } 109 | 110 | const configs = Object.keys(builds).reduce((prev, key) => { 111 | prev[key] = genConfig(builds[key]); 112 | 113 | return prev; 114 | }, {}); 115 | 116 | module.exports = { 117 | configs, 118 | utils, 119 | uglifyOptions: commons.uglifyOptions, 120 | paths 121 | }; 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-use-form 2 | 3 | **⚠ Deprecated. ⚠** 4 | 5 | This project will be continued in the future releases of vee-validate starting with `vee-validate@4` which will come with Vue.js 3 support. 6 | 7 | --- 8 | 9 | This is a [Vue composition API](https://github.com/vuejs/composition-api) function that allows you to do form validation, powered by vee-validate. 10 | 11 | ## Install 12 | 13 | **⚠ Not production ready yet. ⚠** 14 | 15 | ```sh 16 | yarn add vue-use-form 17 | 18 | # OR 19 | 20 | npm i vue-use-form 21 | ``` 22 | 23 | ## Usage 24 | 25 | In your component file: 26 | 27 | ```js 28 | import { ref } from '@vue/composition-api'; 29 | import { useForm, useField } from 'vue-use-form'; 30 | 31 | export default { 32 | setup() { 33 | const fname = ref(''); 34 | const { form, submit } = useForm({ 35 | onSubmit () { 36 | console.log('Submitting!', { 37 | fname: fname.value 38 | }); 39 | } 40 | }); 41 | 42 | const { errors } = useField('firstName', { 43 | rules: 'required', 44 | value: fname, 45 | form 46 | }); 47 | 48 | return { fname, errors, submit }; 49 | } 50 | }; 51 | ``` 52 | 53 | In your Template: 54 | 55 | ```vue 56 |
57 | 58 | {{ errors[0] }} 59 | 60 |
61 | ``` 62 | 63 | ## API 64 | 65 | ### useForm(object) 66 | 67 | The `useForm` function accepts options to configure the form. 68 | 69 | ```js 70 | const { form, submit } = useForm({ 71 | onSubmit () { 72 | // this will only run when the form is valid. 73 | // send to server! 74 | } 75 | }); 76 | ``` 77 | 78 | It returns an object containing two properties: 79 | 80 | - `form`: A form controller object, which can be used with `useField` to associate fields in the same form. 81 | - `submit`: A safe form submit handler to bind to your submission handler, it will validate all the fields before it runs the given `onSubmit` handler. 82 | 83 | ### useField(string, string | object) 84 | 85 | The `useField` function accepts a `string` which is the input name, and options to configure the field: 86 | 87 | ```js 88 | const field = useField('firstName', { 89 | rules: 'required', // vee-validate style rules, can be a Ref. 90 | value: fname, // the initial field value, optional. 91 | form // a form controller object returned from "useForm", used to group fields. optional. 92 | }); 93 | ``` 94 | 95 | It returns the following members: 96 | 97 | | Prop | Type | Description | 98 | | -------- | ---------------------- | -------------------------------------------------------------------------------------- | 99 | | flags | `ValidationFlags` | Object containing vee-validate flags for that field. | 100 | | errors | `string[]` | list of validation errors | 101 | | validate | `() => ValidationResult` | Triggers validation for the field. | 102 | | reset | `() => void` | Resets validation state for the field. | 103 | | onInput | `() => void` | Updates some flags when the user inputs, you should bind it to the element. | 104 | | onBlur | `() => void` | Updates some flags when the user focuses the field, you should bind it to the element. | 105 | | value | `Ref` | A default ref in-case you didn't pass the initial value | 106 | 107 | ## Credits 108 | 109 | - API Inspired by some react libraries: [Formik](https://jaredpalmer.com/formik/), [react-final-form-hooks](https://github.com/final-form/react-final-form-hooks). 110 | - Powered by [vee-validate](https://github.com/baianat/vee-validate). 111 | 112 | ## License 113 | 114 | MIT 115 | -------------------------------------------------------------------------------- /src/useField.ts: -------------------------------------------------------------------------------- 1 | import { watch, ref, reactive, Ref, isRef, toRefs, computed, onMounted } from '@vue/composition-api'; 2 | import { validate } from 'vee-validate'; 3 | import { ValidationFlags, ValidationResult } from 'vee-validate/dist/types/types'; 4 | import { Flag, FormController } from './types'; 5 | import { debounce, hasRefs } from './utils'; 6 | import { DELAY } from './constants'; 7 | 8 | type RuleExp = string | Record; 9 | 10 | interface FieldOptions { 11 | value: Ref; 12 | rules: RuleExp | Ref; 13 | immediate: boolean; 14 | form?: FormController; 15 | } 16 | 17 | type FieldAugmentedOptions = string | Ref | FieldOptions; 18 | 19 | export function useFlags() { 20 | const flags: ValidationFlags = reactive(createFlags()); 21 | const passed = computed(() => { 22 | return flags.valid && flags.validated; 23 | }); 24 | 25 | const failed = computed(() => { 26 | return flags.invalid && flags.validated; 27 | }); 28 | 29 | return { 30 | ...toRefs(flags), 31 | passed, 32 | failed 33 | }; 34 | } 35 | 36 | export function useField(fieldName: string, opts?: FieldAugmentedOptions) { 37 | const errors: Ref = ref([]); 38 | const { value, rules, form, immediate } = normalizeOptions(opts); 39 | const initialValue = value.value; 40 | const flags = useFlags(); 41 | 42 | function commitResult(result: ValidationResult) { 43 | errors.value = result.errors; 44 | flags.changed.value = initialValue !== value.value; 45 | flags.valid.value = result.valid; 46 | flags.invalid.value = !result.valid; 47 | flags.validated.value = true; 48 | flags.pending.value = false; 49 | } 50 | 51 | const validateField = async (newVal: any): Promise => { 52 | flags.pending.value = true; 53 | const result = await validate(newVal, isRef(rules) ? rules.value : rules, { 54 | name: fieldName, 55 | values: form?.valueRecords ?? {} 56 | }); 57 | 58 | commitResult(result); 59 | 60 | return result; 61 | }; 62 | 63 | const handler = debounce(DELAY, validateField); 64 | 65 | watch(value, handler, { 66 | lazy: true 67 | }); 68 | 69 | if (isRef(rules)) { 70 | watch(rules as Ref, handler, { 71 | lazy: true 72 | }); 73 | } else if (hasRefs(rules)) { 74 | Object.keys(rules).forEach(key => { 75 | if (!isRef(rules[key])) { 76 | return; 77 | } 78 | 79 | watch(rules[key], handler, { lazy: true }); 80 | }); 81 | } 82 | 83 | const reset = () => { 84 | const defaults = createFlags(); 85 | Object.keys(flags).forEach((key: string) => { 86 | flags[key as Flag].value = defaults[key as Flag]; 87 | }); 88 | 89 | errors.value = []; 90 | }; 91 | 92 | const onBlur = () => { 93 | flags.touched.value = true; 94 | flags.untouched.value = false; 95 | }; 96 | 97 | const onInput = () => { 98 | flags.dirty.value = true; 99 | flags.pristine.value = false; 100 | }; 101 | 102 | onMounted(() => { 103 | validate(initialValue, isRef(rules) ? rules.value : rules).then(result => { 104 | if (immediate) { 105 | commitResult(result); 106 | return; 107 | } 108 | 109 | // Initial silent validation. 110 | flags.valid.value = result.valid; 111 | flags.invalid.value = !result.valid; 112 | }); 113 | }); 114 | 115 | const field = { 116 | vid: fieldName, 117 | model: value, 118 | ...flags, 119 | errors, 120 | reset, 121 | validate: validateField, 122 | onInput, 123 | onBlur 124 | }; 125 | 126 | form?.register(field); 127 | 128 | return field; 129 | } 130 | 131 | function normalizeOptions(opts: FieldAugmentedOptions | undefined): FieldOptions { 132 | const defaults = { 133 | value: ref(''), 134 | immediate: false, 135 | rules: '' 136 | }; 137 | 138 | if (!opts) { 139 | return defaults; 140 | } 141 | 142 | if (isRef(opts)) { 143 | return { 144 | ...defaults, 145 | rules: opts 146 | }; 147 | } 148 | 149 | if (typeof opts === 'string') { 150 | return { 151 | ...defaults, 152 | rules: opts 153 | }; 154 | } 155 | 156 | return { 157 | ...defaults, 158 | ...(opts ?? {}) 159 | }; 160 | } 161 | 162 | function createFlags(): Record { 163 | return { 164 | changed: false, 165 | valid: false, 166 | invalid: false, 167 | touched: false, 168 | untouched: true, 169 | dirty: false, 170 | pristine: true, 171 | validated: false, 172 | pending: false, 173 | required: false, 174 | passed: false, 175 | failed: false 176 | }; 177 | } 178 | --------------------------------------------------------------------------------